Перейти к содержанию

OpenAI

Час ожидания в Codex стоит до 188 миллионов токенов

9 мин чтения

Текст написан ИИ-моделью по источникам, на которые ведут ссылки, и сверен с ними второй моделью. Как мы работаем

Всё о теме: Агенты для программирования →
Изображение: relux.works

Команда Relux в разборе от 11 сентября объяснила, почему недельный лимит подписки Pro за 200 долларов в Codex заканчивается за сутки. Goal-режим превращает ожидание дочерних агентов в непрерывный опрос, и каждая проверка заново перечитывает весь контекст. Авторы прошли по 3808 сессиям с двух машин за июль, август и сентябрь 2026 года, на версиях CLI от 0.144 до 0.154. Каждое утверждение о механике у них подкреплено ссылкой на исходники Codex на GitHub, сами логи сессий не опубликованы.

Ключевая сцена случилась в 23:50: автор спросил модель, зачем она так часто проверяет логи. Модель ответила, что «контракт цели требует, чтобы каждый ход либо продвигал работу, либо доказывал проверенное ожидание живым опросом запусков», и пообещала опрашивать реже. Через 0,3 секунды после этого ответа goal-режим запустил следующий ход. Модель не решает, когда её вызовут снова, поэтому сдержать обещание она не могла.

Сколько стоит час ожидания

Модель не помнит разговор: на каждом шаге ей заново отправляют всю переписку, и она отвечает одним действием. Если переписка занимает 240 тысяч токенов, шаг стоит 240 тысяч входных токенов, даже когда весь шаг сводится к tail -n 18. Весь разбор строится на одной формуле: цена ожидания равна числу проверок, умноженному на размер контекста. Если агент может спать между проверками и не тратить на это шаг, опрос обходится не дороже уведомления. Беда начинается, когда спать ему нечем.

В исходной сессии оркестратор на muse-spark 1.3 через провайдера opencode ждал своих дочерних агентов. За 48 минут goal запустил 173 продолжения и сделал 466 вызовов модели, каждый из которых перечитывал от 120 до 470 тысяч токенов. Всего ушло 149 миллионов входных токенов, или 188 миллионов в час. Остановил это только ответ провайдера с кодом 429.

На официальной gpt-5.6-sol тот же цикл обходился дешевле, но порядок цифр не менялся. В сессии A модель ждала по 30 секунд на вызов, это потолок для запущенной команды. За 7,8 часа ожидания она делала 271 шаг в час и потратила 650 миллионов токенов, 83 миллиона в час. В сессии B модель дочитывала вывод пустым write_stdin и получала до пяти минут паузы на вызов. Там 92 часа ожидания, 43 шага в час и 1,85 миллиарда токенов, около 20 миллионов в час.

Дешевле всех вышла gpt-6-astra, единственная модель, которой выдан инструмент паузы clock.sleep. В двух goal-сессиях она вызвала его 60 и 129 раз и спала по 45 секунд. Час ожидания стоил ей 10-15 миллионов токенов при 55-100 вызовах. По подсчёту авторов, это в 6-18 раз дешевле спина (так в разборе называют опрос без пауз) и всё равно в 20-30 раз дороже уведомления, потому что goal перезапускал модель 600 и 443 раза за сессию.

Иногда модель заканчивает ход, ничего не сделав, например пишет «жду», и goal тут же запускает её снова. В логах есть цепочки из четырёх таких ходов за 20 секунд. В одной сессии 14 пустых ходов стоили 77 миллионов входных токенов.

