Convide e ganhe

Como funcionam as recompensas

Compartilhe seu link. Quando um amigo se cadastrar por ele e adicionar saldo, você receberá a recompensa exibida nas recargas posteriores.

Anthropic Messages, Chat Completions e Responses: como escolher, converter e entender os limites de compatibilidade

HTTP 200 não significa que um agente seja compatível. Três ciclos completos de ferramentas mostram as diferenças reais entre Messages, Chat Completions e Responses, além de orientar migração, testes e diagnóstico.

Conteúdo

Um mesmo agente pode continuar recebendo 200 depois que você troca o endpoint e os nomes dos campos, mas deixar de executar ferramentas, retornar JSON fora do Schema ou perder de repente o contexto entre turnos. Em geral, isso não significa que o modelo “ficou menos inteligente”. A aplicação apenas tratou três protocolos diferentes como se fossem uma única interface.

Uma migração bem-sucedida precisa ser verificada em pelo menos três níveis:

  1. O formato é aceito: o servidor consegue interpretar a solicitação e retorna um status de sucesso.
  2. O comportamento é equivalente: as ferramentas são chamadas, os resultados voltam corretamente ao modelo, o stream termina por completo e o contexto multietapas continua coerente.
  3. As capacidades são preservadas: Schema estrito, estado nativo de reasoning, ferramentas hospedadas e saídas estruturadas não são ignorados nem rebaixados silenciosamente.

HTTP 200 comprova apenas o primeiro nível. Uma conversão simples pode bastar para uma solicitação que gera apenas um bloco de texto. Quando entram agentes, ferramentas, argumentos em streaming, estado multietapas ou modelos de reasoning, é necessário validar toda a cadeia de interação.

Conclusão prática: escolha o protocolo pelo cliente e pelas capacidades, não pelo nome do modelo

CenárioMelhor ponto de partidaMotivo
Uma aplicação existente já usa o OpenAI SDK e messages de forma estávelChat CompletionsExige menos mudanças e permite manter o ciclo atual de mensagens e ferramentas
Um novo agente OpenAI precisa de ferramentas hospedadas, Items tipados ou continuidade de estado no servidorResponsesA OpenAI recomenda essa interface para novos projetos, e ela oferece um conjunto mais amplo de recursos para agentes
Claude Code, uma aplicação nativa do Claude ou um fluxo que depende de recursos específicos do ClaudeAnthropic MessagesBlocos de conteúdo, resultados de ferramentas, thinking e outros comportamentos seguem o contrato nativo da Anthropic
Um gateway próprio ou roteador multimodeloManter um adaptador separado para cada protocolo upstreamUm único “JSON universal” não consegue representar todos os recursos nativos sem perdas

A OpenAI ainda oferece suporte a Chat Completions, portanto uma aplicação estável não precisa ser reescrita imediatamente apenas porque existe uma interface mais nova. A migração faz mais sentido em projetos novos ou quando há necessidade de recursos nativos de Responses. Anthropic Messages também não é uma interface OpenAI com um campo renomeado para messages: seus blocos de conteúdo, repasse de ferramentas, eventos de streaming e regras de estado formam um contrato próprio.

Diferenças centrais entre as três APIs

Neste artigo, Completions significa Chat Completions, e não o endpoint antigo /v1/completions.

DimensãoOpenAI Chat CompletionsOpenAI ResponsesAnthropic Messages
Endpoint/v1/chat/completions/v1/responses/v1/messages
Entrada principalmessagesItems em input; também aceita entrada simples por mensagensmessages, normalmente com um system separado no nível superior
Saída principalchoices[].messageItems tipados em output[]Blocos de conteúdo em content[]
Definição de ferramentastools[].functionname e parameters aparecem diretamente em tools[]input_schema dentro de tools[]
Argumentos da ferramentafunction.arguments, uma string JSONarguments, uma string JSONtool_use.input, um objeto JSON
ID de correlaçãotool_calls[].idcall_idtool_use.id
Retorno do resultadorole: "tool" + tool_call_idfunction_call_output + call_idtool_result + tool_use_id em uma mensagem user
Estado multietapasA aplicação reenvia o histórico de mensagensReenvio de Items, previous_response_id ou ConversationsA aplicação reenvia mensagens e blocos de conteúdo
Saída estruturada finalresponse_formattext.formatoutput_config.format
Streamingchoices[].deltaEventos tipados de ResponsesEventos de message/content block

A tabela pode dar a impressão de que basta renomear campos. Os problemas reais costumam aparecer na segunda solicitação: depois que o modelo produz uma chamada de ferramenta, como a aplicação a executa, qual ID precisa preservar e com que papel e ordem devolve o resultado? A seguir, a mesma tarefa sem efeitos colaterais é percorrida por completo nos três protocolos.

