← Blog
Claude APIIntegraçãoPreços

Seu relay da API do Claude entrega o modelo real? Como verificar

(os preços do texto seguem a tabela de preços atual)

Resposta curta

Para verificar se um relay da API do Claude envia mesmo suas requisições ao Claude, use a assinatura do pensamento estendido: devolva um bloco thinking assinado sem alterações (um backend genuíno da Anthropic aceita) e depois devolva-o com a assinatura alterada (um backend genuíno recusa com Invalid signature in thinking block). Perguntar ao modelo quem ele é, perguntar a data de corte do conhecimento ou ler o campo model não prova nada. Até o teste da assinatura só mostra que as requisições chegam ao Claude da Anthropic, não qual modelo Claude respondeu, então rode o teste com regularidade e nos modelos que você paga.

Este guia é publicado pela APIVAI, um gateway para os modelos Claude e GPT. Os testes funcionam com qualquer relay que suporte o formato nativo da Anthropic, inclusive o nosso; nossos resultados estão perto do fim.

Por que a troca de modelo preocupa

Comprar o Claude por meio de um revendedor, relay ou gateway significa confiar que uma requisição a claude-sonnet-4-6 chega ao Claude Sonnet 4.6. Várias fontes indicam que essa confiança muitas vezes não se justifica:

  • Pesquisa. Um artigo do CISPA, "Real Money, Fake Models" (arXiv 2603.01919, aceito no ACM CCS 2026), auditou "APIs sombra" usadas em 187 artigos acadêmicos. Nos três serviços examinados a fundo, 45,83% das verificações de impressão digital do modelo não bateram com o modelo anunciado.
  • Fóruns. Fóruns de desenvolvedores chineses têm palavras próprias para isso: 以次充好 (fazer passar o barato por caro), 掺水 (aguar), 降智 (o modelo "ficou mais burro"). Uma troca típica no V2EX: "Como eles garantem que não é outro modelo?" Resposta: "Não garantem, e ainda misturam."
  • Marketplaces. No Hacker News, um usuário conta que foi roteado para versões muito quantizadas "pagando talvez 15% a menos".

A troca não é a única causa de "hoje está pior". A própria Anthropic publicou em 2025 uma análise de bugs de infraestrutura que pioraram parte das respostas do Sonnet, e prompts de sistema ou o tratamento do contexto também mudam as respostas. Por isso o teste precisa ser mecânico, não uma impressão.

Verificações que não provam nada

VerificaçãoPor que falha
Perguntar "qual modelo você é?"O relay pode injetar um prompt de sistema, e modelos genuínos também erram (num levantamento de 190 modelos, cerca de 60% responderam com um nome que não era o seu)
Perguntar a data de cortePerguntamos dez vezes ao Claude Haiku 4.5 em 19/10/2026 e ele respondeu "April 2024" todas as vezes: é consistente, mas um relay pode fazer qualquer modelo responder o mesmo com um prompt de sistema
Ler o campo modelQuem preenche é o relay
Julgar a qualidade das respostasRuído demais: regressões reais, prompts e contexto mudam a qualidade

Como funciona o teste da assinatura de thinking

Com o pensamento estendido ativado, a Anthropic devolve cada bloco thinking com uma signature opaca. Numa conversa com chamadas de ferramenta, o turno anterior do assistente, blocos de thinking incluídos, precisa ser reenviado na próxima requisição, e a Anthropic verifica a assinatura (documentação do extended thinking). Uma assinatura falsa ou editada é recusada:

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

O teste usa essa verificação em três requisições:

  1. Faça uma pergunta que leve o modelo a chamar uma ferramenta, com thinking ligado. A resposta traz um bloco thinking assinado e um bloco tool_use.
  2. Reenvie esse turno sem alterações, com o resultado da ferramenta. Um backend genuíno responde 200.
  3. Reenvie de novo com os últimos caracteres da assinatura trocados. Um backend genuíno responde 400 com o erro acima.

A terceira requisição é a prova. Um relay que serve outro modelo pode gerar uma sequência parecida com uma assinatura, mas não consegue fazer o verificador da Anthropic aceitar a assinatura real e recusar a editada, a menos que suas requisições cheguem de fato à Anthropic.

As mesmas respostas trazem indícios mais fracos que vale registrar: IDs de mensagem que começam com msg_, IDs de chamada de ferramenta que começam com toolu_, e objetos usage com service_tier e um objeto cache_creation aninhado. Cada um deles, isoladamente, pode ser forjado.

Rodar o teste uma vez

