Три правила, по которым API отвечает «не сейчас» и «не столько»: частота запросов, размер страницы и срок жизни ответа.
Ограничение частоты запросов
Запрос проверяется по двум окнам сразу. "default" — окно отдельного ключа: у каждого ключа оно своё, и два ключа одного аккаунта не делят счётчик. "account" — потолок всего аккаунта, общий для всех его ключей и кратно больший, чем окно одного ключа. Оба окна одной длины; отказать может любое из них. Вызов по сессионной куке считается иначе: его окно и так принадлежит аккаунту, поэтому второго счётчика у него нет — потолок к нему не применяется и в заголовках не появляется.
Эти два заголовка несёт ответ, дошедший до ограничителя, — то есть всякий, кроме отказов, которые принимаются выше него: api_disabled и origin_mismatch приходят без RateLimit вовсе, и их отсутствие ничего не говорит о ваших окнах. Формат — структурированный вид черновика draft-ietf-httpapi-ratelimit-headers и никакой другой: семейства X-RateLimit-* здесь нет. q — квота окна, w — длина окна в секундах, r — сколько запросов в окне ещё осталось, t — сколько секунд до его сброса. Поле pk черновика не отправляется.
Пара заголовков всегда описывает ровно одно окно — то, которое связывает этот запрос, и его имя стоит первым в кавычках. Пока хватает обоих, это окно с меньшим остатком; отказавшее окно всегда описывает себя. Поэтому читать нужно имя: если в ответе "account", распределение нагрузки по новым ключам ничего не даст — r у них тоже не вырастет.
Примеры заголовков ответа — числа в них взяты для образца:
# окно ключа
RateLimit-Policy: "default";q=120;w=60
RateLimit: "default";r=42;t=37
# потолок аккаунта
RateLimit-Policy: "account";q=480;w=60
RateLimit: "account";r=0;t=37Действующие квоты опубликованы в GET /v1/me, поле limits. Заголовок RateLimit-Policy сообщает квоту HTTP-окна, которое ограничивает текущий запрос.
Превышение окна — 429 с кодом rate_limited. К RateLimit добавляется Retry-After в секундах задержки (RFC 9110); повторять раньше нечего. Отказ по потолку аккаунта не расходует окно ключа: запрос, которому отказали, ничего из личной квоты ключа не забирает. Тот же код rate_limited отдают и ограничения отправки и параллельности. В details причина request_rate означает частоту HTTP-запросов, submit_rate — отправку генераций, concurrency — занятые места, queue_full — заполненную очередь. Поля scope, limit и current уточняют ограничение. Минутные окна сообщают retry_after_seconds и Retry-After; при занятых местах дождитесь завершения задачи. Другие ограничения маршрутов могут возвращать только общий код.
Отправка генераций и параллельность
Окно отправки через API-ключи и OAuth общее для аккаунта и отдельно от окна студии. Повтор уже принятого запроса с тем же Idempotency-Key не расходует квоту отправки. Отказ из-за занятых мест тоже её не расходует; отклонённый запрос, который уже потребовал обработки референсов, расходует.
Читайте квоты из limits.submissions, а параллельность из limits.concurrency ответа GET /v1/me. Лимит изображений общий для API и студии и считается отдельно для каждого провайдера генерации. Промо-режимы могут иметь собственные ограничения. Поддерживайте разрешённое число незавершённых задач, добавляя новую после завершения предыдущей. Проверяйте статусы пакетом или получайте события: каждый HTTP-запрос проверки тоже расходует квоту запросов.
Постраничный вывод
Коллекция принимает ?cursor= и ?limit= и отвечает конвертом { data, next_cursor }. Отдельного признака «есть ли ещё» нет: next_cursor: null уже это говорит.
limit — целое от 1 до 100, по умолчанию 20. Значение вне этих границ — 400 с кодом invalid_limit, а не молчаливое урезание.
cursor непрозрачен. Его нужно сохранить и передать обратно байт в байт; разбирать, собирать или править его нельзя — внутренняя форма токена не является частью контракта и может измениться. Испорченный токен — 400 с кодом invalid_cursor. Коллекция идёт от новых записей к старым, и порядок устойчив: страницы не пересекаются и не теряют записи на границе.
# первая страница
curl -s "https://api.coremind.ru/v1/generations?limit=20" \
-H "Authorization: Bearer cmk_..."
# {"data": [...], "next_cursor": "eyJjIjoiMjAyNi0..."}
# следующая — тот же токен без изменений
curl -s "https://api.coremind.ru/v1/generations?limit=20&cursor=eyJjIjoiMjAyNi0..." \
-H "Authorization: Bearer cmk_..."
# {"data": [...], "next_cursor": null}Пакетное чтение статусов ограничено отдельно: POST /v1/generations/status принимает до 50 идентификаторов за вызов. Это способ добрать пропущенное после оборванного потока событий: один вызов вместо 50 обращений к GET /v1/generations/{id} расходует один запрос из окна вместо стольких же, поэтому добирать выгоднее пакетом, спрашивая только о тех генерациях, что не дошли до конечного статуса. Больше — 400 с кодом invalid_limit и details[0] = { field: "ids", reason: "out_of_range" }, список не обрезается. Ответ приходит в том же конверте, и next_cursor там всегда null — запрос уже назвал всю свою страницу.
Кеширование
Валидатор есть у одного маршрута — GET /v1/models. Окна свежести у него нет вовсе: Cache-Control: private, max-age=0, must-revalidate — ответ нельзя переиспользовать не спросив. Спрашивать при этом дёшево: слабый ETag, возвращённый в If-None-Match, превращает повтор в 304 вместо целого каталога. Значение ETag непрозрачно — разбирать его не нужно.
curl -s -i "https://api.coremind.ru/v1/models" \
-H "Authorization: Bearer cmk_..."
# ETag: W/"8sT1qO2fVb0aZ9kQ"
# Cache-Control: private, max-age=0, must-revalidate
curl -s -i "https://api.coremind.ru/v1/models" \
-H "Authorization: Bearer cmk_..." \
-H 'If-None-Match: W/"8sT1qO2fVb0aZ9kQ"'
# HTTP/1.1 304 Not ModifiedВсе остальные ответы /v1 — private, no-store с Vary: Authorization, Cookie, Origin. Ни один ответ этого API не помечается public: каждый рассказывает об одном аккаунте, поэтому хранить их можно только у себя, а не в общем кеше.
Статусы и коды отказов, о которых говорит эта страница — rate_limited, invalid_limit, invalid_cursor — на странице кодов ошибок.
Доступность
Текущее состояние API и доступность за 30 дней — на status.coremind.ru, там же публикуются сообщения об инцидентах. Если запросы начали падать с 5xx, сначала загляните туда.