Exemplo único: consultar um plano de teste

O usuário pergunta:

Consulte o plano team e informe se ele permite cobrança por uso após exceder o volume incluído.

A ferramenta se chama get_plan_info. Ela lê apenas dados locais fixos e não produz efeitos colaterais externos, por isso é adequada para testar migração de protocolo.

Os dados do plano abaixo são sintéticos e servem apenas para fins didáticos. Eles não representam planos, preços ou benefícios reais da OpenAI, Anthropic ou BetterToken. As três sequências de solicitações e respostas mostram a estrutura dos protocolos e não são registros de execuções reais de API.

A ferramenta no lado da aplicação pode ser implementada como uma função independente do protocolo:

from __future__ import annotations

import json
from typing import Any


PLAN_FIXTURES: dict[str, dict[str, Any]] = {
    "team": {
        "plan_code": "team",
        "display_name": "Team",
        "billing_mode": "usage_based",
        "included_requests": 10_000,
        "overage_allowed": True,
        "source_version": "fixture-2026-09-01",
    }
}


def execute_tool(name: str, raw_arguments: str | dict[str, Any]) -> str:
    """Executa a ferramenta didática somente leitura e retorna uma string JSON que pode ser enviada diretamente ao modelo."""
    if isinstance(raw_arguments, str):
        arguments = json.loads(raw_arguments)
    elif isinstance(raw_arguments, dict):
        arguments = raw_arguments
    else:
        raise TypeError("os argumentos da ferramenta devem ser uma string JSON ou um objeto")

    if name != "get_plan_info":
        raise ValueError(f"ferramenta desconhecida: {name}")

    if set(arguments) != {"plan_code"}:
        raise ValueError("get_plan_info aceita somente plan_code")

    plan_code = arguments["plan_code"]
    if not isinstance(plan_code, str):
        raise TypeError("plan_code deve ser uma string")

    plan = PLAN_FIXTURES.get(plan_code)
    if plan is None:
        return json.dumps(
            {"ok": False, "error": "plan_not_found", "plan_code": plan_code},
            ensure_ascii=False,
        )

    return json.dumps({"ok": True, "data": plan}, ensure_ascii=False)

Mesmo que a solicitação habilite um Schema estrito, a aplicação deve manter suas próprias validações de entrada. O modo estrito limita os argumentos gerados pelo modelo, mas não substitui autorização, validação de enums, idempotência nem verificações de segurança da lógica de negócio.

Chat Completions: ciclo completo de uma ferramenta

Primeira solicitação: pedir que o modelo gere uma chamada de ferramenta

Os exemplos abaixo usam o endpoint oficial da OpenAI para demonstrar o protocolo. Ao conectar um serviço compatível, substitua Base URL, método de autenticação e Model ID conforme a documentação do provedor.

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "messages": [
      {
        "role": "system",
        "content": "Você é um assistente de planos. Responda somente com os dados retornados pela ferramenta; não faça suposições."
      },
      {
        "role": "user",
        "content": "Consulte o plano team e informe se ele permite cobrança por uso após exceder o volume incluído."
      }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_plan_info",
          "description": "Consultar dados de teste fixos pelo código do plano",
          "strict": true,
          "parameters": {
            "type": "object",
            "properties": {
              "plan_code": {
                "type": "string",
                "enum": ["team"]
              }
            },
            "required": ["plan_code"],
            "additionalProperties": false
          }
        }
      }
    ],
    "tool_choice": "required",
    "parallel_tool_calls": false
  }'

A aplicação precisa ler tool_calls na mensagem assistant. A resposta abaixo mantém apenas os campos necessários para a próxima etapa:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_plan_001",
            "type": "function",
            "function": {
              "name": "get_plan_info",
              "arguments": "{\"plan_code\":\"team\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

Dois valores não podem ser perdidos:

  • tool_calls[0].id: deve voltar sem alterações como tool_call_id na segunda solicitação.
  • function.arguments: é uma string JSON. Primeiro faça o parse; depois aplique sua própria validação de Schema e de negócio.

Execute a ferramenta:

tool_result = execute_tool(
    "get_plan_info",
    "{\"plan_code\":\"team\"}",
)

Segunda solicitação: devolver o resultado da ferramenta ao modelo

Chat Completions exige que a mensagem assistant com a chamada original seja mantida no histórico, seguida por uma mensagem de resultado com role: "tool".

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "messages": [
      {
        "role": "system",
        "content": "Você é um assistente de planos. Responda somente com os dados retornados pela ferramenta; não faça suposições."
      },
      {
        "role": "user",
        "content": "Consulte o plano team e informe se ele permite cobrança por uso após exceder o volume incluído."
      },
      {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_plan_001",
            "type": "function",
            "function": {
              "name": "get_plan_info",
              "arguments": "{\"plan_code\":\"team\"}"
            }
          }
        ]
      },
      {
        "role": "tool",
        "tool_call_id": "call_plan_001",
        "content": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
      }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_plan_info",
          "description": "Consultar dados de teste fixos pelo código do plano",
          "strict": true,
          "parameters": {
            "type": "object",
            "properties": {
              "plan_code": {
                "type": "string",
                "enum": ["team"]
              }
            },
            "required": ["plan_code"],
            "additionalProperties": false
          }
        }
      }
    ]
  }'

