Migrar da OpenAI para o Claude: o que muda no código
Se o seu app usa a API da OpenAI, trocar para o Claude Opus 5 pede poucas mudanças: outro SDK, o prompt de sistema num campo próprio, o max_tokens obrigatório e a resposta dividida em blocos.
Este guia mostra cada mudança com o código de antes e o de depois, em Python e em TypeScript. Os exemplos usam o SDK oficial da Anthropic, que fala o formato nativo da API do Claude e dá acesso a todos os recursos dela.
O que muda, em uma tabela
| Na OpenAI | No Claude |
|---|---|
pip install openai |
pip install anthropic |
OPENAI_API_KEY |
ANTHROPIC_API_KEY e ANTHROPIC_BASE_URL |
chat.completions.create() |
messages.create() |
Mensagem com "role": "system" |
Parâmetro system |
| Limite de saída opcional | max_tokens obrigatório |
choices[0].message.content |
Blocos em content: use os de type == "text" |
finish_reason |
stop_reason |
usage.prompt_tokens e completion_tokens |
usage.input_tokens e output_tokens |
stop |
stop_sequences |
temperature, top_p |
Não existem no Claude Opus 5: tire do código |
Ferramentas com function.parameters |
Ferramentas com input_schema |
Resultado de ferramenta com "role": "tool" |
Bloco tool_result numa mensagem user |
response_format com JSON Schema |
output_config.format com JSON Schema |
1. Instale o SDK e configure a chave
pip install anthropic
Para TypeScript e Node.js, npm install @anthropic-ai/sdk.
O SDK lê a URL base e a chave das variáveis de ambiente:
export ANTHROPIC_BASE_URL="https://api.open33.tech"
export ANTHROPIC_API_KEY="SUA_CHAVE"
No Windows, use $env:ANTHROPIC_BASE_URL = "..." no PowerShell. Os dois SDKs, o da OpenAI e o da Anthropic, podem ficar instalados juntos, o que permite migrar uma função de cada vez.
2. A chamada básica
Antes, com a OpenAI:
from openai import OpenAI
client = OpenAI()
resposta = client.chat.completions.create(
model="gpt-4o",
temperature=0.3,
messages=[
{"role": "system", "content": "Você é o suporte da loja. Responda em português, em até 3 frases."},
{"role": "user", "content": "Qual o prazo para trocar um produto?"},
],
)
print(resposta.choices[0].message.content)
Depois, com o Claude:
import anthropic
client = anthropic.Anthropic() # lê ANTHROPIC_BASE_URL e ANTHROPIC_API_KEY
resposta = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system="Você é o suporte da loja. Responda em português, em até 3 frases.",
messages=[
{"role": "user", "content": "Qual o prazo para trocar um produto?"},
],
)
texto = "".join(b.text for b in resposta.content if b.type == "text")
print(texto)
O que mudou:
- O prompt de sistema saiu da lista de mensagens e foi para o parâmetro
system. - O
max_tokensé obrigatório. No Claude Opus 5, ele limita o raciocínio e a resposta juntos. Um valor apertado corta a resposta no meio. - O
temperaturesaiu. O Claude Opus 5 recusatemperature,top_petop_kcom erro 400. Para mudar o estilo da resposta, escreva no prompt, como o "em até 3 frases" do exemplo. - A resposta vem em blocos. Por padrão, o Claude Opus 5 raciocina antes de responder, e a lista
contentpode trazer um bloco de raciocínio antes do texto. Por isso o código junta só os blocos detype == "text", em vez de lercontent[0].
Se o seu código usa a API Responses da OpenAI (client.responses.create), a troca é parecida: instructions vira system, input vira messages e output_text vira a junção dos blocos de texto.
3. O mesmo em TypeScript
Antes:
import OpenAI from "openai";
const client = new OpenAI();
const resposta = await client.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: "Você é o suporte da loja. Responda em português." },
{ role: "user", content: "Qual o prazo para trocar um produto?" },
],
});
console.log(resposta.choices[0].message.content);
Depois:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // lê ANTHROPIC_BASE_URL e ANTHROPIC_API_KEY
const resposta = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
system: "Você é o suporte da loja. Responda em português.",
messages: [{ role: "user", content: "Qual o prazo para trocar um produto?" }],
});
const texto = resposta.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("");
console.log(texto);
4. Históricos salvos no formato da OpenAI
Se você guarda conversas no banco no formato da OpenAI, com o sistema dentro da lista, não precisa converter tudo de uma vez. Esta função separa o sistema e devolve o resto no formato do Claude:
def para_o_claude(mensagens_openai: list[dict]) -> dict:
"""Converte uma conversa de texto no formato da OpenAI para os parâmetros do Claude."""
sistema = "\n\n".join(m["content"] for m in mensagens_openai if m["role"] == "system")
conversa = [
{"role": m["role"], "content": m["content"]}
for m in mensagens_openai
if m["role"] in ("user", "assistant")
]
parametros = {"messages": conversa}
if sistema:
parametros["system"] = sistema
return parametros
historico = [
{"role": "system", "content": "Você é o suporte da loja."},
{"role": "user", "content": "Oi, comprei um tênis ontem."},
{"role": "assistant", "content": "Oi! Como posso ajudar com o seu tênis?"},
{"role": "user", "content": "Posso trocar o tamanho?"},
]
resposta = client.messages.create(model="claude-opus-5", max_tokens=16000, **para_o_claude(historico))
A função cobre conversas de texto. Se o histórico tem chamadas de ferramenta (tool_calls e "role": "tool"), converta essas mensagens para blocos tool_use e tool_result, no formato da seção 6.
A API do Claude, como a da OpenAI, não guarda a conversa: você manda o histórico inteiro a cada chamada. A primeira mensagem da lista precisa ser do usuário.
5. Streaming
Antes:
stream = client.chat.completions.create(model="gpt-4o", messages=mensagens, stream=True)
for pedaco in stream:
print(pedaco.choices[0].delta.content or "", end="", flush=True)
Depois:
with client.messages.stream(
model="claude-opus-5",
max_tokens=16000,
system="Você é o suporte da loja.",
messages=mensagens,
) as stream:
for texto in stream.text_stream:
print(texto, end="", flush=True)
O text_stream entrega só o texto, sem os blocos de raciocínio. O guia de streaming na API do Claude mostra como mandar o texto para o navegador.
6. Ferramentas (function calling)
No Claude, function calling se chama tool use. A definição da ferramenta perde um nível e troca parameters por input_schema:
Antes:
ferramentas_openai = [{
"type": "function",
"function": {
"name": "consultar_pedido",
"description": "Consulta o status de um pedido pelo número.",
"parameters": {
"type": "object",
"properties": {"numero": {"type": "string"}},
"required": ["numero"],
},
},
}]
Depois:
ferramentas = [{
"name": "consultar_pedido",
"description": "Consulta o status de um pedido pelo número.",
"input_schema": {
"type": "object",
"properties": {"numero": {"type": "string"}},
"required": ["numero"],
},
}]
Se você tem muitas ferramentas, converta automaticamente:
def converter_ferramentas(ferramentas_openai: list[dict]) -> list[dict]:
return [
{
"name": f["function"]["name"],
"description": f["function"].get("description", ""),
"input_schema": f["function"].get("parameters", {"type": "object", "properties": {}}),
}
for f in ferramentas_openai
]
O loop também muda. No Claude, ele fica assim:
import json
def consultar_pedido(numero: str) -> dict:
return {"numero": numero, "status": "em transporte", "previsao": "2026-10-02"}
FUNCOES = {"consultar_pedido": consultar_pedido}
mensagens = [{"role": "user", "content": "Cadê o meu pedido 10231?"}]
while True:
resposta = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=ferramentas,
messages=mensagens,
)
if resposta.stop_reason != "tool_use":
break
mensagens.append({"role": "assistant", "content": resposta.content})
resultados = []
for bloco in resposta.content:
if bloco.type == "tool_use":
saida = FUNCOES[bloco.name](**bloco.input)
resultados.append({
"type": "tool_result",
"tool_use_id": bloco.id,
"content": json.dumps(saida, ensure_ascii=False),
})
mensagens.append({"role": "user", "content": resultados})
print("".join(b.text for b in resposta.content if b.type == "text"))
As diferenças que mais causam erro:
- Os argumentos já chegam prontos em
bloco.input, como dicionário. Não precisa dejson.loads, como nofunction.argumentsda OpenAI. - Devolva a resposta inteira do assistente (
resposta.content), com todos os blocos, e não só o texto. - Não existe o papel
tool. Todos os resultados voltam juntos numa única mensagemuser, em blocostool_result, cada um com otool_use_iddo pedido.
O exemplo completo, com tratamento de erros, está no guia de tool use na API do Claude.
7. Respostas em JSON
Antes:
response_format={
"type": "json_schema",
"json_schema": {"name": "pedido", "schema": esquema, "strict": True},
}
Depois:
output_config={"format": {"type": "json_schema", "schema": esquema}}
Com o output_config, o bloco de texto da resposta traz um JSON que segue o esquema. Todo objeto do esquema precisa de "additionalProperties": false. Se você usava o modo JSON simples ({"type": "json_object"}), escreva um esquema: é mais seguro. O passo a passo, com Pydantic e Zod, está no guia de respostas em JSON na API do Claude.
8. Erros e novas tentativas
Os nomes das exceções são os mesmos nos dois SDKs, só muda o pacote: openai.RateLimitError vira anthropic.RateLimitError, e o mesmo vale para AuthenticationError, BadRequestError, APIConnectionError e APIStatusError.
Os dois SDKs já tentam de novo sozinhos, duas vezes, em erros de conexão, 429 e 5xx. A API do Claude tem um erro a mais, o 529, quando a API está sobrecarregada. O guia de erros da API do Claude explica cada código.
Checklist da migração
- Trocar o SDK e as variáveis da chave.
- Mover o prompt de sistema para o parâmetro
system. - Definir o
max_tokenscom folga. - Tirar
temperatureetop_p, e os parâmetros que a API do Claude não tem, comofrequency_penalty,presence_penalty,neseed. - Ler a resposta pelos blocos de texto.
- Converter as ferramentas e o loop de chamadas.
- Trocar o
response_formatpelooutput_config. - Testar com conversas reais e ajustar os prompts que mudarem de comportamento.
Perguntas frequentes
Quanto trabalho dá migrar da OpenAI para o Claude?
Numa integração simples, as mudanças cabem em poucas linhas: o SDK, o system, o max_tokens e a leitura da resposta. Ferramentas e JSON pedem mais ajustes, mas a lógica é a mesma.
Preciso reescrever os meus prompts?
Na maioria dos casos, não. Os prompts funcionam como estão, mas vale testar com conversas reais e ajustar o que mudar de comportamento, como o tamanho das respostas.
Por que dá erro 400 com temperature?
O Claude Opus 5 não aceita temperature, top_p nem top_k. Tire esses parâmetros e controle o estilo da resposta pelo prompt.
O Claude tem function calling?
Tem, com o nome de tool use. A ferramenta usa input_schema no lugar de parameters, os argumentos chegam como dicionário e os resultados voltam em blocos tool_result.
Posso usar a OpenAI e o Claude no mesmo projeto?
Pode. Os dois SDKs convivem no mesmo projeto, cada um com a sua chave. Isso permite migrar aos poucos, uma função de cada vez, e comparar as respostas.