Все цифры в разборе приведены в полных входных токенах, как их считает клиент. В архиве 97,8% входа пришло из KV-кэша: в нём хранятся уже посчитанные значения для начала промпта, и повторяющийся префикс не пересчитывается. В API OpenAI кэшированный вход стоит 10% обычной цены. По какой формуле токены превращаются в проценты лимита подписки, не опубликовано: клиент получает от сервера только процент использования и время сброса. Авторы допускают, что скидка за кэш там действует, иначе тяжёлая сессия выбирала бы окно за минуты, но сотни миллионов лишних токенов съедают его и со скидкой. Довод о том, что в rate limits API кэшированные токены считаются как обычные, они после обсуждения на Reddit вычеркнули: это правило касается поминутных лимитов запросов, а не тарифа.

Что показал архив

Goal был включён в 52 сессиях из 3808, в полутора процентах. На них пришлось 29,2 из 59,2 миллиарда входных токенов. Все семь самых дорогих сессий архива, от 1,8 до 5,7 миллиарда токенов каждая, шли с goal, а самая тяжёлая сессия без него весит 1,2 миллиарда. Без принудительного продолжения модель заканчивала ход и ждала человека, и это ожидание ничего не стоило.

В каждом событии token_count в логах лежит снимок лимитов аккаунта. За десять недель с июля по сентябрь недельное окно тринадцать раз доходило до 99% и одиннадцать раз упиралось в 100%. Ошибка «You've hit your usage limit» встречается в 28 календарных днях. От начала окна до 99% в медиане проходило 25 часов, в самом быстром случае 7, в самом медленном 93. Пятичасовое окно за тот же период кончалось всего дважды. При 83-188 миллионах токенов за час ожидания в сутки набегает два-четыре миллиарда, отсюда и «за день» в заголовке разбора.

Почему модель не может просто подождать

Как только поток освобождается, обработчик on_thread_idle в расширении goal вызывает continue_if_idle и сразу начинает новый ход. Паузы перед перезапуском нет, счётчика пустых продолжений тоже, а бюджет токенов на цель по умолчанию не задан. В исходной сессии между концом одного хода и началом следующего проходило 0,02-0,05 секунды, в медиане 0,03. Авторы называют любой goal-режим встроенным в продукт циклом while true; do codex "continue"; done.

Капкан достраивает текст контракта в continuation.md. Ожидание засчитывается, только если модель в этом ходу опросила живой процесс, а «жду» без опроса считается отсутствием прогресса. Сама модель может перевести цель только в complete или blocked. Blocked разрешён после трёх одинаковых блокеров подряд, и ожидание дочерних процессов блокером не считается. Поставить цель на паузу может только человек.

Нужный инструмент в Codex при этом есть. clock.sleep появился в версии 0.141.0 18 июня, своё имя получил в 0.143.0 8 июля, умеет спать до 12 часов и просыпается от нового ввода. Регистрируется он, только если запись модели в каталоге объявляет поддержку clock или инструмент принудительно включён в конфиге. Во встроенном каталоге clock есть только у gpt-6-astra, у gpt-5.6-sol, gpt-5.5, gpt-5.4 и у всех моделей на кастомных провайдерах список пуст. codex features list показывает sleep_tool stable true, но это флаг функции, и наличия инструмента в запросе он не доказывает. Из 420 вызовов sleep в архиве 418 сделала gpt-6-astra, на все остальные модели пришлось всего два. Авторы видят в этом поэтапную раскатку: модель, которую не учили выбирать длительность сна, может заснуть вместо работы или крутить sleep 1 в цикле. Goal при этом включён для всех моделей.

Без sleep остаётся shell. По умолчанию Codex возвращает управление модели через 10 секунд, по аргументу yield_time_ms можно попросить до 30, а пустой write_stdin ждёт до пяти минут. У muse-spark закрылся и этот путь: провайдер передавал числа как 60000.0 вместо 60000, и парсер Codex отклонял вызов с ошибкой вида invalid type: floating point 60000.0, expected u64. На этой модели упали 10 из 10 вызовов write_stdin и 23 из 25 вызовов с yield_time_ms. Принудительно включённый sleep её бы тоже не спас, потому что его duration_ms тоже целое число. Медианный ход длился 10 секунд, средний 17: модель читала контекст, делала один опрос и писала «изменений нет». Из 181 хода сессии 135 состояли ровно из одной такой команды.

