← Блог
Claude APIИнтеграцияЦены

Настоящий ли Claude отдаёт ваш API-прокси? Как это проверить

(цены в тексте следуют актуальному прайс-листу)

Коротко

Проверить, что прокси для Claude API действительно отправляет запросы в Claude, можно по подписи блока thinking: отправьте подписанный блок thinking обратно без изменений (настоящий бэкенд Anthropic его примет), затем с изменённой подписью (настоящий бэкенд откажет с ошибкой Invalid signature in thinking block). Вопросы «какая ты модель», дата отсечения знаний и поле model ничего не доказывают. Даже тест подписи показывает только, что запросы доходят до Claude от Anthropic, но не какая именно модель ответила, поэтому запускайте его регулярно и на тех моделях, за которые платите.

Это руководство публикует APIVAI — шлюз к моделям Claude и GPT. Тесты работают с любым прокси, поддерживающим нативный формат Anthropic, в том числе с нашим; наши результаты — ближе к концу.

Почему подмены моделей опасаются

Покупая Claude через реселлера, прокси или шлюз, вы верите, что запрос к claude-sonnet-4-6 дойдёт до Claude Sonnet 4.6. Несколько источников показывают, что верить на слово не стоит:

  • Исследования. Работа CISPA «Real Money, Fake Models» (arXiv 2603.01919, принята на ACM CCS 2026) проверила «теневые API», которыми пользовались авторы 187 научных статей. У трёх подробно изученных сервисов 45,83% проверок по отпечаткам модели не совпали с заявленной моделью.
  • Форумы. На китайских форумах для этого есть свои слова: 以次充好 («выдавать дешёвое за дорогое»), 掺水 («разбавлять»), 降智 («модель отупела»). Типичный диалог на V2EX: «Как они гарантируют, что это не другая модель?» — «Никак, и ещё смешивают».
  • Маркетплейсы. На Hacker News пользователь пишет, что запросы уходили на сильно квантованные версии, «а платил я процентов на 15 меньше».

Подмена — не единственная причина ощущения «сегодня хуже». Anthropic в 2025 году публиковала разбор инфраструктурных багов, из-за которых часть ответов Sonnet ухудшилась, а другие системные промпты и обработка контекста тоже меняют ответы. Поэтому проверка должна быть механической, а не на глаз.

Проверки, которые ничего не доказывают

ПроверкаПочему не работает
Спросить «какая ты модель?»Прокси может подставить системный промпт, а настоящие модели и сами ошибаются (в обзоре 190 моделей около 60% назвали себя не своим именем)
Спросить дату отсечения знанийМы спросили Claude Haiku 4.5 десять раз 19.10.2026 и каждый раз получили «April 2024»: ответ стабильный, но прокси может заставить любую модель отвечать так же через системный промпт
Посмотреть поле modelЕго заполняет сам прокси
Оценить качество ответовСлишком шумно: качество меняют и реальные деградации, и промпты, и контекст

Как работает тест подписи thinking

Когда включено расширенное мышление, Anthropic возвращает каждый блок thinking вместе с непрозрачной подписью signature. В диалоге с вызовами инструментов предыдущий ход ассистента вместе с блоками thinking нужно отправить обратно, и Anthropic проверяет подпись (документация по extended thinking). Поддельная или изменённая подпись отклоняется:

400 invalid_request_error: messages.1.content.0: Invalid `signature` in `thinking` block

Тест использует эту проверку три раза:

  1. Задать вопрос, на который модель вызовет инструмент, с включённым thinking. В ответе будут подписанный блок thinking и блок tool_use.
  2. Отправить этот ход обратно без изменений вместе с результатом инструмента. Настоящий бэкенд ответит 200.
  3. Отправить ещё раз, заменив последние символы подписи. Настоящий бэкенд ответит 400 с ошибкой выше.

Доказательство — в третьем запросе. Прокси с другой моделью может сгенерировать строку, похожую на подпись, но не может заставить проверку Anthropic принять настоящую подпись и отвергнуть изменённую, если запросы не доходят до Anthropic.

В тех же ответах есть и более слабые признаки, которые стоит записывать: ID сообщений начинаются с msg_, ID вызовов инструментов — с toolu_, в usage есть service_tier и вложенный объект cache_creation. Каждый из них по отдельности можно подделать.