Uma mensagem final representativa seria:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "O plano Team permite cobrança por uso após exceder o volume incluído. Os dados de teste incluem 10.000 solicitações, e overage_allowed é true."
      },
      "finish_reason": "stop"
    }
  ]
}

Se o adaptador converter apenas o primeiro turno do usuário, mas não preservar os tool_calls do assistant, ou inserir o ID errado em tool_call_id, a segunda solicitação deixa de ser a continuação da mesma chamada.

Responses: ciclo completo de uma ferramenta

Responses representa mensagens, reasoning, chamadas e resultados como tipos diferentes de Item. Não presuma que output[0] sempre seja o texto final; a lógica deve tratar cada Item de acordo com seu type.

Primeira solicitação: pedir ao modelo um Item function_call

curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "instructions": "Você é um assistente de planos. Responda somente com os dados retornados pela ferramenta; não faça suposições.",
    "input": "Consulte o plano team e informe se ele permite cobrança por uso após exceder o volume incluído.",
    "tools": [
      {
        "type": "function",
        "name": "get_plan_info",
        "description": "Consultar dados de teste fixos pelo código do plano",
        "strict": true,
        "parameters": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "tool_choice": "required",
    "parallel_tool_calls": false,
    "store": false
  }'

Um Item representativo de chamada seria:

{
  "id": "resp_plan_001",
  "object": "response",
  "output": [
    {
      "type": "function_call",
      "id": "fc_plan_001",
      "call_id": "call_plan_001",
      "name": "get_plan_info",
      "arguments": "{\"plan_code\":\"team\"}",
      "status": "completed"
    }
  ]
}

Use call_id para correlacionar o resultado. id: "fc_plan_001" é o ID do próprio Item e não deve substituir call_id.

Execute a ferramenta:

tool_result = execute_tool(
    "get_plan_info",
    "{\"plan_code\":\"team\"}",
)

Segunda solicitação: devolver um function_call_output

O exemplo abaixo usa reprodução manual sem estado, portanto envia novamente instructions, a entrada original do usuário, a chamada da ferramenta e seu resultado.

curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "instructions": "Você é um assistente de planos. Responda somente com os dados retornados pela ferramenta; não faça suposições.",
    "input": [
      {
        "role": "user",
        "content": "Consulte o plano team e informe se ele permite cobrança por uso após exceder o volume incluído."
      },
      {
        "type": "function_call",
        "call_id": "call_plan_001",
        "name": "get_plan_info",
        "arguments": "{\"plan_code\":\"team\"}"
      },
      {
        "type": "function_call_output",
        "call_id": "call_plan_001",
        "output": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
      }
    ],
    "tools": [
      {
        "type": "function",
        "name": "get_plan_info",
        "description": "Consultar dados de teste fixos pelo código do plano",
        "strict": true,
        "parameters": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "store": false
  }'

Um Item final de saída representativo seria:

{
  "id": "resp_plan_002",
  "object": "response",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "O plano Team permite cobrança por uso após exceder o volume incluído. Os dados de teste incluem 10.000 solicitações, e overage_allowed é true."
        }
      ]
    }
  ]
}

Se você optar por continuidade de estado no servidor, pode permitir que a primeira resposta seja armazenada e usar o formato abaixo na segunda solicitação:

{
  "model": "YOUR_OPENAI_MODEL",
  "previous_response_id": "resp_plan_001",
  "input": [
    {
      "type": "function_call_output",
      "call_id": "call_plan_001",
      "output": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"overage_allowed\":true}}"
    }
  ]
}

Um previous_response_id pertence ao serviço upstream que criou aquela resposta. Ele não pode ser enviado a outro provedor para continuidade. Isso também não torna a entrada anterior gratuita: a documentação atual da OpenAI informa que os token de entrada anteriores da cadeia continuam sendo cobrados como input.

Quando a resposta contém um reasoning Item, a reprodução sem estado também precisa preservar o Item correspondente conforme a documentação. Não é possível descartá-lo para criar um “formato uniforme” e ainda afirmar que o contexto de reasoning permaneceu equivalente.

Anthropic Messages: ciclo completo de uma ferramenta

Messages representa a chamada como um bloco tool_use no conteúdo assistant e devolve o resultado como um bloco tool_result na próxima mensagem user. Os argumentos já são um objeto, e não uma string JSON ainda pendente de parse.