Сильнее всего это бьёт по оркестраторам. С одним дочерним процессом можно заблокироваться до его завершения, и трёхчасовой процесс обойдётся примерно в 36 вызовов, по 12 шагов в час. Когда процессов несколько, оркестратору нужно реагировать по ходу: принять результат, перезапустить упавший. Долго блокироваться он не рискует и переходит на короткий цикл опроса. В Codex есть барьер wait_agent, который ждёт первого завершившегося из нескольких агентов, но он видит только агентов, запущенных через spawn_agent. Qwen 3.6 через локальный llama.cpp вела себя так же, 21 из 22 ходов её goal-сессии были чистым опросом: доступ к инструменту паузы решает каталог моделей, провайдер тут ни при чём.

В исходной сессии получился и естественный эксперимент. В 00:03 провайдер ответил 429, и цикл замер на час. В 01:34 автор возобновил цель, и опрос пошёл снова, раз в 12 секунд. В 01:35 автор нажал Ctrl-C, а через восемь минут попросил продолжить без goal. Хватило пяти вызовов, работа была закончена. Контекст, модель и задача остались те же, поменялся только диспетчер.

Как ждёт Claude Code

В Claude Code есть такой же режим /goal, но диспетчер у него другой. По документации, после каждого хода условие проверяет отдельная небольшая модель. Если ещё работает фоновая команда или субагент, проверка откладывается, а их результат приходит новым ходом. Когда фоновая работа затягивается, первая проверка случается через 30 минут, дальше интервал удваивается, и без человека таких проверок не больше трёх. Несколько ходов подряд без вызова инструментов останавливают цикл с предупреждением. Механику уведомлений авторы смотрели по ClawCodex, стороннему порту на Python: background.py ждёт процесс и кладёт в очередь конверт <task-notification>, и пока процесс работает, модель не вызывают. По словам авторов, обучение модели здесь роли не играет. В одном продукте ждёт обёртка и будит модель, в другом модель вынуждена спрашивать сама.

Что с этим делать

Самый простой вариант, по мнению авторов, - брать для goal-задач gpt-6-astra на минимальном effort, только у неё sleep есть из коробки. Для остальных моделей sleep включается в ~/.codex/config.toml:

[features]<br>

sleep_tool = { mode = "always_on" }

После перезапуска Codex clock.sleep появляется в наборе у любой модели. Проверить просто: попросить модель поспать пять секунд через clock.sleep и посмотреть, появился ли вызов. Оговорок две. Модель не учили пользоваться инструментом, и она может продолжить опрашивать shell. А у моделей на кастомных провайдерах, которые пишут 60000.0, инструмент не заработает, пока не исправят сериализацию чисел. Сами авторы включили sleep через конфиг, учат оркестратор ждать все дочерние процессы одним вызовом и ставят бюджет на каждую цель, но признают, что в масштабе этот путь ещё не проверен.

От Codex они просят четыре небольших изменения: регистрировать clock.sleep при любой активной цели независимо от каталога, делать паузу между продолжениями после пустого или ждущего хода, принимать 60000.0 там, где ожидается 60000, и прописать в continuation.md sleep и длинный write_stdin как допустимые формы проверенного ожидания. Проверить свои сессии можно скриптом codex-rollout-audit. Это один файл на Python без зависимостей, он читает ~/.codex/sessions, никуда ничего не отправляет и выводит сводку по сессиям, эпизоды исчерпания лимита и число отклонённых дробных аргументов.

Свой разбор авторы закончили той же формулой: число проверок, умноженное на размер контекста. «Пока модели не дадут способ ждать, не тратя шаг, платит тот, кто ждёт».

Пока никто не написал. Будьте первым.

Присоединяйтесь к разговору

Войдите, чтобы оставить комментарий. Он появится после модерации.