Однократная проверка

Python с requests, нативный формат Anthropic. На Claude Haiku три запроса стоят доли цента, на Sonnet — около цента.

import os, requests

BASE = os.environ["RELAY_BASE_URL"]      # e.g. https://your-relay.example (no /v1)
KEY = os.environ["RELAY_API_KEY"]
MODEL = os.environ.get("MODEL", "claude-sonnet-4-6")

H = {"x-api-key": KEY, "anthropic-version": "2023-06-01", "content-type": "application/json"}
TOOLS = [{"name": "get_weather", "description": "Get the weather for a city",
          "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}]
THINK = {"type": "enabled", "budget_tokens": 1024}
Q = {"role": "user", "content": "What is the weather in Paris? Use the tool."}

def call(messages):
    r = requests.post(f"{BASE}/v1/messages", headers=H, timeout=120, json={
        "model": MODEL, "max_tokens": 1600, "thinking": THINK, "tools": TOOLS, "messages": messages})
    return r.status_code, r.json()

s, a = call([Q])
blocks = a.get("content", [])
print("1st:", s, a.get("id"), [b["type"] for b in blocks])
print("usage fields:", sorted(a.get("usage", {}).keys()))
tool = next((b for b in blocks if b["type"] == "tool_use"), None)
think = next((b for b in blocks if b["type"] == "thinking"), None)
if not (tool and think and think.get("signature")):
    raise SystemExit("No thinking block with a signature: cannot run the test (try another model or prompt).")

def second(content):
    return call([Q, {"role": "assistant", "content": content},
                 {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool["id"], "content": "Sunny, 18C"}]}])

s2, b2 = second(blocks)
print("2nd, signature unchanged:", s2, "(expect 200)")

bad = [dict(b, signature=b["signature"][:-8] + "AAAAAAAA") if b["type"] == "thinking" else b for b in blocks]
s3, b3 = second(bad)
print("3rd, signature tampered:", s3, b3.get("error", {}).get("message", ""), "(expect 400 Invalid `signature`)")
Подпись без измененийИзменённая подписьЧто это значит
200400 «Invalid signature»Запросы доходят до Claude от Anthropic, прокси передаёт блоки thinking без искажений
200200Подписи никто не проверяет: бэкенд — не Anthropic
400400Прокси ломает блоки thinking, часто из-за конвертации в формат OpenAI; многоходовые сессии Claude Code на нём тоже будут падать

Наблюдение за прокси во времени

Один запуск показывает один момент, а прокси меняют маршрутизацию. Монитор ниже прогоняет те же три запроса для каждой указанной модели, дописывает в журнал строку JSON на каждую проверку, сравнивает её с прошлой проверкой той же модели и шлёт сообщение в Telegram, если что-то изменилось.

Он записывает четыре сигнала:

СигналО чём говорит изменение
Вердикт по подписиЗапросы больше не доходят до Anthropic или блоки thinking начали ломаться
Число входных токенов для фиксированного запросаОдин и тот же запрос к одной модели всегда даёт одно и то же число. Другое число — другой токенизатор (модель другого семейства) или системный промпт, добавленный прокси
Формат ID и поля usageЗа прокси сменился канал
ЗадержкаСлабый сигнал: сообщается, только если выросла втрое
import json, os, re, time
import requests

BASE = os.environ["RELAY_BASE_URL"].rstrip("/")
KEY = os.environ["RELAY_API_KEY"]
MODELS = [m.strip() for m in os.environ.get("MODELS", "claude-haiku-4-5").split(",") if m.strip()]
LOG = os.environ.get("LOG_FILE", "relay_watch.jsonl")

