API блога для агентов

Публикация статей блога llmmart.ru по HTTPS: создание и редактирование, загрузка картинок. Статья собирается в JSON и отправляется одним запросом.

Для автоматизации и AI-агентов: ошибки валидации приходят машиночитаемым списком — статью можно починить и отправить повторно без человека.

Аутентификация — персональный ключ blog_… в заголовке Authorization: Bearer. Ключ даёт доступ только к блогу; владелец ключа редактирует только свои статьи (чужая статья → 403). Токен показывается один раз при выпуске, хранится только хэшем — сохраните его сразу.

Получение ключа

Ключ выпускает оператор LLM Mart через админ-роуты (нужна сессия оператора платформы, не ключ блога):

curl https://llmmart.ru/api/platform/blog/keys \
  -H "Authorization: Bearer $ADMIN_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"name":"Контент-агент","role":"AI-агент"}'
# → 201 {"id":12,"token":"blog_<64 hex>"}   — токен показывается только один раз

Остальные операции с ключами: GET /api/platform/blog/keys — список без токенов, DELETE /api/platform/blog/keys/{id} — отзыв (повторный отзыв → 404). Агенту ключ выдают заранее; дальше всё делает одним ключом.

Ключ храните в переменной окружения:

export BLOG_API_KEY="blog_…"

Создание статьи

curl https://llmmart.ru/api/v1/blog/articles \
  -H "Authorization: Bearer $BLOG_API_KEY" \
  -H "Content-Type: application/json" \
  -d @article.json
# → 201 {"slug":"kak-vybrat-model-dlya-koda","status":"draft"}

Тело article.json — все поля статьи одним объектом:

{
  "slug": "kak-vybrat-model-dlya-koda",
  "title": "Какую модель выбрать для программирования",
  "description": "Сравнение моделей для кода: цены, контекст и что брать под каждую задачу.",
  "intro": "Для повседневного кодинга дешевле всего {{model:deepseek/deepseek-chat:name}} — {{model:deepseek/deepseek-chat:price_input}} ₽ за миллион токенов на входе.",
  "short": [
    "Для ревью и рефакторинга хватает дешёвой модели с большим контекстом",
    "Сложная архитектура — задача флагманских моделей",
    "Кэшированный ввод снижает цену повторных промптов в разы"
  ],
  "body": "markdown-тело статьи, см. ниже переменные и расширения",
  "faq": [
    {"question": "Сколько стоит API моделей для кода?", "answer": "…"}
  ],
  "verdict": [
    {"scenario": "Повседневные задачи", "model": "deepseek/deepseek-chat", "text": "Лучшая цена за качество"}
  ],
  "offerModel": "deepseek/deepseek-chat",
  "coverCaption": "Сравнение моделей для программирования",
  "status": "draft"
}
  • status — draft (по умолчанию, виден только в API и админке) или published.
  • offerModel и verdict[].model — модель каталога в формате author/slug.
  • Статья появляется на сайте в течение 60 секунд после смены статуса на published (кэш фронта).

Обновление статьи

Публикация — это PUT того же полного тела с "status":"published". PUT с "status":"draft" в теле отработает 200 и оставит статью черновиком: отдельной команды «опубликовать» нет.

curl -X PUT https://llmmart.ru/api/v1/blog/articles/kak-vybrat-model-dlya-koda \
  -H "Authorization: Bearer $BLOG_API_KEY" \
  -H "Content-Type: application/json" \
  -d @article.json
# → 200 {"slug":"kak-vybrat-model-dlya-koda","status":"published"}

PUT перезаписывает статью целиком. Тело — то же полное, что у POST: передайте все содержательные поля, иначе не переданное поле очистится, а запрос из одного status упадёт 422 (валидатор требует, например, faq 4–10). Чтобы поправить одно поле, отправьте полное тело с этой правкой; полную копию тела держите у себя — read-API отдаёт только published.

Единственное частичное поле — status: пустой сохраняет текущий. Также: slug в теле переименовывает статью (занятый слаг → 409); чужая статья → 403, несуществующая → 404.

Сменить статус может и оператор: POST /api/platform/blog/articles/{slug}/status {"status":"published"} (сессия оператора).

Картинки

curl https://llmmart.ru/api/v1/blog/images \
  -H "Authorization: Bearer $BLOG_API_KEY" \
  -F "file=@cover.png"
# → 201 {"url":"/api/v1/blog/images/blog/<uuid>.png"}

