Texto e streaming
Chame modelos de texto no formato oficial da OpenAI, da Anthropic ou do Google Gemini e lide corretamente com conversas, ferramentas, streams e custos.
Modelos de texto respondem na mesma requisição HTTP: não há tarefa para consultar por polling. Você pode esperar a resposta inteira ou recebê-la por streaming — o texto chega em pedacinhos enquanto o modelo ainda está escrevendo, do jeito que um app de chat vai digitando uma resposta. Os pedaços transmitidos chegam como Server-Sent Events (SSE): linhas de texto simples que começam com data:, enviadas uma depois da outra por uma conexão que fica aberta até a resposta terminar.
Escolha um modelo de texto disponível
Os exemplos de shell desta página foram escritos para um terminal de macOS ou Linux e usam o jq, uma pequena ferramenta de linha de comando para JSON (brew install jq no macOS, sudo apt install jq no Debian ou no Ubuntu). No Windows, rode-os no WSL, ou use os exemplos com os SDKs para Python.
Primeiro, coloque a sua chave de API numa variável de ambiente — um valor com nome que o terminal entrega aos comandos que ele roda, para que a chave nunca precise ser colada num comando:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"Liste os modelos de texto que esta chave pode chamar agora:
curl -s "https://api.spicyapi.ai/api/v1/models?modality=text" \
-H "Authorization: Bearer $SPICY_API_KEY" \
| jq -r '.data.items[] | select(.enabled and .available) | .model'Deve aparecer um ID de modelo por linha. Copie um deles e guarde-o em SPICY_MODEL; todos os exemplos abaixo o leem dali:
export SPICY_MODEL="MODEL_ID_FROM_CATALOG" # troque por uma das linhas da lista acimaMODEL_ID_FROM_CATALOG é só um marcador — nunca o envie literalmente. Antes de contar com ferramentas, imagens, configurações de raciocínio ou saída estruturada, leia o inputSchema desse modelo em GET /api/v1/models/{model}: esses recursos só funcionam se o schema os listar. Ser compatível com um protocolo não dá a todo modelo todas as capacidades.
O mesmo ID de modelo funciona com qualquer um dos protocolos abaixo, não importa quem fez o modelo: você pode chamar o modelo de outro fabricante no formato da Anthropic, ou um modelo que não é do Google no formato do Gemini.
Protocolos suportados
Todos estes endpoints compartilham a mesma chave de API e o mesmo catálogo de modelos, além dos mesmos limites de requisições, da mesma validação de parâmetros e da mesma cobrança; só mudam os formatos de requisição e de resposta. Use aquele que o seu código ou SDK atual já fala.
| Protocolo | Endpoint | Cabeçalho da chave de API |
|---|---|---|
| OpenAI Chat Completions | POST /v1/chat/completions | Authorization: Bearer |
| OpenAI Responses | POST /v1/responses | Authorization: Bearer |
| Anthropic Messages | POST /v1/messages | x-api-key ou Authorization: Bearer |
| Google Gemini | POST /v1beta/models/{model}:generateContent | x-goog-api-key ou Authorization: Bearer |
| Google Gemini (streaming) | POST /v1beta/models/{model}:streamGenerateContent | x-goog-api-key ou Authorization: Bearer |
| Stream nativo com preço confirmado | POST /api/v1/jobs/stream | Authorization: Bearer |
Em /v1 e /v1beta, as respostas e os erros seguem os próprios protocolos e não trazem o envelope nativo {code,msg,data,request_id}, então não os interprete com um cliente que só lê body.data. O formato do Gemini coloca o ID do modelo no caminho da URL; as barras do ID ficam como estão e não precisam de codificação.
Envie a chave só num cabeçalho da requisição. x-api-key e x-goog-api-key só funcionam em /v1 e /v1beta; o /api/v1 nativo só aceita Authorization: Bearer. Um parâmetro de consulta ?key= nunca é aceito: quando uma chave vai parar numa URL, ela fica guardada no histórico do navegador, em servidores proxy e em logs de acesso. Todos os SDKs oficiais enviam a chave num cabeçalho, então não são afetados.
Use um SDK oficial
Mude só a URL base e a chave; todo o resto funciona do jeito normal de cada SDK.
| SDK | URL base |
|---|---|
OpenAI (openai) | https://api.spicyapi.ai/v1 |
Anthropic (anthropic) | https://api.spicyapi.ai |
Google GenAI (google-genai) | https://api.spicyapi.ai |
Os SDKs da Anthropic e do Google acrescentam /v1 ou /v1beta sozinhos, então a URL base deles não leva caminho de versão; a URL base do SDK da OpenAI precisa incluir /v1. Os três exemplos em Python abaixo leem SPICY_API_KEY do ambiente, junto com um SPICY_MODEL escolhido no catálogo atual.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
reply = client.chat.completions.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(reply.choices[0].message.content)import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai")
message = client.messages.create(
model=os.environ["SPICY_MODEL"],
max_tokens=256,
messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(message.content[0].text)import os
from google import genai
from google.genai import types
client = genai.Client(
api_key=os.environ["SPICY_API_KEY"],
http_options=types.HttpOptions(base_url="https://api.spicyapi.ai"),
)
response = client.models.generate_content(
model=os.environ["SPICY_MODEL"],
contents="Explain a rainbow in one sentence.",
)
print(response.text)A camada de compatibilidade cobra o preço vigente na aceitação e não aceita cotação
/v1 e /v1beta cobram o preço vigente no momento em que a requisição é aceita. Eles não aceitam quoteId nem expectedCost, então este caminho não tem teto por chamada — um ciclo de agente descontrolado continua gastando.
Há duas formas de pôr um limite, e a maioria das integrações quer as duas:
- Uma chamada de cada vez — peça a cotação com o
POST /api/v1/jobs/quotenativo e depois enviePOST /api/v1/jobs/streamcom oquoteIde oexpectedCostretornados. Se o preço tiver mudado nesse meio-tempo, a aceitação falha com40901em vez de cobrar mais do que você aprovou. Veja Stream nativo com preço confirmado. - Tudo o que esta chave gasta — defina os limites de gasto diário, mensal e acumulado da chave. Eles valem para todos os caminhos, inclusive este, e ultrapassá-los retorna
40202em vez de uma conta maior. Veja Autenticação.
Chaves de idempotência nos endpoints de texto
Um cabeçalho Idempotency-Key impede que uma nova tentativa por falha de rede cobre duas vezes. Nos endpoints de texto, ele não permite recuperar uma resposta perdida, porque a resposta é transmitida a você e não fica armazenada:
- Reenviar uma chave cuja requisição já terminou, com o mesmo corpo, retorna
400(This Idempotency-Key was already used for a completed request, and its answer cannot be replayed. Use a new Idempotency-Key to send a new request) e não cobra nada a mais. O registro da tarefa também não vai conter o texto. - Se aquela requisição anterior falhou, reenviar a mesma chave com o mesmo corpo retorna o mesmo erro de antes, e nada é cobrado.
- Reenviá-la com um corpo diferente retorna
409.
Então: envie uma chave nova a cada mensagem nova (por exemplo, $(uuidgen) no shell ou uuid.uuid4() em Python), e só reaproveite uma chave quando reenviar exatamente a mesma requisição depois de um erro de rede — sabendo que a resposta dessa requisição não pode ser recuperada. Se quiser uma resposta de novo, envie a requisição com uma chave nova e pague por ela.
Nos SDKs, passe a chave em cada chamada, nunca no objeto do cliente — com uma única chave para o cliente inteiro, toda mensagem depois da primeira seria recusada com 409 durante 24 horas:
import os, uuid
# SDK da OpenAI (client do exemplo da OpenAI acima). O SDK da Anthropic aceita
# o mesmo argumento extra_headers em client.messages.create().
reply = client.chat.completions.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
extra_headers={"Idempotency-Key": str(uuid.uuid4())},
)
# SDK do Google GenAI (client e types do exemplo do Google GenAI acima):
# http_options dentro do config de cada chamada
response = client.models.generate_content(
model=os.environ["SPICY_MODEL"],
contents="Explain a rainbow in one sentence.",
config=types.GenerateContentConfig(
http_options=types.HttpOptions(headers={"Idempotency-Key": str(uuid.uuid4())}),
),
)Os SDKs da OpenAI e da Anthropic repetem sozinhos as requisições que estouram o tempo limite, com os mesmos cabeçalhos. Se uma dessas repetições chegar a uma requisição que já tínhamos aceitado, o SDK lança um erro 400 de requisição inválida com a mensagem acima: a primeira tentativa foi processada e cobrada, e a resposta dela se perdeu.
Use modelos da SpicyAPI no cliente de linha de comando do Grok
Isto configura o cliente de linha de comando da xAI para enviar as requisições dele à SpicyAPI: daí em diante ele conversa com os modelos do catálogo da SpicyAPI, e não com o endpoint da própria xAI. Entre eles está a família de modelos Grok publicada no catálogo: você a chama pelo ID de modelo na SpicyAPI, como qualquer outro modelo. O cliente pode apontar um modelo personalizado para qualquer endpoint compatível com OpenAI. Adicione a entrada abaixo a ~/.grok/config.toml; env_key indica o nome da variável de ambiente que o cliente lê, então a chave em si nunca fica no arquivo.
[model.spicyapi]
model = "MODEL_ID_FROM_CATALOG"
base_url = "https://api.spicyapi.ai/v1"
name = "SpicyAPI"
env_key = "SPICY_API_KEY"
[models]
default = "spicyapi"Troque MODEL_ID_FROM_CATALOG por um ID exato de modelo de texto do catálogo atual, no formato <publisher>/<model>/<task>; nunca deixe o marcador no lugar. Depois, inicie com grok -m spicyapi, ou troque dentro de uma sessão com /model spicyapi. O cliente fala Chat Completions, então o mapeamento de parâmetros, os finais de stream e os formatos de erro descritos abaixo valem sem mudança.
Mapeamento de parâmetros entre protocolos
Os campos de cada protocolo são primeiro convertidos para os nomes de parâmetro canônicos da plataforma e depois validados contra o inputSchema do modelo escolhido. Um parâmetro que o modelo não suporta retorna 400; ele nunca é descartado em silêncio enquanto a requisição é cobrada normalmente. A tabela abaixo mostra só o mapeamento, e "—" quer dizer que o protocolo não tem campo equivalente. Se um modelo aceita um parâmetro, e quais valores permite, é definido pelo inputSchema desse modelo.
| Parâmetro canônico | Chat Completions | Responses | Messages | Gemini |
|---|---|---|---|---|
messages | messages | instructions, input | system, messages | systemInstruction, contents |
max_tokens | max_tokens, max_completion_tokens | max_output_tokens | max_tokens | generationConfig.maxOutputTokens |
temperature | temperature | temperature | temperature | generationConfig.temperature |
top_p | top_p | top_p | top_p | generationConfig.topP |
tools | tools | tools (tipo function) | tools (ferramentas personalizadas) | tools[].functionDeclarations |
tool_choice | tool_choice | tool_choice | tool_choice | toolConfig.functionCallingConfig |
response_format | response_format | text.format | — | generationConfig.responseMimeType mais responseSchema ou responseJsonSchema |
reasoning_effort | reasoning_effort | reasoning.effort | — | generationConfig.thinkingConfig |
Os campos que não estão na tabela também são convertidos em parâmetros canônicos e validados pelo modelo; tudo o que faltar no inputSchema dele retorna 400. Por exemplo, só alguns modelos listam seed no inputSchema; nos outros, o seed do Chat e o generationConfig.seed do Gemini retornam 400. Configurações de amostragem e sequências de parada são a exceção: em um modelo que não as aceita, elas são ignoradas (citadas no cabeçalho de resposta X-Spicy-Ignored-Params), e um valor fora do que o modelo aceita é trocado pelo valor aceito mais próximo (citado em X-Spicy-Adjusted-Params). Isso vale para temperature, top_p e as penalidades, o stop do Chat, o stop_sequences do Messages, o generationConfig.stopSequences do Gemini e os níveis de reasoning_effort. Leia o schema do modelo que você escolheu em vez de presumir um campo de outro modelo.
Algumas exceções:
- Campos que só fazem sentido na plataforma dona do protocolo são ignorados e não afetam a geração:
user,metadataestoreno Chat;user,metadata,storeeincludeno Responses;metadatano Messages. - O Responses não suporta
previous_response_id; envie o histórico completo eminput. - O
safetySettingsdo Gemini é aceito, mas não tem efeito.cachedContente ferramentas do lado do servidor, comogoogleSearchecodeExecution, não são suportados e retornam400. Passe imagens comoinlineData(base64) oufileData.fileUri(uma URL https); se um modelo aceita imagens é definido peloinputSchemadele.
Envie um stream do Chat
Com SPICY_API_KEY e SPICY_MODEL definidas como acima, este exemplo grava a requisição num arquivo e transmite a resposta. Troque o prompt de exemplo como precisar e valide os parâmetros do modelo contra o schema atual antes de enviar. Cada execução manda um Idempotency-Key novo, porque cada execução é uma mensagem nova.
jq -n --arg model "$SPICY_MODEL" '{
model: $model,
messages: [{role: "user", content: "Explain a rainbow in one sentence."}],
stream: true
}' > chat-request.json
curl --fail-with-body --no-buffer --max-time 120 \
https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @chat-request.json--no-buffer mostra cada pedaço assim que ele chega, e --max-time 120 desiste depois de dois minutos. Devem aparecer linhas como as de baixo, chegando uma a uma, e no final data: [DONE]. A resposta vem espalhada pelos pedaços de delta.content; junte-os na ordem para ter o texto completo. O último pedaço antes de [DONE] traz usage.
data: {"id":"chatcmpl-req_01k...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"A rainbow"}}]}
data: {"id":"chatcmpl-req_01k...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":" forms when"}}]}
data: [DONE]O uuidgen vem com o macOS; no Linux, ele faz parte do pacote uuid-runtime.
Envie uma requisição do Gemini
Este exemplo usa as mesmas SPICY_API_KEY e SPICY_MODEL e, de novo, um Idempotency-Key novo: é uma requisição diferente da do exemplo do Chat, então reaproveitar aquela chave retornaria 409. O ID do modelo vai na URL, então o corpo da requisição não tem campo de modelo. Coloque a URL entre aspas duplas e escreva a variável como ${SPICY_MODEL}, para que o shell não leia os dois-pontos logo depois dela como um modificador de variável.
jq -n '{
systemInstruction: {parts: [{text: "Answer in one sentence."}]},
contents: [{role: "user", parts: [{text: "Explain a rainbow."}]}],
generationConfig: {temperature: 0.7, maxOutputTokens: 256}
}' > gemini-request.json
curl --fail-with-body --no-buffer --max-time 120 \
"https://api.spicyapi.ai/v1beta/models/${SPICY_MODEL}:streamGenerateContent?alt=sse" \
-H "x-goog-api-key: $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @gemini-request.jsonSe você não precisa de streaming, troque :streamGenerateContent?alt=sse por :generateContent e leia a resposta em candidates[0].content.parts[].text; o uso fica em usageMetadata. Sem alt=sse, uma requisição de streaming retorna um único array JSON escrito aos poucos; os SDKs oficiais acrescentam alt=sse por você.
Stream nativo com preço confirmado
POST /api/v1/jobs/stream é o único endpoint de texto que aceita cotação, então ele pode se recusar a rodar se o preço tiver mudado. O corpo da requisição tem o mesmo formato do createTask: model mais um input cujos campos vêm do inputSchema do modelo (para modelos de texto, messages e os outros parâmetros canônicos da tabela acima).
# 1. Escreva a requisição uma única vez.
jq -n --arg model "$SPICY_MODEL" '{
model: $model,
input: {messages: [{role: "user", content: "Explain a rainbow in one sentence."}]}
}' > native-request.json
# 2. Peça a cotação. Isto não cria nada e não cobra nada.
QUOTE=$(curl -s https://api.spicyapi.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @native-request.json)
echo "$QUOTE" | jq '{code, msg, estimatedCost: .data.estimatedCost, maxCharge: .data.maxCharge, expiresAt: .data.expiresAt}'
# 3. Em até cinco minutos, acrescente quoteId e expectedCost à requisição sem alterações e transmita.
jq --argjson quote "$QUOTE" \
'. + {quoteId: $quote.data.quoteId, expectedCost: $quote.data.estimatedCost}' \
native-request.json > native-confirmed.json
curl --fail-with-body --no-buffer --max-time 120 \
https://api.spicyapi.ai/api/v1/jobs/stream \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @native-confirmed.jsonO passo 2 mostra code: 200 e o preço em dólares americanos; qualquer outro code quer dizer que a cotação falhou, e o msg diz por quê. Se a chamada de stream falhar antes de qualquer texto ser enviado — por exemplo, com 40901 porque o preço ou a requisição mudou, ou com 40201 por saldo insuficiente —, você recebe o JSON normal {code,msg,data,request_id} e nada fica retido. Depois que o streaming começou, uma falha encerra o stream com event: error e data: {"code":"stream_interrupted"}, e nada é cobrado por ela. Modelos de imagem, vídeo e áudio retornam 503 aqui; para eles, use createTask.
A resposta chega como linhas SSE data:. Diferente de /v1/chat/completions, este endpoint não promete um único formato de evento para todos os modelos, então teste o seu parser com o modelo que você escolheu; se precisar de um formato único e estável, use um endpoint /v1 e limite os gastos com os limites da chave.
Envie imagens, vídeo e áudio a um modelo de texto
Alguns modelos de texto leem imagens além de texto, e poucos também leem vídeo e áudio. Você envia a mídia junto com uma pergunta e a resposta continua sendo texto. O que um modelo aceita está escrito no esquema dele; o protocolo não pressupõe isso. Confira o modelo que você escolheu:
curl -s "https://api.spicyapi.ai/api/v1/models/$SPICY_MODEL" \
-H "Authorization: Bearer $SPICY_API_KEY" \
| jq -c '[.data.inputSchema.properties.messages.items.properties.content.items.oneOf[]?.properties.type.const]'A lista nomeia as partes de conteúdo que o modelo aceita. ["text","image_url"] significa texto e imagens; um modelo que também lê vídeo e áudio lista ainda video_url, audio_url e input_audio. Uma lista vazia significa somente texto. Enviar uma parte que o modelo não lista retorna 400 e não cobra nada.
Para enviar mídia, faça o content da mensagem ser uma lista de partes em vez de uma string: uma parte text para a pergunta e uma parte para cada arquivo:
jq -n --arg model "$SPICY_MODEL" '{
model: $model,
stream: true,
messages: [{
role: "user",
content: [
{type: "text", text: "What is in this picture?"},
{type: "image_url", image_url: {url: "https://example.com/photo.jpg"}}
]
}]
}' > image-request.json
curl --fail-with-body --no-buffer --max-time 120 \
https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @image-request.jsonCada tipo de mídia tem a sua própria parte:
| Parte | Formato | De onde vem o arquivo |
|---|---|---|
| Imagem | {"type": "image_url", "image_url": {"url": "…"}} | Uma URL HTTPS pública, um arquivo enviado (spicy://f/…) ou um URI data: pequeno |
| Vídeo | {"type": "video_url", "video_url": {"url": "…"}} | Um arquivo enviado (spicy://f/…) ou uma URL HTTPS pública |
| Arquivo de áudio | {"type": "audio_url", "audio_url": {"url": "spicy://f/…"}} | Somente um arquivo enviado |
| Áudio inline | {"type": "input_audio", "input_audio": {"data": "<base64>", "format": "wav"}} | Base64 na requisição, wav ou mp3 |
Como enviar um arquivo e obter o URI spicy://f/… dele está descrito em Mídia; os uploads duram um dia. Algumas regras decorrem de como cada tipo chega ao modelo:
- Imagens podem ser uma URL HTTPS pública que devolva o próprio arquivo de imagem (não uma página que o exiba), um arquivo enviado ou um URI
data:dentro dos limites inline. JPEG e PNG funcionam em todos os modelos que aceitam imagens. - Vídeo e áudio funcionam melhor como uploads. Lemos o arquivo enviado e o entregamos ao modelo dentro da requisição, então o vídeo e o áudio enviados em uma mesma requisição podem somar no máximo 14 MB: vídeo MP4 ou WebM, áudio MP3 ou WAV. Um total maior retorna
400e não cobra nada. Uma URL pública de vídeo é repassada como link e só funciona se o provedor do modelo conseguir baixá-la. - Áudio inline (
input_audio) segue o formato da OpenAI e precisa caber no corpo de requisição de 2 MiB, por isso serve para clipes curtos; envie qualquer áudio mais longo por upload e useaudio_url.
Dá para enviar vários arquivos em uma mesma mensagem, cada um como uma parte própria, e as mensagens anteriores do histórico também podem trazer mídia.
Os outros protocolos carregam as imagens no seu próprio formato, e todos chegam ao modelo de forma idêntica. As partes de vídeo e áudio são enviadas pelo Chat Completions e pelo fluxo nativo.
| Protocolo | Parte de imagem |
|---|---|
| Chat Completions | {"type": "image_url", "image_url": {"url": "…"}} |
| Responses | {"type": "input_image", "image_url": "…"} |
| Anthropic Messages | {"type": "image", "source": {"type": "url", "url": "…"}}, ou uma fonte base64 |
| Gemini | inlineData (base64) ou fileData.fileUri (uma URL HTTPS) |
Quanto custa. A mídia é contada como tokens de entrada, pela tarifa de entrada normal do modelo; não existe preço separado por arquivo. Uma imagem costuma somar de algumas centenas a alguns milhares de tokens, conforme o modelo e o tamanho da imagem; vídeo e áudio crescem com a duração. O bloco usage da resposta informa o número exato.
O que fica retido de antemão. Antes de executar uma requisição, retemos um limite superior e liberamos a diferença quando ela é liquidada. Em uma requisição cuja única mídia são imagens que você enviou por upload ou inline, o limite soma uma margem fixa por imagem. Em uma que traz vídeo ou áudio, ou uma imagem informada como URL pública, o limite é dimensionado pelo limite de entrada inteiro do modelo, porque o tamanho não pode ser conhecido de antemão; por isso o valor retido pode ficar bem acima da cobrança final. Envie as imagens por upload para manter a retenção pequena, mantenha saldo suficiente para ela, ou cote a requisição antes com POST /api/v1/jobs/quote para vê-la.
No Playground. Em um modelo que aceita mídia, a caixa de mensagem mostra um botão de anexar. Você também pode colar uma captura de tela ou soltar arquivos na caixa; cada mensagem aceita até quatro anexos, e o vídeo e o áudio entre eles podem somar 14 MB. Modelos que leem apenas texto não mostram o botão.
Conversas e chamadas de ferramentas
Chat Completions
- Sem streaming: leia
choices[0].message. - Com streaming: processe
choices[].deltaum evento SSE por vez, e não por pacote de rede. Monte ostool_callspeloindex, e só valide e execute uma chamada quando osargumentsdela estiverem completos. - Mantenha a mensagem do assistente no histórico e devolva cada resultado numa mensagem
toolcom otool_call_idcorrespondente.
Responses
- Envie o histórico completo em
input;previous_response_idé recusado. - Devolva os resultados de funções como itens
function_call_outputcom ocall_idcorrespondente.
Messages
- Use blocos de conteúdo
text,tool_useetool_resultem vez do formato de mensagem do Chat.
Gemini
- Os
contentsalternam entre dois papéis,useremodel. - Quando o modelo quer chamar uma ferramenta, ele retorna uma parte
functionCall. Depois de rodar a ferramenta, coloque o resultado numa partefunctionResponsee mande-o de volta no próximo conteúdouser, usando o mesmonameda chamada.
Conclusão, erros e desconexões
| Protocolo | Conclusão normal | Uso |
|---|---|---|
| Chat Completions | data: [DONE] | usage no último pedaço |
| Responses | Evento response.completed | response.usage nesse evento |
| Messages | message_stop depois de message_delta | usage em message_delta |
| Gemini | Último evento com finishReason; não existe [DONE] | usageMetadata |
Confira o status HTTP e o Content-Type antes de ler um stream; os erros voltam no JSON próprio de cada protocolo:
| Protocolo | Corpo do erro |
|---|---|
| Chat Completions, Responses | {"error": {"message", "type", "param", "code"}} |
| Messages | {"type": "error", "error": {"type", "message"}} |
| Gemini | {"error": {"code", "message", "status"}} |
Os códigos de status HTTP são os mesmos nos quatro protocolos. O status do Gemini usa os nomes de status do Google: 400 é INVALID_ARGUMENT, 401 é UNAUTHENTICATED, 402 é FAILED_PRECONDITION, 403 é PERMISSION_DENIED, 404 é NOT_FOUND, 409 é ABORTED, 429 é RESOURCE_EXHAUSTED, 500 é INTERNAL, 503 é UNAVAILABLE e 504 é DEADLINE_EXCEEDED. Faça o seu código decidir pelo código de status e pelo tipo de erro, nunca pelo texto da mensagem.
Se algo falhar depois que o streaming começou, o Chat e o Gemini enviam um evento com um objeto de erro, o Responses envia um evento de erro e o Messages envia event: error. O seu cliente precisa tratar esses eventos de erro, assim como tempos limite estourados e uma conexão que se fecha sem o marcador normal de conclusão. Se o stream passar por um proxy seu, desligue o buffer de resposta nele; caso contrário, os pedaços chegam em rajadas.
O que você paga quando um stream para antes da hora
- Você se desconecta (fecha a aba, aborta a requisição, estoura o tempo limite do cliente): desconectar não interrompe a cobrança. Você é cobrado pelo uso que o modelo informa, que em muitos modelos cobre a resposta inteira — inclusive o texto que você nunca recebeu —, e nunca mais do que o valor reservado quando a requisição foi aceita. O texto que você não recebeu não pode ser buscado depois. Para gastar menos, diminua
max_tokensem vez de se desconectar. - O modelo ou o nosso serviço falha no meio do stream (você recebe um evento de erro, ou o stream termina sem o marcador de conclusão): a requisição é marcada como falha e nada é cobrado.
- O modelo enviou texto ou raciocínio a você e depois a resposta dele foi considerada vazia ou recusada: o que você já recebeu é cobrado pelo uso que o modelo informa, nunca acima do valor reservado, e o stream termina normalmente. Só quando nada chegou até você o valor é devolvido por inteiro.
- A requisição passa do prazo do servidor: ela termina como falha e nada é cobrado.
Tempos limite e streaming
Sem streaming, a resposta chega de uma vez quando fica pronta, e respostas longas e raciocínios longos levam tempo: modelos como Grok, GLM, DeepSeek Pro e Claude Opus podem pensar por minutos antes de escrever qualquer coisa, principalmente com um reasoning_effort alto. A borda de rede na frente da API fecha uma conexão pela qual nada chegou por cerca de 100 segundos, então requisições dos dois tipos são mantidas abertas enquanto o modelo trabalha.
- Uma requisição sem streaming que ainda está em andamento depois de uns 30 segundos começa a resposta mais cedo. Você recebe o status
200,Content-Type: application/jsone o cabeçalhoX-Spicy-Keepalive: 1, depois uma quebra de linha a cada 15 segundos e, por fim, o corpo JSON. JSON aceita espaços em branco antes de um valor, então parsers padrão (json.loads,JSON.parse,response.json()) e os SDKs oficiais leem o corpo sem mudança. Uma requisição que termina antes recebe uma resposta normal, com o código de status real e sem esse cabeçalho. - A partir daí, uma falha vem no corpo, não no status. O
200já foi enviado, então o corpo traz o objeto de erro habitual do protocolo (o da tabela acima), com o mesmo código e a mesma mensagem; no formato Gemini,error.codecontinua com o status HTTP que o erro teria. QuandoX-Spicy-Keepaliveestiver presente, verifique se o corpo traz um objetoerrorantes de ler a resposta: os SDKs oficiais não tratam um200como erro. Uma requisição com falha não é cobrada. - O streaming também mantém a conexão aberta. Enquanto o modelo ainda está trabalhando, uma resposta em streaming traz linhas de keep-alive (comentários
: keep-aliveno SSE, linhas em branco no formato Gemini), então a conexão nunca fica ociosa, não importa quanto tempo o modelo pense. Seu parser precisa ignorá-las, como o formato SSE exige; os SDKs oficiais já fazem isso. - Prefira
stream: trueem tudo o que puder demorar. Você vê a resposta enquanto ela é escrita, e uma falha chega como um evento de erro que os SDKs entendem. Chat Completions, Responses e Messages aceitamstream: true; no formato Gemini, chame:streamGenerateContent?alt=sseem vez de:generateContent. Um corpo diferente é uma requisição nova, então envie-a com umIdempotency-Keynovo: reutilizar o antigo retorna409. - Uma requisição cortada pela borda de rede não é cobrada. Se nada chegar até você por cerca de 100 segundos e a conexão for encerrada, a requisição aparece em Console → Registros e em
GET /api/v1/jobscomo falha, comerrorCodetimeoute uma mensagem que explica o que aconteceu. Se você reenviá-la com o mesmoIdempotency-Key, recebe essa mensagem como erro. - Seus próprios limites de tempo continuam valendo. Um tempo limite total no seu cliente HTTP, ou em um proxy que você mantém, também encerra uma requisição, e depois que qualquer coisa chegou até você (inclusive uma linha de keep-alive), isso conta como desconexão da sua parte (veja O que você paga quando um stream para antes da hora). Os SDKs oficiais da OpenAI esperam até 10 minutos por padrão; os exemplos de curl desta página desistem depois de 2 minutos, então aumente
--max-timepara raciocínios longos.
Veja como ligar o streaming nos SDKs da OpenAI e no curl:
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
stream = client.chat.completions.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.SPICY_API_KEY, baseURL: "https://api.spicyapi.ai/v1" });
const stream = await client.chat.completions.create({
model: process.env.SPICY_MODEL,
messages: [{ role: "user", content: "Explain a rainbow in one sentence." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
process.stdout.write("\n");curl --fail-with-body --no-buffer --max-time 900 \
https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @chat-request.jsonO exemplo em Node.js usa await no nível superior, então execute-o como módulo ES (por exemplo, salvando-o como stream.mjs). O comando curl envia o chat-request.json de Envie um stream do Chat, que já define stream: true; com --max-time 900, a resposta pode levar até 15 minutos.
Cotações e cobrança
Para confirmar o custo explicitamente, use o stream nativo com uma cotação. /v1 e /v1beta não aceitam as credenciais da cotação; eles aplicam o preço vigente na aceitação.
Chamadas de texto não retornam ID de tarefa, e um marcador de fim de texto não prova qual foi a cobrança final. Para saber quanto uma chamada custou, liste as tarefas recentes com a mesma chave de API — GET /api/v1/jobs?model=<model>&from=<YYYY-MM-DD> — e encontre a certa pelo createdAt, ou procure em Console → Registros. Só considere cost definitivo quando settled for true. Veja Tarefas assíncronas e Cobrança.
reasoning_tokens já está incluído em completion_tokens: não o some duas vezes.
Outros formatos de requisição
Estes exemplos descrevem só os envelopes dos protocolos. Troque MODEL_ID_FROM_CATALOG por um modelo atual que suporte os campos mostrados; nunca envie o próprio marcador.
{
"model": "MODEL_ID_FROM_CATALOG",
"input": "Explain a rainbow in one sentence.",
"stream": false
}{
"model": "MODEL_ID_FROM_CATALOG",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Explain a rainbow in one sentence."}],
"stream": false
}Continue lendo
Cotações e compatibilidade de protocolos · Idempotência · Cobrança · Integração em produção · Solução de problemas e recuperação
Cotações e compatibilidade de protocolos
Descubra o preço exato de uma requisição antes de pagar, envie-a por esse preço, recupere-se de uma resposta perdida e entenda como são cobradas as APIs compatíveis de texto e vídeo.
Exemplos de chat
Programas completos para conversas, roleplay, streaming na sua interface e chamadas de ferramentas, em Python, Node.js e curl.

