Схема и Python-модель разошлись
12 августа 2026 года в официальном репозитории openai/codex появился issue #38158. В нём указано, что сгенерированный Python-тип TokenUsageBreakdown не содержит поля cacheWriteInputTokens, хотя это поле описано в JSON Schema протокола Codex app-server v2.
На первый взгляд это локальный дефект типизации. Для команды, которая использует данные Python-клиента, чтобы оценивать стоимость агентных запусков, разбирать всплески token usage и принимать решения о повторном использовании длинного контекста, дефект создаёт вполне практическую слепую зону.
Важно не расширять установленный факт. Нет данных о том, что OpenAI неверно списывает деньги, меняет серверные лимиты или допускает двойное списание токенов. Подтверждён другой дефект: клиентская Python-модель может не сохранить показатель записи токенов в кэш, который способен передать протокол.
В issue #38158 автор сопоставляет исходную схему app-server v2 с Python bindings. В схеме присутствует cacheWriteInputTokens, а в типе TokenUsageBreakdown этого поля нет. На момент, зафиксированный в редакционном досье, обращение оставалось открытым, без назначенного исполнителя и связанного pull request.
Опубликованный Python-файл подтверждает вторую часть сообщения. В классе TokenUsageBreakdown определены cachedInputTokens, inputTokens, outputTokens, reasoningOutputTokens и totalTokens. Ни cacheWriteInputTokens, ни варианта в snake_case cache_write_input_tokens в опубликованном файле нет.
JSON Schema app-server v2, напротив, описывает cacheWriteInputTokens как целочисленное поле со значением по умолчанию 0. Поле не является обязательным, поэтому протокол допускает отсутствие ключа в конкретном сообщении. Но если сервер передал ключ, текущий Python-слой не даёт приложению типизированного доступа к нему.
Вывод здесь должен оставаться узким. Публичные артефакты подтверждают рассинхронизацию между схемой и Python-генерацией. Без проверки конкретного коммита нельзя утверждать, что дефект уже исправлен или обязательно сохранится в следующем релизе.
- Подтверждено: поле присутствует в JSON Schema app-server v2.
- Подтверждено: поле отсутствует в опубликованном Python-классе TokenUsageBreakdown.
- Не подтверждено: ошибка в серверном биллинге, расчёте rate limits или публичном Python SDK openai для Responses API.
Что измеряют cache reads и cache writes
Prompt caching позволяет сервису повторно использовать ранее обработанную часть входного контекста. В агентных и code-сценариях это особенно заметно: в последовательности запросов могут участвовать системный prompt, описание репозитория, инструкции, результаты инструментов или история треда.
cachedInputTokens и cacheWriteInputTokens описывают разные показатели. Первый связан с чтением из кэша, второй с записью в него. Наличие обоих полей в схеме не означает, что они образуют простое, исчерпывающее и непересекающееся разбиение inputTokens.
Поэтому формула inputTokens минус cachedInputTokens не позволяет надёжно восстановить cache writes. Схема не задаёт такую семантику. Попытка достроить отсутствующее поле арифметикой создаст ещё одно допущение, которое легко забыть при интерпретации дашборда.
Иллюстративный payload может включать inputTokens, cachedInputTokens, cacheWriteInputTokens, outputTokens, reasoningOutputTokens и totalTokens. Если при десериализации исчезает только cacheWriteInputTokens, итоговый totalTokens сам по себе не становится неверным. Но аналитик теряет одно из измерений структуры потребления.
Для инженерной телеметрии это существенная разница. Рост totalTokens показывает, что расход увеличился. Ненулевое число cache writes могло бы указывать на прогрев кэша или на изменение кэшируемого префикса. Без этого поля команда может ошибочно связать скачок с удлинением prompt, новой tool loop или ухудшением cache hit rate.
Баг не делает такую ошибку неизбежной, но повышает риск неверной интерпретации при неполной наблюдаемости. В системах с повторяемым длинным контекстом этого уже достаточно, чтобы аккуратнее относиться к удобному объекту SDK.
- cache read: использование уже доступного кэшированного контекста.
- cache write: запись контекста в кэш, если соответствующий режим и модель её поддерживают.
- inputTokens, cachedInputTokens и cacheWriteInputTokens не следует сводить к неоговорённой арифметической формуле.
Почему пропущенное поле может исказить оценку расходов
Для GPT-5.6 и более поздних моделей OpenAI публично описала отдельную экономику prompt caching. Cache writes тарифицируются по ставке 1,25× от некэшированного input, а cache reads получают скидку 90% относительно обычного входного тарифа. Компания также заявляет минимальное время жизни prompt cache в 30 минут.
Если конкретный запуск использует модель, тариф и провайдера, к которым применимо это правило, cache-write-компонент можно оценить как cacheWriteInputTokens × 1,25 × P, где P является известной ставкой обычного input token. Это рабочая модель для оперативной аналитики, но не универсальная формула инвойса.
Практический риск следует именно отсюда. Дашборд, который считает входную часть расходов только как inputTokens × P или пытается вывести скидку лишь из cachedInputTokens, может не учесть отдельную составляющую cache writes. Прогноз затрат на длинные агентные цепочки окажется неполным в момент, когда команда оценивает эффект кэширования.
Из неполной клиентской оценки нельзя заключать, что провайдер недосчитывает или завышает счёт. Схема app-server не содержит полной тарифной таблицы, данных о валюте, правилах округления, конкретной модели, провайдере или финальном механизме выставления счёта.
Репозиторий Codex указывает, что Codex CLI может использовать как ChatGPT-план, так и API key. Значение в telemetry не равно готовой финансовой проводке. Для расчёта реальных затрат важны конкретный продукт, способ авторизации, модель, провайдер и условия тарифа.
Проблема относится к локальной модели стоимости. Если она неполна, бюджетный прогноз или объяснение скачка могут оказаться неточными. Это не доказательство неправильной работы биллинга OpenAI.
- Факт: OpenAI заявляет коэффициент 1,25× для cache writes в описанном ценообразовании GPT-5.6 и более поздних моделей.
- Вывод: если этот тариф применим, отсутствие поля делает локальную cost-модель неполной.
- Не следует утверждать: «баг завышает счёт», «баг уменьшает лимит» или «OpenAI дважды списывает токены».
Лимиты: проблема не в расчёте, а в объяснимости расхода
У issue #38158 есть метка rate-limits, но сама по себе она не означает признания ошибки в серверном ограничителе. Наличие или отсутствие свойства в клиентском классе не меняет фактическое потребление, которое видит инфраструктура провайдера. Оно меняет лишь то, насколько точно разработчик может сопоставить свои наблюдения с поведением системы.
Это важно для команд с budget caps и алертами на уровне треда, пользователя или агента. Если внутреннее правило останавливает процесс на основе собственной оценки стоимости, а эта оценка не учитывает cache-write-компонент, лимит бюджета может сработать позже ожидаемого.
Если telemetry используют для расследования перегрузки, отсутствие поля затрудняет поиск причины. Речь идёт о риске в пользовательской логике контроля, а не о доказательстве нарушения внешних API limits.
Связанный issue #33880 описывает и другие ограничения telemetry app-server. TokenUsageBreakdown не всегда позволяет восстановить число upstream-запросов в рамках turn, а при прерванном turn финальное usage-уведомление может не прийти.
Вместе с issue #38158 это не доказывает системную неверность telemetry. Но эти случаи показывают, почему строгая цепочка «один turn, все запросы, все токены, точная стоимость» требует собственных сырых событий и последующей сверки.
Практический принцип прост: telemetry нужна для оперативного управления и диагностики, а финансовые данные провайдера нужны для окончательной сверки. Подменять одно другим рискованно и после исправления отдельного поля.
- Серверный rate limit и локальный budget cap являются разными контурами контроля.
- Пропуск cacheWriteInputTokens может ухудшить локальные прогнозы и алерты.
- Финальную финансовую сверку следует проводить по официальным usage/cost exports и инвойсу конкретного провайдера, а не по одному объекту SDK.
Граница проблемы: это не общий баг всех Python-интеграций OpenAI
В обсуждении легко смешать библиотеки со схожими названиями. Issue #38158 относится к Python bindings в репозитории openai/codex и к протоколу Codex app-server v2. Это не равнозначно заявлению о дефекте публичной библиотеки openai для прямой работы с Responses API.
Сам Codex описан в репозитории как coding agent, который запускается локально. Его интеграционный слой, модель событий и usage-telemetry нужно рассматривать отдельно от контрактов других API-клиентов.
Распространять вывод на все Python-проекты OpenAI без отдельного воспроизводимого примера технически неверно. Такая формулировка подменила бы конкретный дефект в генерации модели широким утверждением о нескольких продуктах, для которого в доступных источниках нет подтверждения.
Есть и ещё одно ограничение. Даже если raw JSON-RPC frame содержит cacheWriteInputTokens, это не доказывает, что каждое такое значение должно учитываться в расчёте по коэффициенту GPT-5.6.
Тарифные условия зависят от режима использования, модели и провайдера. Связь между протокольным полем и конкретной строкой счёта нужно проверять по актуальной документации выбранного продукта.
Разделение границ проблемы защищает и техническую коммуникацию внутри команды. В баг-репорте, документации дашборда и инцидентном разборе лучше прямо называть задействованный контур: Codex app-server v2, Python bindings и конкретный путь десериализации.
- Объект расследования: openai/codex, Python bindings и app-server v2.
- Необоснованное расширение: «сломана вся библиотека openai» или «все API-пользователи недоплачивают».
- Отдельная проверка нужна для каждой комбинации модели, способа авторизации и провайдера.
Что делать сейчас: сохранять данные до десериализации
Наиболее полезная временная мера состоит в том, чтобы считать типизированный TokenUsageBreakdown неполным объектом представления и отдельно сохранять исходные JSON-RPC-уведомления thread/tokenUsage/updated. Схема предусматривает tokenUsage.last для последнего шага и tokenUsage.total для накопленной статистики по thread.
Raw payload стоит сохранить до передачи в сгенерированную Python-модель. Это не означает отказ от SDK. Типизированная модель остаётся удобной для обычного кода, но не должна быть единственным хранилищем измерений, если команда расследует расход и кэширование.
Для хранилища событий действует критическое правило: отсутствие ключа нельзя автоматически превращать в ноль. Схема задаёт default 0, однако для аудита нужно различать как минимум три состояния.
Первое состояние: сервер передал cacheWriteInputTokens со значением 0. Второе: сервер не передал ключ. Третье: ключ присутствовал в raw payload, но исчез в SDK-представлении. Именно последнее состояние позволяет локализовать рассматриваемый дефект, не смешивая его с отсутствием значения на проводе.
В качестве временного совместимого слоя можно использовать собственную Pydantic-модель или обычный словарь с alias cacheWriteInputTokens. Такой слой не исправит SDK и не изменит серверную семантику. Его задача скромнее: сохранить необработанное измерение в телеметрии до появления проверяемого исправления в официальной генерации.
Данные следует разделить и на уровне архитектуры. Raw-события отвечают на вопрос, что показал app-server. Версионируемая формула с тарифами описывает оценку стоимости в реальном времени. Экспорт usage/cost и инвойс определяют данные для финансовой сверки. Один столбец в дашборде не должен выдавать себя за все три источника истины.
- Логируйте raw JSON-RPC frame до SDK-десериализации.
- Храните статус поля: reported, absent_in_wire_payload или dropped_by_sdk.
- Версионируйте тарифы в cost-модели по модели, дате и провайдеру.
- Не используйте telemetry из app-server как единственное основание для финансового закрытия периода.
Минимальный патч и тест, который не даст проблеме вернуться
Временный extractor можно сделать консервативным. Сначала он проверяет наличие ключа в raw-словаре, затем приводит значение к int и возвращает вместе со значением сведения о его происхождении.
Не следует возвращать 0 при любом отсутствующем ключе. Такая нормализация стирает различие между нулевым значением и отсутствующей информацией, а именно это различие важно для аудита и последующего поиска потери данных.
Для типизированного обходного решения подойдёт совместимая модель с alias cacheWriteInputTokens и полем cache_write_input_tokens. Перед внедрением в production её нужно проверить на реальном или записанном raw frame: события могут проходить через промежуточный gateway, который также способен преобразовывать поля.
Регрессионный тест должен охватывать весь путь, а не только схему. Сначала CI подтверждает, что cacheWriteInputTokens есть в properties исходной schema. Затем тест проверяет наличие cache_write_input_tokens в model_fields Python-класса.
Наконец, fixture с ненулевым значением, например 17, должна пройти десериализацию без потери. Проверки одного лишь default 0 недостаточно: она не обнаружит ситуацию, в которой поле молча отбрасывается.
Когда исправление выйдет, не ограничивайтесь номером версии пакета. Проверьте рантайм-объект, alias и фактический результат разбора raw payload, затем сопоставьте несколько событий до и после обновления. Так можно отделить исправление генерации от изменений в транспортном слое или хранилище.
- Тестируйте ненулевое cacheWriteInputTokens, а не только значение по умолчанию.
- Проверяйте цепочку schema → generated model → десериализация события.
- После обновления фиксируйте версию SDK и результат smoke-теста в журнале изменений телеметрии.
Что этот баг говорит об экономике агентных систем
В агентном программировании стоимость формирует не только финальный ответ модели. На неё влияют размер и стабильность контекста, повторяемость инструкций, число tool calls, прерывания turn, стратегия хранения истории и характер кэширования.
Поэтому командам недостаточно одной строки totalTokens. Нужна детализация, которая позволяет технически объяснить расход, а не только зарегистрировать его постфактум.
В этом смысле issue #38158 не является сенсацией о биллинге. Это показательный сбой наблюдаемости на стыке схемы, code generation и финансовой аналитики. Он особенно чувствителен для команд, которые экспериментируют с длинными репозиторными контекстами и многошаговыми агентами.
В таких системах различие между cache read, cache write и обычным input полезно при оптимизации. Оно позволяет точнее ставить вопросы к данным: изменилась длина контекста, стабилизировался ли префикс, начался ли прогрев кэша или выросло число вызовов инструментов.
Публичная страница OpenAI о GPT-5.6 приводит оценки партнёров о качестве модели в задачах разработки. Эти оценки не относятся к issue #38158 и не подтверждают его последствия. Они лишь показывают, почему измерение стоимости агентных coding-сценариев становится частью инженерной практики, а не только задачей финансовой команды.
- Авторская интерпретация: ценность cacheWriteInputTokens состоит прежде всего в объяснимости cost telemetry.
- Факт: в app-server schema предусмотрены отдельные поля cachedInputTokens и cacheWriteInputTokens.
- Факт: правила cache pricing, опубликованные для GPT-5.6, различают cache writes и cache reads.
Цитата в переводе«GPT-5.6 — одна из самых сильных моделей, которые мы тестировали на CursorBench.»
Цитата в переводе«GPT-5.6 была самой сильной моделью из тех, что мы оценивали в наших тестах агентного code review.»
Что это означает на практике
Пока исправление не подтверждено в используемой версии, Python TokenUsageBreakdown стоит считать неполным источником telemetry для cache-write-сценариев. Это не повод останавливать интеграцию Codex, но достаточный повод проверить, куда именно попадают usage-данные и какие решения принимаются на их основе.
- Перехватывайте уведомления thread/tokenUsage/updated в raw JSON-RPC до десериализации и храните last и total отдельно.
- Не заменяйте отсутствующий cacheWriteInputTokens нулём без статуса происхождения значения.
- Разделите telemetry, оперативную модель стоимости и финансовую сверку по данным провайдера.
- Не восстанавливайте cache writes формулой inputTokens минус cachedInputTokens: схема не задаёт такой семантики.
- Добавьте fixture с ненулевым cacheWriteInputTokens в CI. После будущего обновления проверяйте не только версию SDK, но и реальную десериализацию.
Вывод
Баг #38158 не даёт оснований говорить о неверных счетах OpenAI или сломанных серверных лимитах. Его установленный эффект уже и конкретнее: Python bindings Codex не отражают одно из полей, предусмотренных протоколом app-server v2.
Для разработчиков это повод пересмотреть контур наблюдаемости. Пока исправление не подтверждено в используемой версии, raw-события следует сохранять отдельно, оценку затрат строить с явными допущениями о тарифе, а окончательные расходы сверять по финансовым данным провайдера.
В системах с агентами и длинным повторяемым контекстом эта дисциплина важнее единичного поля. Она не позволяет принять удобный SDK-объект за полную картину использования и помогает избежать архитектурных решений на основе непроверенной арифметики.
Источники
Частые вопросы
Означает ли баг #38158, что OpenAI неверно списывает деньги?
Нет. Доступные материалы подтверждают рассинхронизацию схемы app-server v2 и Python-модели TokenUsageBreakdown. Они не подтверждают ошибку серверного биллинга, двойное списание токенов или неверный расчёт rate limits.
Почему нельзя вычислить cache writes как inputTokens минус cachedInputTokens?
Схема app-server v2 не утверждает, что inputTokens и cachedInputTokens образуют такую арифметическую модель. Поэтому эта разность не позволяет надёжно восстановить cacheWriteInputTokens.
Что нужно сохранять до исправления Python bindings Codex?
Сохраняйте исходные JSON-RPC-уведомления thread/tokenUsage/updated до SDK-десериализации. Для аудита различайте нулевое значение, отсутствие ключа в payload и случай, когда ключ был в raw-событии, но пропал в SDK-представлении.
Относится ли проблема ко всем Python-клиентам OpenAI?
Нет. Issue #38158 относится к Python bindings в репозитории openai/codex и протоколу Codex app-server v2. Распространять вывод на публичную библиотеку openai или Responses API без отдельного воспроизводимого теста нельзя.
Как проверить, что исправление действительно работает?
Проверьте не только версию пакета. Нужны наличие поля в Python-модели, корректный alias и успешная десериализация записанного или реального raw payload с ненулевым cacheWriteInputTokens.
Практический чек-лист
Чек-лист: контроль telemetry и cost-модели Codex
Этот чек-лист можно сверстать отдельным PDF для папки downloads под именем codex-cache-write-telemetry-checklist.pdf. В DOCX он оформляется как самостоятельный блок со штатными заголовками и маркированными списками; ссылку на файл в текст не добавлять.
Сбор данных
- Сохраняйте raw JSON-RPC frame до SDK-десериализации.
- Храните tokenUsage.last и tokenUsage.total раздельно.
- Фиксируйте источник каждого значения usage в хранилище событий.
Статус cacheWriteInputTokens
- Различайте значение 0, отсутствие ключа в wire payload и потерю ключа в SDK.
- Не нормализуйте любое отсутствующее значение в 0.
- Используйте совместимую модель или словарь с alias cacheWriteInputTokens, если требуется временный обходной слой.
Оценка затрат
- Версионируйте тарифы по модели, дате и провайдеру.
- Не выводите cache writes из разности inputTokens и cachedInputTokens.
- Не используйте telemetry app-server как единственное основание для финансового закрытия периода.
- Сверяйте итоговые расходы с официальными usage/cost exports и инвойсом конкретного провайдера.
Регрессия после обновления
- Проверяйте наличие cacheWriteInputTokens в properties исходной schema.
- Проверяйте наличие cache_write_input_tokens в model_fields Python-класса.
- Добавьте в CI fixture с ненулевым значением, например 17.
- После обновления фиксируйте версию SDK и результат smoke-теста в журнале изменений telemetry.
Вопросы и ответы
Означает ли баг #38158, что OpenAI неверно списывает деньги?
Нет. Доступные материалы подтверждают рассинхронизацию схемы app-server v2 и Python-модели TokenUsageBreakdown. Они не подтверждают ошибку серверного биллинга, двойное списание токенов или неверный расчёт rate limits.
Почему нельзя вычислить cache writes как inputTokens минус cachedInputTokens?
Схема app-server v2 не утверждает, что inputTokens и cachedInputTokens образуют такую арифметическую модель. Поэтому эта разность не позволяет надёжно восстановить cacheWriteInputTokens.
Что нужно сохранять до исправления Python bindings Codex?
Сохраняйте исходные JSON-RPC-уведомления thread/tokenUsage/updated до SDK-десериализации. Для аудита различайте нулевое значение, отсутствие ключа в payload и случай, когда ключ был в raw-событии, но пропал в SDK-представлении.
Относится ли проблема ко всем Python-клиентам OpenAI?
Нет. Issue #38158 относится к Python bindings в репозитории openai/codex и протоколу Codex app-server v2. Распространять вывод на публичную библиотеку openai или Responses API без отдельного воспроизводимого теста нельзя.
Как проверить, что исправление действительно работает?
Проверьте не только версию пакета. Нужны наличие поля в Python-модели, корректный alias и успешная десериализация записанного или реального raw payload с ненулевым cacheWriteInputTokens.