spicyapiDocumentação
Texto e chat
Conteúdo principal

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 acima

MODEL_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.

ProtocoloEndpointCabeçalho da chave de API
OpenAI Chat CompletionsPOST /v1/chat/completionsAuthorization: Bearer
OpenAI ResponsesPOST /v1/responsesAuthorization: Bearer
Anthropic MessagesPOST /v1/messagesx-api-key ou Authorization: Bearer
Google GeminiPOST /v1beta/models/{model}:generateContentx-goog-api-key ou Authorization: Bearer
Google Gemini (streaming)POST /v1beta/models/{model}:streamGenerateContentx-goog-api-key ou Authorization: Bearer
Stream nativo com preço confirmadoPOST /api/v1/jobs/streamAuthorization: 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.

SDKURL 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.

OpenAI
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)
Anthropic
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)
Google GenAI
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/quote nativo e depois envie POST /api/v1/jobs/stream com o quoteId e o expectedCost retornados. Se o preço tiver mudado nesse meio-tempo, a aceitação falha com 40901 em 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 40202 em 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:

Idempotency-Key por chamada
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.

~/.grok/config.toml
[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ônicoChat CompletionsResponsesMessagesGemini
messagesmessagesinstructions, inputsystem, messagessystemInstruction, contents
max_tokensmax_tokens, max_completion_tokensmax_output_tokensmax_tokensgenerationConfig.maxOutputTokens
temperaturetemperaturetemperaturetemperaturegenerationConfig.temperature
top_ptop_ptop_ptop_pgenerationConfig.topP
toolstoolstools (tipo function)tools (ferramentas personalizadas)tools[].functionDeclarations
tool_choicetool_choicetool_choicetool_choicetoolConfig.functionCallingConfig
response_formatresponse_formattext.format—generationConfig.responseMimeType mais responseSchema ou responseJsonSchema
reasoning_effortreasoning_effortreasoning.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, metadata e store no Chat; user, metadata, store e include no Responses; metadata no Messages.
  • O Responses não suporta previous_response_id; envie o histórico completo em input.
  • O safetySettings do Gemini é aceito, mas não tem efeito. cachedContent e ferramentas do lado do servidor, como googleSearch e codeExecution, não são suportados e retornam 400. Passe imagens como inlineData (base64) ou fileData.fileUri (uma URL https); se um modelo aceita imagens é definido pelo inputSchema dele.

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.json

Se 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.json

O 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.json

Cada tipo de mídia tem a sua própria parte:

ParteFormatoDe 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 400 e 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 use audio_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.

ProtocoloParte 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
GeminiinlineData (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[].delta um evento SSE por vez, e não por pacote de rede. Monte os tool_calls pelo index, e só valide e execute uma chamada quando os arguments dela estiverem completos.
  • Mantenha a mensagem do assistente no histórico e devolva cada resultado numa mensagem tool com o tool_call_id correspondente.

Responses

  • Envie o histórico completo em input; previous_response_id é recusado.
  • Devolva os resultados de funções como itens function_call_output com o call_id correspondente.

Messages

  • Use blocos de conteúdo text, tool_use e tool_result em vez do formato de mensagem do Chat.

Gemini

  • Os contents alternam entre dois papéis, user e model.
  • Quando o modelo quer chamar uma ferramenta, ele retorna uma parte functionCall. Depois de rodar a ferramenta, coloque o resultado numa parte functionResponse e mande-o de volta no próximo conteúdo user, usando o mesmo name da chamada.

Conclusão, erros e desconexões

ProtocoloConclusão normalUso
Chat Completionsdata: [DONE]usage no último pedaço
ResponsesEvento response.completedresponse.usage nesse evento
Messagesmessage_stop depois de message_deltausage 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:

ProtocoloCorpo 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_tokens em 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/json e o cabeçalho X-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 200 já 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.code continua com o status HTTP que o erro teria. Quando X-Spicy-Keepalive estiver presente, verifique se o corpo traz um objeto error antes de ler a resposta: os SDKs oficiais não tratam um 200 como 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-alive no 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: true em 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 aceitam stream: true; no formato Gemini, chame :streamGenerateContent?alt=sse em vez de :generateContent. Um corpo diferente é uma requisição nova, então envie-a com um Idempotency-Key novo: reutilizar o antigo retorna 409.
  • 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/jobs como falha, com errorCode timeout e uma mensagem que explica o que aconteceu. Se você reenviá-la com o mesmo Idempotency-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-time para raciocínios longos.

Veja como ligar o streaming nos SDKs da OpenAI e no curl:

OpenAI Python
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()
OpenAI Node.js
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
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.json

O 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.

POST /v1/responses
{
  "model": "MODEL_ID_FROM_CATALOG",
  "input": "Explain a rainbow in one sentence.",
  "stream": false
}
POST /v1/messages
{
  "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

Nesta página