HEADERS = {"x-api-key": KEY, "anthropic-version": "2023-06-01", "content-type": "application/json"}
TOOLS = [{"name": "get_weather", "description": "Get the weather for a city",
          "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}]
THINKING = {"type": "enabled", "budget_tokens": 1024}
QUESTION = {"role": "user", "content": "What is the weather in Paris? Use the tool."}


def post(model, messages):
    t = time.time()
    r = requests.post(f"{BASE}/v1/messages", headers=HEADERS, timeout=180, json={
        "model": model, "max_tokens": 1600, "thinking": THINKING, "tools": TOOLS, "messages": messages})
    try:
        body = r.json()
    except ValueError:
        body = {}
    return r.status_code, body, int((time.time() - t) * 1000)


def check(model):
    res = {"ts": time.strftime("%Y-%m-%dT%H:%M:%S"), "model": model}
    status, a, ms = post(model, [QUESTION])
    res.update(status=status, latency_ms=ms)
    if status != 200:
        res["verdict"] = f"error {status}: {a.get('error', {}).get('message', '')[:120]}"
        return res
    blocks = a.get("content", [])
    usage = a.get("usage", {})
    tool = next((b for b in blocks if b.get("type") == "tool_use"), None)
    think = next((b for b in blocks if b.get("type") == "thinking"), None)
    res.update(
        id_format=bool(re.match(r"^msg_[0-9A-Za-z]{20,}$", a.get("id", ""))),
        tool_id_format=bool(tool and tool.get("id", "").startswith("toolu_")),
        usage_fields=all(k in usage for k in ("service_tier", "cache_creation")),
        # Один и тот же запрос к одной и той же модели всегда даёт одно и то же число входных токенов.
        # Другое число — другой токенизатор (другая модель) или добавленный системный промпт.
        input_tokens=usage.get("input_tokens"),
    )
    if not (tool and think and think.get("signature")):
        res["verdict"] = "no signed thinking block: signature test not possible"
        return res

    def second(content):
        return post(model, [QUESTION, {"role": "assistant", "content": content},
                            {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool["id"], "content": "Sunny, 18C"}]}])

    s2, _, _ = second(blocks)
    bad = [dict(b, signature=b["signature"][:-8] + "AAAAAAAA") if b.get("type") == "thinking" else b for b in blocks]
    s3, b3, _ = second(bad)
    rejected = s3 == 400 and "signature" in json.dumps(b3).lower()
    res.update(roundtrip=s2, tampered=s3, tamper_rejected=rejected)
    if s2 == 200 and rejected:
        res["verdict"] = "ok"
    elif s2 == 200 and s3 == 200:
        res["verdict"] = "SIGNATURES NOT CHECKED"
    else:
        res["verdict"] = f"thinking blocks broken ({s2}/{s3})"
    return res


def last_runs():
    prev = {}
    if os.path.exists(LOG):
        with open(LOG, encoding="utf-8") as f:
            for line in f:
                try:
                    r = json.loads(line)
                    prev[r["model"]] = r
                except (ValueError, KeyError):
                    pass
    return prev


def differences(old, new):
    if not old:
        return []
    out = []
    for k in ("verdict", "input_tokens", "id_format", "tool_id_format", "usage_fields"):
        if old.get(k) != new.get(k):
            out.append(f"{k}: {old.get(k)} -> {new.get(k)}")
    if old.get("latency_ms") and new.get("latency_ms") and new["latency_ms"] > 3 * old["latency_ms"]:
        out.append(f"latency_ms: {old['latency_ms']} -> {new['latency_ms']}")
    return out


def notify(text):
    token, chat = os.environ.get("TG_TOKEN"), os.environ.get("TG_CHAT")
    if token and chat:
        requests.post(f"https://api.telegram.org/bot{token}/sendMessage", timeout=30, data={"chat_id": chat, "text": text})


if __name__ == "__main__":
    prev = last_runs()
    with open(LOG, "a", encoding="utf-8") as f:
        for m in MODELS:
            r = check(m)
            f.write(json.dumps(r, ensure_ascii=False) + "\n")
            diff = differences(prev.get(m), r)
            print(m, r["verdict"], r.get("input_tokens"), f"{r['latency_ms']} ms", "; ".join(diff))
            if diff or r["verdict"] != "ok":
                notify(f"relay_watch {m}: {r['verdict']}\n" + "\n".join(diff))

Переменные окружения: RELAY_BASE_URL (без /v1), RELAY_API_KEY, MODELS (через запятую), LOG_FILE и при желании TG_TOKEN и TG_CHAT для Telegram. Запуск по cron, например раз в шесть часов:

