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

> AI Release · @ai_release1 · https://ai-release.org/guides/kak-proverit-storonniy-api-router-na-podmenu-modeli-sra.html

## TL;DR
- Время выполнения: 30–40 минут (плюс время на установку Python-зависимостей)
- Уровень сложности: Intermediate
- Понадобится: `curl`, `jq`, Python 3 с пакетом `requests`, два API-ключа (официальный и от роутера), доступ к документации обоих сервисов, текстовый редактор

## Требования и подготовка
![Иллюстрация к разделу «Требования и подготовка»](https://ai-release.org/guides/img/kak-proverit-storonniy-api-router-na-podmenu-modeli-sra-ru-1.jpg?v=1792832747)


- ОС: Linux, macOS или Windows с WSL — команды ниже даны для bash.
- `curl` и `jq` в PATH. Проверка: `curl --version` и `jq --version` должны вывести версии без ошибок.
- Python 3 и менеджер пакетов: `python3 --version`, установка зависимости — `pip install requests`.
- Официальный API-ключ провайдера модели (эталон) и API-ключ проверяемого роутера.
- Точный идентификатор модели из документации провайдера — например `<id-модели-из-документации>`. Не угадывайте его: скопируйте строку из раздела Models в документации.
- Базовые адреса API (`base_url`) обоих сервисов. Обычно это `https://<домен>/v1` — уточните в документации, включает ли `base_url` версию `/v1`.
- Список моделей и их официальных провайдеров, если вы ещё выбираете, что именно сверять: [Лучшие нейросети и ИИ-модели 2026: что умеют и где попробовать](https://ai-release.org/guides/luchshie-nejroseti-i-modeli-2026.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

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

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

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

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

```bash
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` — по одной строке на промпт. Хороший набор проверяет разные навыки: арифметику, перечисление, код, работу с текстом.

```bash
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 и роутера»](https://ai-release.org/guides/img/kak-proverit-storonniy-api-router-na-podmenu-modeli-sra-ru-2.jpg?v=1792832755)


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

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

```bash
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`:

```python
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. Сравните ответы и вынесите вердикт»](https://ai-release.org/guides/img/kak-proverit-storonniy-api-router-na-podmenu-modeli-sra-ru-3.jpg?v=1792832763)


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

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

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

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

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

Заголовки:

```bash
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` с точностью до единицы. Если совпало меньше половины ответов, а арифметика разошлась — переходите к разделу проблем.

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

- [ ] Параметры запросов идентичны (`temperature: 0`, один `max_tokens`, один `MODEL_ID`)
- [ ] Сохранены сырые JSON обоих ответов, а не скриншоты
- [ ] Сверены `model`, `usage`, `finish_reason`, текст
- [ ] Сверены заголовки ответа
- [ ] Прогон повторён дважды, чтобы исключить случайные сбои сети

Если вы проверяете не проприетарную модель, а открытые веса, набор признаков меняется: [Какие ИИ-модели имеют открытые веса и как их запустить бесплатно: список 2026](https://ai-release.org/guides/kakie-ii-modeli-imeyut-otkrytye-vesa-i-kak-ih-zapustit.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide) — там перечислены модели, у которых имя в ответе часто не содержит версии.

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

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

- Ошибка: `{"error":{"message":"Incorrect API key provided","type":"invalid_request_error","code":"invalid_api_key"}}`
- Причина: переменная не попала в окружение (например, вы открыли новый терминал) или в `OFFICIAL_API_KEY` лежит ключ роутера — при копировании строки легко перепутать.
- Решение: проверьте значения без вывода самого ключа: `echo "${OFFICIAL_API_KEY:0:6}... ${ROUTER_API_KEY:0:6}..."` — префиксы должны отличаться. Затем переэкспортируйте переменные и повторите `curl`.

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

- Ошибка: `{"error":{"message":"Unsupported parameter: 'seed'","type":"invalid_request_error"}}`
- Причина: роутер проксирует запрос в API, где этого параметра нет, либо реализует только базовый набор полей. Это само по себе диагностический признак — официальный API параметр принимал.
- Решение: уберите параметр из запроса и зафиксируйте факт в отчёте. Проверьте, что ответ всё ещё приходит: `jq '.model' router.json`. Если после удаления параметра ответы стали совпадать — расхождение было вызвано не моделью, а обработкой запроса.

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

- Ошибка: не текст ошибки, а расхождение вида `official: "391"` / `router: "17 * 23 = 391"` в `router_audit.json`.
- Причина: подмена не доказана. Причины бывают разные — другая версия или снапшот модели, квантование (сжатие весов), другой системный промпт по умолчанию, кэш ответов на стороне роутера. Поле `model` при этом часто остаётся тем же алиасом.
- Решение: проверьте служебные поля ответа, если провайдер их возвращает (например, поле с версией/снапшотом — сверьтесь с документацией), повторите прогон дважды и сравните `usage.prompt_tokens`. Если счётчик токенов стабильно расходится — перед вами другая модель или другой токенизатор; если совпадает, а текст плавает — вероятнее нестабильность или кэш.

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

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

## FAQ

**Обязательно ли `temperature: 0` даёт побайтово одинаковый ответ?**
Нет. Нулевая температура снижает случайность, но не гарантирует идентичность: влияют версия модели, квантование и загрузка серверов. Поэтому и нужен набор из нескольких промптов, а не один запрос.

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

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

**Сколько промптов достаточно для вывода?**
Начните с 5–10 разнотипных: арифметика, перечисление, генерация кода, подсчёт символов. Задачи с однозначным ответом дают самый читаемый результат, а разницу между ассистентами удобно смотреть в [сравнении ИИ-ассистентов](https://ai-release.org/guides/sravnenie-ii-assistentov.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).
