Ошибка 422 в AS: как мы это победили

Ошибка 422 в AS: как мы это победили

Ваш ассистент AS внезапно начал падать при обращении к DeepSeek. В логах — вот это:

422: Failed to deserialize the JSON body into the target type:
messages[0].role: unknown variant `developer`, expected one of
`system`, `user`, `assistant`, `tool`, `latest_reminder`

 

Мы разобрались, почему это происходит, и сделали своё решение. Ниже — что именно случилось и как это чинится за пять минут.


Почему это сломалось

OpenAI в конце 2024 года ввела новую роль — developer. Она задумывалась как замена system: туда разработчик кладёт инструкции, которые модель должна соблюдать всегда.

AS, как и многие современные клиенты, следует свежей спецификации OpenAI и отправляет системный промпт с role: "developer". Всё было бы хорошо, если бы целевой API был официальным эндпоинтом OpenAI.

Но DeepSeek принимает только эти роли:

system | user | assistant | tool | latest_reminder

Его Rust-бэкенд видит незнакомый вариант developer — и отказывается десериализовать запрос. Отсюда 422 и список допустимых значений в тексте ошибки.

Суть проблемы: AS говорит на новом диалекте OpenAI, DeepSeek его ещё не выучил. Нужен переводчик.

Что мы сделали

Вариант «покопаться в настройках AS и попросить его отправлять system» мы отмели сразу — во многих клиентах роль developer зашита во внутреннюю логику, и пользовательскими настройками её не переопределить.

Вместо этого мы поставили между AS и DeepSeek лёгкий Flask-прокси. Он перехватывает запрос, меняет developer на system и передаёт дальше. Клиент ничего не подозревает, DeepSeek получает валидный запрос, все довольны.

Схема простая:

AS  →  http://127.0.0.1:1234  →  https://api.deepseek.com
        (наш прокси)              (реальный API)

Ключевой кусок кода

Вот та самая функция, которая всё чинит:

def normalize_developer_role(data):
    """Заменяет роль 'developer' на 'system' для совместимости с DeepSeek."""
    if "messages" in data and isinstance(data["messages"], list):
        for msg in data["messages"]:
            if isinstance(msg, dict) and msg.get("role") == "developer":
                msg["role"] = "system"

    if "input" in data and isinstance(data["input"], list):
        for item in data["input"]:
            if isinstance(item, dict) and item.get("role") == "developer":
                item["role"] = "system"

Она обрабатывает два места:

  1. Массив messages в /v1/chat/completions — классический Chat Completions API.
  2. Массив input в /v1/responses — новый Responses API, где системные инструкции тоже лежат в developer.

Прокси обслуживает все три эндпоинта (/v1/chat/completions, /v1/responses, /responses) плюс /v1/models для списка моделей. Так что AS может стучаться куда угодно — всё уйдёт по назначению.


Как запустить

Шаг 1. Скачать и распаковать

Скачать proxy-as-deepseek.zip

unzip proxy-as-deepseek.zip
cd proxy-as-deepseek
pip install flask requests
python proxy.py

По умолчанию прокси слушает 127.0.0.1:1234. Если порт занят — поправьте в последней строке файла.

Шаг 2. Переключить AS на прокси

В настройках AS замените Base URL:

  • Было: https://api.deepseek.com
  • Стало: http://127.0.0.1:1234

API-ключ оставьте свой, от DeepSeek. Прокси прозрачно передаст заголовок Authorization дальше.

Шаг 3. Проверить

Запустите прокси, отправьте запрос из AS — и смотрите в консоль прокси. Туда выводится всё: модель, количество сообщений, распределение ролей, а после ответа — расход токенов и попадание в кэш. Если видите role: system вместо developer — всё работает.


Что ещё умеет прокси

Считает токены. Если DeepSeek вернул поле usage (включая последний чанк потокового ответа), прокси напишет в лог: сколько ушло на prompt, сколько на completion, сколько попало в кэш. Если prompt больше 1000 токенов, а кэш пустой — предупредит. Полезно, когда хочется понять, почему запросы дорогие.

Не ломает стриминг. Для запросов с stream: true используется stream_with_context — чанки идут в реальном времени, как и раньше. Разбор usage происходит только на том чанке, где оно есть, и не мешает основному потоку.

Не мешает заголовкам. Hop-by-hop заголовки (Host, Content-Length, Connection и прочие) вычищаются, чтобы не путать DeepSeek. Всё остальное — включая ваш ключ — уходит как есть.


Почему не «просто поправить настройки»

Мы пробовали. В некоторых клиентах есть переключатель supportsDeveloperRole: false, который якобы должен заставить их отправлять system. На практике он либо отсутствует, либо игнорируется — базовая ветка кода всё равно отправляет developer. Это не наша выдумка: с тем же самым сталкивались разработчики Cherry Studio и OmniRoute, и оба проекта в итоге поставили ровно такой же прокси-слой с конвертацией ролей.

Прокси-слой обходит всю внутреннюю логику клиента. Это самый надёжный вариант — и, что важно, не требует ждать, пока AS или DeepSeek что-то там починят.


Файл: proxy-as-deepseek.zip · Python 3.8+ · Flask + Requests

Комментарии 0

Чтобы оставить комментарий, войдите на сайт — комментируют только зарегистрированные пользователи.

Комментариев пока нет — будьте первым.