curl, jq, Python 3 с пакетом requests, два API-ключа (официальный и от роутера), доступ к документации обоих сервисов, текстовый редактор
curl и jq в PATH. Проверка: curl --version и jq --version должны вывести версии без ошибок.python3 --version, установка зависимости — pip install requests.<id-модели-из-документации>. Не угадывайте его: скопируйте строку из раздела Models в документации.base_url) обоих сервисов. Обычно это https://<домен>/v1 — уточните в документации, включает ли base_url версию /v1.API-роутер — это посредник: вы отправляете запрос на его адрес, а он перенаправляет его одному из провайдеров моделей и возвращает ответ. Подмена модели — ситуация, когда роутер отвечает не той моделью, которая заявлена в его тарифах или документации.
Что сделать: создайте рабочую папку, экспортируйте переменные окружения с адресами и ключами, подготовьте набор из 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 = максимально предсказуемый ответ), а одинаковые промпты и параметры убирают главный источник расхождений — саму генерацию.

Что сделать: отправьте один и тот же запрос сначала на официальный адрес, потом на адрес роутера, сохраняя тело ответа и заголовки в отдельные файлы.
Зачем: вам нужны сырые 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 считает токенизатор конкретной модели. Токен — кусочек текста, на который модель режет вход; у разных моделей нарезка разная, поэтому расхождение счётчиков на одном и том же промпте — сильный сигнал.

Что сделать: сведите результаты в таблицу и пройдитесь по каждому признаку отдельно: текст, 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 с точностью до единицы. Если совпало меньше половины ответов, а арифметика разошлась — переходите к разделу проблем.
Финальный чек-лист перед выводом:
temperature: 0, один max_tokens, один MODEL_ID)model, usage, finish_reason, текстЕсли вы проверяете не проприетарную модель, а открытые веса, набор признаков меняется: Какие ИИ-модели имеют открытые веса и как их запустить бесплатно: список 2026 — там перечислены модели, у которых имя в ответе часто не содержит версии.
Проблема. Оба запроса возвращают 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"}}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 после каждой смены тарифа или модели у роутера.
Обязательно ли temperature: 0 даёт побайтово одинаковый ответ?
Нет. Нулевая температура снижает случайность, но не гарантирует идентичность: влияют версия модели, квантование и загрузка серверов. Поэтому и нужен набор из нескольких промптов, а не один запрос.
Роутер вернул другое значение в поле model — это подмена?
Не обязательно. Роутер может возвращать внутренний алиас вместо публичного имени модели. Смотрите на совокупность: usage.prompt_tokens, finish_reason и поведение на детерминированных задачах.
Можно ли проверить роутер без официального ключа?
Нет. Эталон нужен: без ответа официального API сравнивать не с чем. Если ключа нет, единственный доступный признак — заголовки ответа и поведение на арифметике, но это косвенная проверка.
Сколько промптов достаточно для вывода?
Начните с 5–10 разнотипных: арифметика, перечисление, генерация кода, подсчёт символов. Задачи с однозначным ответом дают самый читаемый результат, а разницу между ассистентами удобно смотреть в сравнении ИИ-ассистентов.