Primeira solicitação: pedir ao Claude que retorne tool_use

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "YOUR_CLAUDE_MODEL",
    "max_tokens": 512,
    "system": "Você é um assistente de planos. Responda somente com os dados retornados pela ferramenta; não faça suposições.",
    "messages": [
      {
        "role": "user",
        "content": "Consulte o plano team e informe se ele permite cobrança por uso após exceder o volume incluído."
      }
    ],
    "tools": [
      {
        "name": "get_plan_info",
        "description": "Consultar dados de teste fixos pelo código do plano",
        "strict": true,
        "input_schema": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "tool_choice": {
      "type": "tool",
      "name": "get_plan_info"
    }
  }'

Forçar uma ferramenta específica depende do suporte do modelo e da configuração escolhidos. Se o modelo de destino não oferecer esse suporte, use auto e verifique na aplicação se uma chamada realmente foi retornada.

Uma resposta representativa seria:

{
  "id": "msg_plan_001",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_plan_001",
      "name": "get_plan_info",
      "input": {
        "plan_code": "team"
      }
    }
  ],
  "stop_reason": "tool_use"
}

O objeto input pode ser enviado diretamente ao executor da ferramenta:

tool_result = execute_tool(
    "get_plan_info",
    {"plan_code": "team"},
)

Segunda solicitação: colocar tool_result na mensagem user imediatamente seguinte

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "YOUR_CLAUDE_MODEL",
    "max_tokens": 512,
    "system": "Você é um assistente de planos. Responda somente com os dados retornados pela ferramenta; não faça suposições.",
    "messages": [
      {
        "role": "user",
        "content": "Consulte o plano team e informe se ele permite cobrança por uso após exceder o volume incluído."
      },
      {
        "role": "assistant",
        "content": [
          {
            "type": "tool_use",
            "id": "toolu_plan_001",
            "name": "get_plan_info",
            "input": {
              "plan_code": "team"
            }
          }
        ]
      },
      {
        "role": "user",
        "content": [
          {
            "type": "tool_result",
            "tool_use_id": "toolu_plan_001",
            "content": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
          }
        ]
      }
    ],
    "tools": [
      {
        "name": "get_plan_info",
        "description": "Consultar dados de teste fixos pelo código do plano",
        "strict": true,
        "input_schema": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ]
  }'

Uma resposta final representativa seria:

{
  "id": "msg_plan_002",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "O plano Team permite cobrança por uso após exceder o volume incluído. Os dados de teste incluem 10.000 solicitações, e overage_allowed é true."
    }
  ],
  "stop_reason": "end_turn"
}

Messages tem requisitos claros de ordem: tool_result deve aparecer imediatamente após a mensagem assistant que contém o tool_use correspondente. Se um turno assistant gerar várias chamadas de ferramentas do cliente, todos os blocos de resultado devem voltar na mensagem user seguinte e ser correlacionados um a um por tool_use_id. Se a mesma mensagem user também contiver texto normal, os blocos de resultado devem vir antes do texto.

O que pode ser mapeado diretamente e o que inevitavelmente perde informação

CapacidadeAvaliação da conversãoTratamento correto
Texto comum do usuárioNormalmente pode ser mapeado diretamentePreservar texto, ordem e tipos multimodais, não apenas as strings visíveis
Schema básica de funçãoPode ser remodeladaConverter entre function.parameters, parameters de Responses e input_schema de Messages, e depois revalidar o subconjunto compatível de JSON Schema
Argumentos da ferramentaÉ preciso converter o tipoAs duas interfaces OpenAI normalmente retornam strings JSON; Messages retorna um objeto. Normalizar, fazer parse e validar antes da lógica de negócio
ID da chamadaO significado deve ser preservado, mas namespaces não podem ser reutilizadosManter um canonical call ID interno junto do ID original do upstream e devolver o campo próprio do protocolo
Chamadas paralelasPodem ser suportadas, mas nunca correlacionadas pela posição no arrayAssociar cada resultado por tool_call_id, call_id ou tool_use_id
Instruções system/developerPodem sofrer perdasDistinguir escopo global, de etapa da conversa e de turno único; degradar ou rejeitar explicitamente se o protocolo de destino não expressar o escopo original
Saída estruturada finalOs campos não são mecanicamente intercambiáveisChat usa response_format, Responses usa text.format e Messages usa output_config.format
Argumentos em streamingExigem parser específico do protocoloAcumular fragmentos por evento e ID da chamada e fazer parse do JSON somente após o evento de conclusão
Estado multietapas no servidorNão há equivalente universalIDs como previous_response_id ficam vinculados ao upstream original; entre upstreams, reproduzir o contexto visível ou usar sticky routing
Estado thinking/reasoningNormalmente não pode ser convertido sem perdasPreservar Items opacos, thinking blocks, assinaturas ou conteúdo criptografado exatamente como o protocolo nativo exigir; nunca inventá-los
Ferramentas hospedadasMuitas vezes não têm equivalente diretoDeclarar suporte e alternativas separadamente para web search, file search, computer use, server tools e recursos semelhantes
Geração de vários candidatosPode não haver equivalenteNão assumir que n de Chat Completions mapeia diretamente para Responses; fazer várias solicitações na aplicação ou mudar o comportamento do produto