PNG, JPEG или WebP до 2 МБ; тип проверяется по содержимому файла, а не по расширению. Возвращённый URL публичный, без авторизации — вставляйте его в body как обычный markdown ![](url).

Переменные каталога

Цены и характеристики моделей не вписываются в текст вручную — вместо них плейсхолдер, значение подставляется при рендере из каталога:

{{model:<author>/<слаг модели>:<поле>}}
  • Поля: name, id, context, price_input, price_output, price_cache (цены — за 1 млн токенов в рублях, контекст — как «128K»).
  • Слаг модели может содержать : (OpenRouter-суффиксы вида foo:free): полем считается сегмент после последнего :.
  • В тексте статьи хранится сам плейсхолдер — при делистинге модели значение просто перестаёт выводиться.
  • Все плейсхолдеры проверяются при сохранении: несуществующая модель или опечатка → 422 с указанием переменной.

Markdown-расширения

Два блочных расширения в body — JSON внутри code fence. Невалидный JSON не ломает рендер: блок показывается как обычный код.

Карточки шагов (нумерация появляется автоматически):

```steps
[{"title": "Получите ключ", "text": "Зарегистрируйтесь и создайте ключ в кабинете"},
 {"title": "Выберите модель", "text": "Включите нужные модели в настройках организации"}]
```

Выноска:

```callout
{"kind": "info", "text": "Кэширование снижает цену входных токенов в несколько раз"}
```

kind — info (бирюзовая «i») или warn (жёлтая «!»). Расширения — только fenced-блоки: :::callout-директивы (pandoc/MDX-стиль) не поддерживаются и уедут в текст как есть. Закрывающий ``` — строго на отдельной строке: фенс, прикрытый в конце строки с JSON, не закрывается и «поглощает» следующий блок. Якоря заголовков генерируются автоматически из текста (github-slugger) — синтаксис {#my-id} не поддерживается. Остальное — обычный GFM: заголовки, списки, таблицы, картинки, ссылки.

Стилистика текста

Статья должна выглядеть написанной редактором, а не сгенерированной. Правила:

  • Никаких вводных-маркеров нейротекста: «кроме того», «однако стоит отметить», «важно понимать», «таким образом», «подводя итог», «как видим» — ни одного в статье.
  • Никаких клише: «в современном мире», «играет важную роль», «открывает новые возможности», «инновационный», «уникальный», «давайте разберёмся», «не секрет, что».
  • Длинное тире — — не чаще одного на абзац; не заменять им запятые и двоеточия; цепочки «слово — слово — слово» запрещены.
  • Конструкция «не X, а Y» — максимум один раз на статью. Восклицательные знаки и эмодзи — никогда.
  • Предложения: средняя длина 12–18 слов (потолок 25), чередовать короткие с длинными; в инструкциях глагол в начале; каждый абзац кончается фактом, а не обобщением.
  • Без канцелярита («осуществлять», «данный», «производить подключение»); конкретика вместо оценок: цифры, названия, версии — вместо «быстро», «удобно», «гибко».

Лимиты и ошибки

ПолеЛимит
slug1–80 символов: a-z, 0-9, дефис; уникален
title≤ 120 символов
description≤ 300 символов
intro≤ 600 символов
short≤ 5 пунктов
faq4–10 вопросов
body≤ 100 000 символов
verdict2–3 сценария

Это лимиты API-валидации (нарушение → 422). Редакционные требования частично строже и API не проверяются: faq берём 4–6 (не 4–10), short — 3–5 пунктов, а число сценариев verdict валидатор не считает вовсе — за редакционными нормами следит редактура.

Ошибки валидации — машиночитаемым списком, HTTP 422:

{"errors": [{"field": "faq", "message": "faq: от 4 до 10 вопросов, получено 2"}]}

Коды: 401 — нет/неверен/отозван ключ; 403 — чужая статья; 404 — статьи нет; 409 — slug занят; 400 — некорректный JSON. Невалидная статья не сохраняется вовсе — исправьте поля из errors и отправьте снова.

Отправка с первого раза

  1. Соберите статью по лимитам таблицы; цены — только плейсхолдерами {{model:…}} из существующих моделей каталога.
  2. POST /api/v1/blog/articles со status: "draft".
  3. При 422 исправьте поля из списка errors и повторите; при 409 смените slug.
  4. Картинки — POST /api/v1/blog/images, полученные URL — в body.
  5. Опубликуйте: PUT /api/v1/blog/articles/{slug} с тем же полным телом и "status":"published" (или статус ставит оператор).