0 */6 * * * . /opt/relay-watch/env && python3 /opt/relay-watch/relay_watch.py >> /opt/relay-watch/cron.log 2>&1

Историю удобно смотреть через jq:

jq -r '[.ts, .model, .verdict, .input_tokens, .latency_ms] | @tsv' relay_watch.jsonl | tail -20

Как читать изменения

Что изменилосьВероятная причина
Вердикт стал SIGNATURES NOT CHECKEDИзменённая подпись больше не отклоняется: запросы обрабатывает не Anthropic
Вердикт стал thinking blocks brokenПрокси начал конвертировать запросы и теряет подписи
Изменилось только input_tokensДобавлен или изменён системный промпт, либо запрос уходит в модель с другим токенизатором. Сначала проверьте, не выпустила ли Anthropic новую версию модели
Пропали ID msg_ / toolu_ или поля usageЗа прокси сменился канал
Ошибки 402 или 429Бюджет или лимиты ключа, а не подмена
Выросла только задержкаСкорее нагрузка, чем подмена; смотрите, сохранится ли

Чего не покажет никакой внешний тест

  • Он доказывает «это Claude от Anthropic», но не «какой именно Claude». Прокси, который тихо отправляет запросы к Opus в Sonnet (обе модели у Anthropic), пройдёт тест с настоящей подписью. Число входных токенов тут тоже не поможет: у моделей одного семейства токенизатор может совпадать. Подмену уровня видно только по стоимости и задержке в сравнении с официальным API на одинаковых запросах или по небольшому набору сложных задач, которые прогоняются время от времени.
  • Он показывает момент проверки. Повторяйте её и проверяйте модели, которыми реально пользуетесь.
  • Нужен нативный формат Anthropic. Эндпоинты в формате OpenAI выбрасывают подписи thinking, на них тест не запустить.
  • Прохождение — это «признаков подмены нет», а не доказательство. К тому же выводу приходят академические аудиты: какая модель работает, доказала бы только аппаратная аттестация (TEE), а её не даёт ни один канал перепродажи.

Наши результаты

19 октября 2026 года мы проверили APIVAI: разовый тест — на Claude Haiku 4.5 через публичный эндпоинт и на Claude Sonnet 4.6 на том же бэкенде, монитор — на Claude Haiku 4.5.

  • Первый запрос: ID сообщения msg_011…, ID инструмента toolu_…, в usage есть service_tier, inference_geo и вложенный cache_creation, блок thinking с подписью.
  • Подпись без изменений: 200. Изменённая подпись: 400 Invalid signature in thinking block.
  • Монитор показал одинаковое число входных токенов (759) в последовательных запусках; когда мы вручную изменили записанное число, следующий запуск сообщил об изменении.
  • Чего нет: заголовков anthropic-ratelimit-* в ответах, поэтому как сигнал мы их не предлагаем.

Больше эти тесты ничего не показывают — с учётом ограничений выше.

FAQ

Можно ли запустить тест на эндпоинте в формате OpenAI?

Нет. В формате OpenAI нет подписей thinking, проверять нечего. Укажите в скрипте нативный адрес Anthropic у прокси (тот, что использует Claude Code); если прокси даёт только формат OpenAI, этим тестом его не проверить.

Как часто запускать?

Для большинства задач достаточно раза в несколько часов: раз в шесть часов на двух моделях — это 24 запроса в сутки. Чаще раза в час смысла мало, подмена — не поминутное событие. Проверяйте те модели, за которые платите.

Сколько стоит одна проверка?

Три коротких запроса на модель: доли цента на Claude Haiku и около цента на Claude Sonnet по ценам самого прокси.

Заметит ли тест замену Opus на Sonnet?

Нет. Обе модели — от Anthropic, подпись будет настоящей в любом случае, а токенизатор у них может совпадать. Чтобы поймать замену уровня, сравнивайте стоимость и задержку с официальным API на одинаковых запросах или держите несколько сложных задач, на которых дешёвая модель ошибается.

Если прокси прошёл тест, можно не беспокоиться?

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

Связанные материалы

Готовы начать?

Получите API-ключ за 30 секунд. Оплата по факту использования Claude и GPT.

Начать