Por isso, a abstração interna mais confiável para um gateway não é um grande objeto com todos os campos possíveis. Modele separadamente mensagens, escopo de instruções, definições de ferramentas, chamadas, resultados, identificadores de estado, eventos de streaming e estado nativo opaco. Quando uma capacidade não puder ser representada, retorne explicitamente “não suportado” ou “conversão com perdas”, em vez de excluir o campo em silêncio.

strict, response_format e text.format resolvem problemas diferentes

Um erro frequente de migração é tratar “argumentos válidos de ferramenta” e “resposta final em um formato JSON específico” como a mesma função.

ObjetivoChat CompletionsResponsesAnthropic Messages
Restringir argumentos da chamadatools[].function.stricttools[].stricttools[].strict
Restringir a saída final do modeloresponse_formattext.formatoutput_config.format

O strict da ferramenta restringe como o modelo chama a função. A saída estruturada final restringe o conteúdo entregue ao usuário. Um agente pode precisar dos dois ao mesmo tempo: primeiro chamar uma ferramenta com argumentos estritos e depois devolver o resultado sob um JSON Schema fixo.

A documentação atual da OpenAI também contém uma diferença de padrão fácil de ignorar:

  • Em Chat Completions, chamadas de função não são estritas por padrão.
  • Quando strict é omitido em Responses, o serviço tenta normalizar o Schema para o modo estrito. Se houver incompatibilidade, pode voltar ao modo não estrito e exibir strict: false na definição interpretada.

Para deixar a intenção clara e evitar dependência de padrões específicos da interface, solicitações de produção devem definir deliberadamente strict: true ou strict: false. Um Schema estrito também precisa cumprir os requisitos correspondentes, como proibir propriedades adicionais e listar todos os campos obrigatórios.

Mais importante: uma camada de compatibilidade pode aceitar um campo sem aplicar a restrição. A documentação oficial da Anthropic sobre compatibilidade com o OpenAI SDK informa que, nessa camada específica, campos como function strict, response_format e reasoning_effort são ignorados, e a maioria dos campos não suportados não produz erro. Assim, uma solicitação pode retornar 200 mesmo que o Schema ou a configuração de reasoning não tenham sido aplicados.

Isso não significa que Anthropic Messages nativo não tenha recursos equivalentes. Messages nativo oferece entrada estrita de ferramentas e usa output_config.format para o JSON final. O diagnóstico deve começar respondendo: a chamada está indo para Messages nativo ou para uma camada OpenAI-compatible?

system, developer e escopo de instruções não podem ser preservados apenas concatenando strings

Interfaces no estilo OpenAI aceitam diferentes papéis no histórico, enquanto Responses também oferece instructions. Anthropic Messages tradicionalmente usa um system no nível superior. Em setembro de 2026, alguns modelos atuais também aceitam role: "system" no meio da conversa, mas não todos, e há restrições de posição e ordem em relação às ferramentas.

Ao mesmo tempo, a camada de compatibilidade da Anthropic com o OpenAI SDK coleta mensagens system/developer, junta-as com quebras de linha e as promove para um único prompt de sistema no início. Isso torna a solicitação utilizável, mas altera o momento e o escopo originais. Uma instrução developer destinada a começar apenas no oitavo turno pode afetar a semântica dos sete primeiros depois de ser movida para o início.

Um adaptador mais seguro primeiro distingue três níveis dentro da aplicação:

  • Instruções globais: valem para toda a conversa.
  • Instruções de etapa: passam a valer a partir de um turno específico.
  • Instruções de turno único: controlam somente a tarefa atual.

Mapeie a instrução apenas quando o protocolo de destino puder expressar o mesmo escopo. Caso contrário, adote uma estratégia explícita: manter a solicitação em um modelo que suporte o recurso, degradar a instrução e registrar a diferença, ou rejeitar a migração. A concatenação silenciosa exige pouco código, mas costuma gerar “a solicitação funcionou, o comportamento mudou”.

Streaming exige uma máquina de estados, não simples concatenação de token de texto