Python com requests, formato nativo da Anthropic. No Claude Haiku as três requisições custam uma pequena fração de centavo; no Sonnet, cerca de um centavo.

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`)")
Assinatura inalteradaAssinatura editadaO que significa
200400 "Invalid signature"As requisições chegam ao Claude da Anthropic e o relay repassa os blocos de thinking intactos
200200Ninguém verifica as assinaturas: o backend não é a Anthropic
400400O relay quebra os blocos de thinking, muitas vezes porque converte as requisições para o formato da OpenAI; sessões de vários turnos do Claude Code também vão falhar

Acompanhar um relay ao longo do tempo

Uma execução mostra um momento, e os relays mudam o roteamento. O monitor abaixo repete as mesmas três requisições para cada modelo informado, acrescenta uma linha JSON por verificação a um log, compara com a verificação anterior do mesmo modelo e envia uma mensagem no Telegram quando algo muda.

Ele registra quatro sinais:

SinalO que uma mudança sugere
Veredito da assinaturaAs requisições não chegam mais à Anthropic, ou os blocos de thinking começaram a ser quebrados
Número de tokens de entrada de uma requisição fixaA mesma requisição ao mesmo modelo sempre dá o mesmo número. Outro número indica outro tokenizador (outra família de modelos) ou um prompt de sistema adicionado pelo relay
Formato dos IDs e campos de usageOutro canal por trás do relay
LatênciaSinal fraco: só é avisada se triplicar
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")),
        # A mesma requisição ao mesmo modelo sempre dá o mesmo número de tokens de entrada.
        # Outro número indica outro tokenizador (outro modelo) ou um prompt de sistema adicionado.
        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))

Variáveis de ambiente: RELAY_BASE_URL (sem /v1), RELAY_API_KEY, MODELS (separados por vírgula), LOG_FILE e, opcionalmente, TG_TOKEN e TG_CHAT para o Telegram. Rode pelo cron, por exemplo a cada seis horas:

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

Consulte o histórico com jq:

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

Como interpretar uma mudança

O que mudouCausa provável
O veredito virou SIGNATURES NOT CHECKEDA assinatura editada não é mais recusada: a Anthropic não está processando as requisições
O veredito virou thinking blocks brokenO relay começou a converter as requisições e perde as assinaturas
Só input_tokens mudouUm prompt de sistema foi adicionado ou alterado, ou a requisição vai para um modelo com outro tokenizador. Verifique antes se a Anthropic lançou uma nova versão do modelo
Sumiram os IDs msg_ / toolu_ ou os campos de usageMudou o canal por trás do relay
Erros 402 ou 429Orçamento ou limites da chave, não troca de modelo
Só a latência subiuMais provável carga do que troca; observe se persiste

O que nenhum teste externo consegue dizer

  • Prova "o Claude da Anthropic", não "qual Claude". Um relay que manda em silêncio as requisições de Opus para o Sonnet, ambos na Anthropic, passa com uma assinatura genuína. O número de tokens de entrada também não ajuda aqui, porque modelos da mesma família podem compartilhar o tokenizador. Uma troca de nível só aparece comparando custo e latência com a API oficial em requisições idênticas, ou com um pequeno conjunto de tarefas difíceis rodadas de tempos em tempos.
  • Mostra o momento do teste. Repita, e nos modelos que você realmente usa.
  • Precisa do formato nativo da Anthropic. Endpoints no formato da OpenAI descartam as assinaturas de thinking, então o teste não roda neles.
  • Passar significa "nenhum indício de troca", não uma prova. As auditorias acadêmicas chegam à mesma conclusão: só a atestação por hardware (TEE) provaria qual modelo está rodando, e nenhum canal de revenda oferece isso.

Nossos resultados

Testamos a APIVAI em 19 de outubro de 2026: o teste único com o Claude Haiku 4.5 pelo endpoint público e com o Claude Sonnet 4.6 no mesmo backend, e o monitor com o Claude Haiku 4.5.

  • Primeira requisição: ID de mensagem msg_011…, ID de ferramenta toolu_…, usage com service_tier, inference_geo e um objeto cache_creation aninhado, e um bloco de thinking assinado.
  • Assinatura inalterada: 200. Assinatura editada: 400 Invalid signature in thinking block.
  • O monitor registrou o mesmo número de tokens de entrada (759) em execuções seguidas; quando editamos à mão o número registrado, a execução seguinte avisou da mudança.
  • Ausentes: cabeçalhos anthropic-ratelimit-* nas respostas, por isso não os oferecemos como sinal.

É tudo o que esses testes mostram, dentro dos limites acima.

FAQ

Posso rodar o teste num endpoint no formato da OpenAI?

Não. O formato da OpenAI não carrega assinaturas de thinking, então não há o que verificar. Aponte o script para o endereço nativo da Anthropic do relay (o que o Claude Code usa); se um relay só oferece o formato da OpenAI, este teste não consegue verificá-lo.

Com que frequência devo rodar?

A cada poucas horas basta na maioria dos casos: a cada seis horas com dois modelos são 24 requisições por dia. Rodar mais de uma vez por hora acrescenta pouco, porque a troca não acontece minuto a minuto. Verifique os modelos que você realmente paga.

Quanto custa uma verificação?

Três requisições curtas por modelo: uma pequena fração de centavo no Claude Haiku e cerca de um centavo no Claude Sonnet, aos preços do próprio relay.

Ele percebe se o Opus for trocado pelo Sonnet?

Não. Os dois são modelos da Anthropic, então a assinatura é genuína em ambos os casos, e eles podem compartilhar o tokenizador. Para pegar uma troca de nível, compare custo e latência com a API oficial em requisições idênticas, ou mantenha algumas tarefas difíceis que o modelo mais barato erra.

Se um relay passar, posso ficar tranquilo?

Passar significa que não há sinal de troca no momento do teste, nada além disso. Continue monitorando, acompanhe a fatura por requisição e prefira relays que mostrem, para cada requisição, tokens e custos que você consiga conferir.

Guias relacionados

Pronto para começar?

Obtenha sua chave de API em 30 segundos. Pague pelo uso do Claude e do GPT.

Começar