← Все гайды AI Release
EN Подписаться на @ai_release1

Как проверить сторонний API-роутер на подмену модели: сравнение ответов с официальным API

Обновлено: 24.10.2026 · AI Release · @ai_release1
Как проверить сторонний API-роутер на подмену модели: сравнение ответов с официальным API

TL;DR

Требования и подготовка

Иллюстрация к разделу «Требования и подготовка»
Иллюстрация к разделу «Требования и подготовка»

API-роутер — это посредник: вы отправляете запрос на его адрес, а он перенаправляет его одному из провайдеров моделей и возвращает ответ. Подмена модели — ситуация, когда роутер отвечает не той моделью, которая заявлена в его тарифах или документации.

Шаг 1. Зафиксируйте окружение и параметры запроса

Что сделать: создайте рабочую папку, экспортируйте переменные окружения с адресами и ключами, подготовьте набор из 5–10 контрольных промптов.

Зачем: сравнение имеет смысл только при одинаковых входных данных. Если в одном запросе стоит temperature: 0.7, а в другом temperature: 0, разница в ответах ничего не докажет.

mkdir -p ~/router-audit && cd ~/router-audit

export OFFICIAL_BASE_URL="https://<официальный-домен>/v1"
export ROUTER_BASE_URL="https://<домен-роутера>/v1"
export OFFICIAL_API_KEY="<ваш-официальный-ключ>"
export ROUTER_API_KEY="<ваш-ключ-роутера>"
export MODEL_ID="<id-модели-из-документации>"

<официальный-домен> — адрес API самого провайдера модели, <домен-роутера> — адрес проверяемого сервиса. Значения берите из документации, а не из примеров в интернете.

Подготовьте файл prompts.txt — по одной строке на промпт. Хороший набор проверяет разные навыки: арифметику, перечисление, код, работу с текстом.

cat > prompts.txt <<'EOF'
Сколько будет 17 * 23? Ответь одним числом.
Перечисли первые 10 простых чисел через запятую.
Напиши функцию на Python, которая переворачивает строку.
Назови три столицы Европы через запятую.
Сколько букв "о" в слове "молокопровод"? Ответь числом.
EOF

Как проверить: выполните echo "$MODEL_ID" и wc -l prompts.txt. На экране должен появиться идентификатор модели и число 5 — столько промптов вы заготовили. Если MODEL_ID пустой, значит переменная не экспортирована или содержит опечатку.

Почему это работает: temperature — параметр случайности (0 = максимально предсказуемый ответ), а одинаковые промпты и параметры убирают главный источник расхождений — саму генерацию.

Шаг 2. Снимите ответы официального API и роутера

Иллюстрация к разделу «Шаг 2. Снимите ответы официального API и роутера»
Иллюстрация к разделу «Шаг 2. Снимите ответы официального API и роутера»

Что сделать: отправьте один и тот же запрос сначала на официальный адрес, потом на адрес роутера, сохраняя тело ответа и заголовки в отдельные файлы.

Зачем: вам нужны сырые JSON-ответы, а не пересказ в чате. В теле ответа есть поля model, usage и finish_reason, по которым видно, кто и как отвечал.

PROMPT='Сколько будет 17 * 23? Ответь одним числом.'

curl -s -D headers_official.txt "$OFFICIAL_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $OFFICIAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"model\":\"$MODEL_ID\",\"messages\":[{\"role\":\"user\",\"content\":\"$PROMPT\"}],\"temperature\":0,\"max_tokens\":50}" \
  -o official.json

curl -s -D headers_router.txt "$ROUTER_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"model\":\"$MODEL_ID\",\"messages\":[{\"role\":\"user\",\"content\":\"$PROMPT\"}],\"temperature\":0,\"max_tokens\":50}" \
  -o router.json

Флаг -D сохраняет заголовки ответа, -o — тело. Заголовки нужны, потому что официальный шлюз и сторонний сервис обычно подписываются по-разному (Server, Via, X-*).

Автоматизируйте прогон всех промптов скриптом audit.py:

import json, os, requests

def ask(base_url, api_key, prompt):
    r = requests.post(
        f"{base_url}/chat/completions",
        headers={"Authorization": f"Bearer {api_key}",
                 "Content-Type": "application/json"},
        json={"model": os.environ["MODEL_ID"],
              "messages": [{"role": "user", "content": prompt}],
              "temperature": 0, "max_tokens": 300},
        timeout=60,
    )
    r.raise_for_status()
    return r.json()