As três interfaces oferecem streaming, mas os eventos não são equivalentes:

  • Chat Completions normalmente acumula texto e fragmentos de tool_calls em choices[].delta.
  • Responses emite eventos tipados como response.output_text.delta, response.function_call_arguments.delta, response.function_call_arguments.done, response.completed e error.
  • Messages usa message_start, content_block_start, content_block_delta, content_block_stop, message_delta e message_stop; os argumentos chegam fragmentados por input_json_delta.partial_json.

Os argumentos podem ser divididos assim:

{"plan_
code":"te
am"}

Nenhum desses fragmentos é JSON válido sozinho. Acumule-os por ID da chamada ou índice do bloco e faça o parse somente depois de receber o evento de conclusão dos argumentos:

from __future__ import annotations

import json
from collections import defaultdict
from typing import Any


class ToolArgumentAssembler:
    def __init__(self) -> None:
        self._buffers: dict[str, list[str]] = defaultdict(list)

    def add_delta(self, call_id: str, fragment: str) -> None:
        self._buffers[call_id].append(fragment)

    def finish(self, call_id: str) -> dict[str, Any]:
        if call_id not in self._buffers:
            raise KeyError(f"call_id desconhecido: {call_id}")

        raw = "".join(self._buffers.pop(call_id))
        value = json.loads(raw)
        if not isinstance(value, dict):
            raise TypeError("os argumentos da ferramenta devem ser decodificados como um objeto")
        return value

    def discard(self, call_id: str) -> None:
        self._buffers.pop(call_id, None)

O adaptador também deve registrar um estado terminal explícito:

created -> receiving -> completed
                   \-> failed
                   \-> disconnected

disconnected não é igual a completed. Anthropic Messages pode emitir um event: error dentro do stream depois que a conexão HTTP já foi estabelecida com sucesso; Responses também possui eventos de erro separados. Observar apenas o status HTTP inicial, ou tratar o encerramento da conexão como conclusão natural, pode truncar argumentos ou a resposta final.

O parser também deve tolerar tipos de evento desconhecidos: registrar e ignorar os que não afetem o recurso atual, em vez de interromper todo o cliente sempre que o servidor adicionar um evento.

Estado multietapas e estado de reasoning não podem ser inventados

Chat Completions e fluxos tradicionais de Messages normalmente dependem do reenvio do histórico pela aplicação. Responses também pode manter estado no servidor com previous_response_id ou Conversations. O conceito de “turno anterior” não é intercambiável entre eles.

Quando um gateway recebe um ID de estado, só existem três estratégias válidas:

  1. Sticky routing: enviar as próximas solicitações ao mesmo upstream que criou o estado.
  2. Reprodução completa: reenviar todas as mensagens, chamadas, resultados e elementos de estado nativo que possam ser reproduzidos legalmente.
  3. Rejeição explícita: se o upstream de destino não puder continuar, devolver um erro diagnosticável e permitir que o cliente reinicie a conversa.

Não envie um previous_response_id da OpenAI para a Anthropic nem apresente um ID interno de conversa do gateway como se outro provedor pudesse entendê-lo.

O estado de reasoning também não se resolve renomeando campos:

  • Em configurações sem estado ou com certas políticas de retenção, Responses pode retornar reasoning Items criptografados que precisam ser reenviados na solicitação seguinte.
  • Fluxos de thinking da Anthropic podem incluir thinking blocks, assinaturas ou outro estado opaco. Ao usar ferramentas e conversas multietapas, preserve-os conforme a documentação nativa.
  • Em setembro de 2026, o uso manual de Anthropic thinking.type: "enabled" com budget_tokens está obsoleto em modelos da geração 4.6 e é rejeitado nas gerações 4.7 e posteriores; os modelos mais novos usam adaptive thinking e o controle de effort correspondente.

Portanto, não é possível criar uma regra permanente que equipare OpenAI reasoning_effort a Anthropic budget_tokens. Uma descrição correta da capacidade deve incluir o modelo de destino, seu modo thinking atual e a estratégia de degradação quando não houver suporte.

Chamadas paralelas: correlacione por ID, nunca pela posição no array

Um modelo pode solicitar várias ferramentas em um turno. Os tempos de execução variam e os resultados podem chegar em outra ordem. O adaptador precisa manter uma relação semelhante a esta:

canonical_call_id
  -> provider
  -> provider_call_id
  -> tool_name
  -> validated_arguments
  -> execution_status
  -> result

Ao devolver os resultados:

  • Chat Completions cria uma mensagem role: "tool" para cada resultado e inclui o tool_call_id correspondente.
  • Responses cria um Item function_call_output para cada resultado e inclui o call_id correspondente.
  • Messages coloca os blocos tool_result no turno user imediatamente seguinte e inclui em cada um seu tool_use_id.

Durante os testes de migração, comece com parallel_tool_calls: false, faça o fluxo com uma única ferramenta funcionar e só então ative o paralelismo. Em produção, ferramentas com efeitos colaterais — enviar e-mail, cobrar ou criar recursos — também precisam de chaves de idempotência. Retries de rede, desconexões do stream ou replay pelo upstream podem entregar a mesma chamada semântica de novo; o texto gerado pelo modelo não basta para saber se ela já foi executada.

Por que HTTP 200 não basta para validar compatibilidade

Um teste de migração realmente útil deve cobrir pelo menos estes caminhos:

TesteCritério de aprovação
Texto comumO conteúdo é legível e o escopo system/developer se comporta como esperado
Uma chamada de ferramentaNome, argumentos, ID, resultado e resposta final formam um ciclo completo
Chamadas paralelasCada resultado é associado por ID sem trocas nem perdas
Argumentos em streamingOs fragmentos são montados por completo e o JSON é analisado ao final
Schema estrito da ferramentaCampos e tipos inválidos são rejeitados como esperado ou degradados explicitamente
Saída estruturada finalA resposta cumpre o Schema especificado, e não apenas “parece JSON”
Erro de execuçãoO modelo recebe um erro estruturado e não entra em loop nem inventa sucesso
Continuidade multietapasO segundo turno consegue citar fatos do primeiro, com regras de estado claras
reasoning/thinkingO modo declarado funciona e o estado nativo não é removido nem inventado
Erros no stream e desconexõesO cliente distingue conclusão, falha e interrupção de conexão
Erro controlado de APITipo de erro, request ID e política de retry continuam diagnosticáveis

Use entradas e fixtures fixos e registre separadamente, para cada protocolo:

  • se o resultado final de negócio é equivalente;
  • se a chamada e o retorno do resultado estão completos;
  • latência P50 e P95;
  • usage de input, output e cache;
  • tipo de erro, request ID e estado terminal;
  • quais capacidades foram degradadas explicitamente.

Não registre API Keys, prompts sensíveis completos nem saídas privadas do usuário. No mínimo, logs de erro devem preservar HTTP status, upstream error type/code, uma mensagem curta, request ID, endpoint, protocolo, Model ID e estado terminal do stream. Caso contrário, model_not_found, falta de permissão e caminhos incompatíveis podem virar um único 400 impossível de diagnosticar.

Diagnóstico por sintoma: onde o agente quebrou

SintomaCausa comumComo diagnosticar e corrigir
Retorna 200, mas o modelo nunca chama uma ferramentaA definição não foi enviada, tool_choice foi ignorado, o modelo não suporta ferramentas ou o prompt é insuficienteImprimir a solicitação final; verificar o modelo e a camada compatível; expor apenas uma ferramenta somente leitura no teste e forçar ou pedir explicitamente seu uso
O modelo retorna uma chamada, mas a aplicação não executaA aplicação ainda lê um campo antigo, como apenas message.contentLer tool_calls, o Item function_call ou o bloco tool_use conforme o protocolo
O JSON dos argumentos não faz parseUm fragmento de streaming foi tratado como JSON completo ou um objeto foi analisado novamente como stringEsperar o evento de conclusão; primeiro identificar se o valor é string ou objeto
Aparecem campos extras mesmo com strictA camada compatível ignora silenciosamente, o Schema não atende ao modo estrito ou a chamada não vai ao endpoint nativoVerificar endpoint e documentação; definir strict explicitamente; adicionar um teste de regressão que viole o Schema de propósito
A segunda solicitação informa que falta resultadoO ID não corresponde ou o primeiro Item assistant/tool não foi preservadoGuardar a chamada upstream e seu ID sem alterações; devolver o resultado imediatamente onde o protocolo exige
Messages retorna tool_use ids ... without tool_resulttool_result não vem logo após a chamada ou há texto comum antes deleColocar todos os tool_result correspondentes na próxima mensagem user e antes do texto opcional
O streaming trava ou entrega só parte dos argumentosO cliente espera apenas o fim do texto e não trata estados terminais de argumentos e errosImplementar uma máquina de eventos específica por protocolo e distinguir completed, failed, error e disconnected
O segundo turno esquece o primeiroHistórico, chamadas ou result Items foram omitidos; ou previous_response_id pertence a outro upstreamReenviar todo o contexto visível ou manter sticky routing; não transferir IDs de estado entre provedores
Uma instrução system passa a valer cedo demais ao trocar de interfaceA camada compatível promoveu um system/developer do meio da conversa para o inícioModelar o escopo; degradar visivelmente ou manter o protocolo nativo quando o mapeamento sem perdas for impossível
A ferramenta executa duas vezesA solicitação foi repetida, o stream foi reproduzido após desconexão ou não há idempotênciaUsar ferramentas somente leitura nos testes; derivar a chave de idempotência de ferramentas com efeitos colaterais de um canonical call ID
A saída final é JSON, mas campos somem às vezesO prompt apenas pede “retorne JSON” sem habilitar saída estruturadaUsar response_format, text.format ou output_config.format da interface e validar novamente na aplicação

Sequência de migração mais segura

  1. Identifique o protocolo realmente enviado pelo cliente. Não deduza pelo nome do modelo. Registre endpoint completo, método do SDK, campos de nível superior e tipos de evento do stream.
  2. Liste os comportamentos que precisam ser mantidos. No mínimo: ferramentas, chamadas paralelas, Schema estrito, saída estruturada final, estado multietapas, streaming e thinking/reasoning.
  3. Prefira primeiro o protocolo nativo. Se um recurso puder ser implementado diretamente com Messages ou Responses nativo, evite uma camada compatível adicional.
  4. Crie uma matriz de capacidades de conversão. Marque cada função como totalmente suportada, suportada com perdas ou não suportada e exponha o resultado ao caller.
  5. Execute um ciclo completo de duas solicitações com fixture sem efeitos colaterais. Não pare ao provar que uma solicitação retorna texto; execute a ferramenta e devolva o resultado.
  6. Depois teste paralelismo, streaming e erros. Só habilite ferramentas com efeitos colaterais e tráfego real depois que o caminho normal passar.
  7. Aumente o tráfego gradualmente e compare métricas. Monitore correção, latência, usage, erros e execuções duplicadas, não apenas a taxa de sucesso HTTP.

Como escolher o ponto de entrada correspondente no BetterToken

O BetterToken oferece caminhos de conexão diferentes para clientes diferentes. O protocolo continua sendo determinado pelo wire contract realmente usado pelo cliente:

  • Chat Completions: a URL completa é https://www.bettertoken.ai/v1/chat/completions. Para SDKs ou ferramentas que acrescentam o caminho automaticamente, Base URL normalmente é https://www.bettertoken.ai/v1. Consulte a referência da Chat Completions API.
  • Codex / Responses: a documentação atual do Codex usa base_url = "https://www.bettertoken.ai/v1" e wire_api = "responses"; o Codex acrescenta /responses. Consulte o guia do Codex.
  • Claude Code / Messages: a documentação atual usa ANTHROPIC_BASE_URL=https://bettertoken.ai, sem /v1 no final de Base URL; o cliente acrescenta /v1/messages. Consulte o guia do Claude Code.

Usar o mesmo Dashboard, API Key ou nome de modelo não transforma três protocolos em um único formato. Para uma ferramenta pronta, escolha o protocolo que ela espera. Para um agente próprio, valide as capacidades necessárias contra os ciclos completos e a matriz de aceitação deste artigo.

Perguntas frequentes

OpenAI-compatible significa uma réplica completa da OpenAI API?

Não. Normalmente significa que certos endpoints e estruturas de dados podem ser chamados por clientes no estilo OpenAI. Modelos, parâmetros, eventos de streaming, ferramentas, saída estruturada, ferramentas hospedadas e semântica de erros ainda precisam ser verificados individualmente.

Basta trocar Base URL e API Key?

Às vezes, para solicitações de texto simples que já usam o mesmo wire contract. Um agente com ferramentas ainda exige validação de definições, retorno do resultado na segunda solicitação, eventos de streaming, Schema estrito, estado e erros. Se o cliente espera Responses, apenas /chat/completions não é suficiente; se espera Messages, um endpoint no estilo OpenAI não se adapta sozinho.

Um adaptador universal consegue converter os três protocolos?

Ele pode cobrir texto comum e parte do ciclo de funções, mas não deve prometer suporte total sem perdas. Estado gerenciado pelo provedor, ferramentas hospedadas, estado opaco de thinking/reasoning, alguns escopos system e recursos específicos do modelo muitas vezes não têm equivalente universal. O adaptador deve expor uma matriz de capacidades e as informações de degradação.

Por que os testes unitários passam, mas o agente real falha?

Muitos testes simulam apenas a primeira resposta do modelo. Eles não validam o retorno do resultado na segunda solicitação, chamadas paralelas, fragmentos em streaming ou continuidade de estado. Amplie o teste para “solicitação do usuário → chamada da ferramenta pelo modelo → execução na aplicação → retorno do resultado → resposta final” para revelar falhas reais do protocolo.

É melhor migrar primeiro Chat Completions ou Responses?

Uma aplicação estável em Chat Completions pode continuar funcionando e migrar capacidade por capacidade conforme o valor de negócio. Um novo agente OpenAI, ou um que precise explicitamente de Items tipados, ferramentas hospedadas ou estado de Responses, é melhor começar diretamente em Responses. Os critérios são capacidade e custo de migração, não se o nome da interface parece mais novo.

Quer otimizar seu fluxo de trabalho com LLMs?

Conecte modelos por uma única API, gerencie chaves e controle os gastos com IA.

Começar grátis