report = []
for prompt in open("prompts.txt", encoding="utf-8").read().splitlines():
    if not prompt.strip():
        continue
    a = ask(os.environ["OFFICIAL_BASE_URL"], os.environ["OFFICIAL_API_KEY"], prompt)
    b = ask(os.environ["ROUTER_BASE_URL"], os.environ["ROUTER_API_KEY"], prompt)
    ca = a["choices"][0]["message"]["content"].strip()
    cb = b["choices"][0]["message"]["content"].strip()
    report.append({"prompt": prompt, "equal": ca == cb,
                   "official_model": a.get("model"), "router_model": b.get("model"),
                   "official_usage": a.get("usage"), "router_usage": b.get("usage"),
                   "official_finish": a["choices"][0].get("finish_reason"),
                   "router_finish": b["choices"][0].get("finish_reason"),
                   "official": ca, "router": cb})

json.dump(report, open("router_audit.json", "w", encoding="utf-8"),
          ensure_ascii=False, indent=2)
print(f"Совпало дословно: {sum(x['equal'] for x in report)} из {len(report)}")

Запуск: python3 audit.py.

Как проверить: jq -S '{model, usage, content: .choices[0].message.content}' official.json должен вывести компактный JSON с именем модели и числом токенов. Если вместо этого вы видите null или пустой файл — запрос не прошёл, смотрите содержимое headers_official.txt и тело ответа.

Почему это работает: usage.prompt_tokens считает токенизатор конкретной модели. Токен — кусочек текста, на который модель режет вход; у разных моделей нарезка разная, поэтому расхождение счётчиков на одном и том же промпте — сильный сигнал.

Шаг 3. Сравните ответы и вынесите вердикт

Иллюстрация к разделу «Шаг 3. Сравните ответы и вынесите вердикт»
Иллюстрация к разделу «Шаг 3. Сравните ответы и вынесите вердикт»

Что сделать: сведите результаты в таблицу и пройдитесь по каждому признаку отдельно: текст, model, usage, finish_reason, заголовки.

Зачем: одиночное расхождение почти ничего не значит, а совпадение сразу нескольких признаков — значит много.

Что сравниваемПоле в ответеПризнак подмены
Имя модели`model`роутер возвращает другое имя или алиас без версии
Число входных токенов`usage.prompt_tokens`на одном промпте заметно другой счётчик — вероятно, другой токенизатор
Причина остановки`choices[0].finish_reason``length` там, где официальный API вернул `stop`
Текст ответа`choices[0].message.content`расходится на детерминированных задачах (арифметика, перечисление)
Заголовки`Server`, `Via`, `X-*`официальный отдаёт свой шлюз, роутер — сторонний

Быстрая сверка одного ответа:

jq -r '.choices[0].message.content' official.json
jq -r '.choices[0].message.content' router.json
jq '.model, .usage' official.json router.json

Заголовки:

grep -iE '^(server|via|x-)' headers_official.txt
grep -iE '^(server|via|x-)' headers_router.txt

Как проверить: откройте router_audit.json и посмотрите на поле equal. Успех выглядит так: большинство промптов с equal: true, official_model и router_model совпадают, official_usage.prompt_tokens равен router_usage.prompt_tokens с точностью до единицы. Если совпало меньше половины ответов, а арифметика разошлась — переходите к разделу проблем.

Финальный чек-лист перед выводом:

Если вы проверяете не проприетарную модель, а открытые веса, набор признаков меняется: Какие ИИ-модели имеют открытые веса и как их запустить бесплатно: список 2026 — там перечислены модели, у которых имя в ответе часто не содержит версии.

Возможные проблемы и решения

Проблема. Оба запроса возвращают 401, хотя ключи скопированы.

Проблема. Роутер отклоняет запрос с параметром temperature или seed.

Проблема. Ответы расходятся при temperature: 0, но model совпадает.

Резюме и следующие шаги

Вы получили воспроизводимый протокол: одинаковые промпты, два адреса, сырые JSON и таблица признаков. Дальше расширьте набор промптов до 20–30 штук и прогоняйте audit.py после каждой смены тарифа или модели у роутера.

FAQ

Обязательно ли temperature: 0 даёт побайтово одинаковый ответ?

Нет. Нулевая температура снижает случайность, но не гарантирует идентичность: влияют версия модели, квантование и загрузка серверов. Поэтому и нужен набор из нескольких промптов, а не один запрос.

Роутер вернул другое значение в поле model — это подмена?

Не обязательно. Роутер может возвращать внутренний алиас вместо публичного имени модели. Смотрите на совокупность: usage.prompt_tokens, finish_reason и поведение на детерминированных задачах.

Можно ли проверить роутер без официального ключа?

Нет. Эталон нужен: без ответа официального API сравнивать не с чем. Если ключа нет, единственный доступный признак — заголовки ответа и поведение на арифметике, но это косвенная проверка.

Сколько промптов достаточно для вывода?

Начните с 5–10 разнотипных: арифметика, перечисление, генерация кода, подсчёт символов. Задачи с однозначным ответом дают самый читаемый результат, а разницу между ассистентами удобно смотреть в сравнении ИИ-ассистентов.

🤖 Кратко для ИИ