spicyapiDocumentação
Texto e chat
Conteúdo principal

Exemplos de chat

Programas completos para conversas, roleplay, streaming na sua interface e chamadas de ferramentas, em Python, Node.js e curl.

Cada exemplo desta página é um programa completo: defina duas variáveis de ambiente, copie e execute. Todos chamam POST https://api.spicyapi.ai/v1/chat/completions com as bibliotecas oficiais da OpenAI, portanto o mesmo código funciona com todos os modelos de texto do catálogo. Texto e streaming explica o protocolo; esta página mostra como criar os recursos mais comuns sobre ele.

Antes de começar

1. Coloque sua chave de API em uma variável de ambiente, para nunca precisar colá-la no código:

export SPICY_API_KEY="sk-spicy-paste-your-key-here"

2. Escolha um modelo de texto do catálogo em tempo real e guarde o ID em SPICY_MODEL. Todos os exemplos o leem. Este comando lista os modelos de texto que sua chave pode chamar agora e exige jq:

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'

export SPICY_MODEL="MODEL_ID_FROM_CATALOG"   # replace with one line from the list above

IDs de modelos de texto têm o formato publisher/model/chat. MODEL_ID_FROM_CATALOG é apenas um marcador: nunca o envie literalmente.

3. Confira o que o modelo aceita. Os modelos variam: alguns aceitam temperature, outros não; reasoning_effort tem valores diferentes conforme o modelo; somente alguns aceitam imagens. Campos de amostragem não aceitos, como temperature ou frequency_penalty, são ignorados e identificados no cabeçalho de resposta X-Spicy-Ignored-Params. Também tratamos valores padrão comuns dos clientes: um stop não aceito é ignorado, um n: 1 não aceito significa uma única resposta, e um nível de raciocínio reconhecido é mapeado para o nível aceito mais próximo quando necessário. O cabeçalho X-Spicy-Adjusted-Params mostra as configurações ajustadas. Campos funcionais não aceitos, como tools ou response_format, continuam retornando 400. O comando mostra os campos, os valores de reasoning_effort e os tipos de conteúdo de mensagem aceitos:

curl -s "https://api.spicyapi.ai/api/v1/models/$SPICY_MODEL" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  | jq '.data.inputSchema.properties | {
      fields: keys,
      reasoning_effort: .reasoning_effort.enum,
      content_parts: [.messages.items.properties.content.items.oneOf[]?.properties.type.const]
    }'

Para uma visão rápida de todos os modelos de texto, GET /v1/models?modality=text retorna supported_parameters, context_length e max_output_tokens de cada modelo. Ele lista somente nomes de campos; use o comando acima quando precisar dos valores permitidos.

4. Instale a biblioteca OpenAI da sua linguagem: pip install openai para Python ou npm install openai para Node.js 18 ou mais recente. Salve os programas Node.js com extensão .mjs, pois usam await no nível superior, e execute com node file.mjs.

Deixe espaço para o raciocínio

Muitos modelos de texto raciocinam antes de responder, e esse raciocínio conta em max_tokens. Os exemplos omitem max_tokens, permitindo que cada modelo use seu padrão. Se você o definir, deixe uma margem generosa, de alguns milhares de tokens: um valor muito baixo pode ser consumido inteiramente pelo raciocínio, fazendo a solicitação falhar com uma mensagem para aumentar max_tokens ou reduzir reasoning_effort. Essa falha não é cobrada.

Conversa com várias interações

A API não guarda memória entre chamadas. Uma conversa é uma lista de mensagens que você mantém e reenvia por inteiro a cada interação: primeiro a mensagem system, depois mensagens user e assistant alternadas. Após cada resposta, adicione-a à lista como mensagem assistant.

Cada mensagem reenviada é cobrada novamente como entrada; remova as interações mais antigas quando a conversa ficar longa. Sempre mantenha a mensagem system e remova as interações em pares de usuário e assistente, para que o histórico continue começando com uma mensagem user.

chat.py
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
KEEP = 20  # messages kept after the system prompt; an even number keeps user/assistant pairs together

history = [{"role": "system", "content": "You are a patient cooking coach. Keep every answer under 80 words."}]

while True:
    try:
        user_text = input("you> ").strip()
    except EOFError:  # Ctrl-D
        break
    if user_text in ("", "quit", "exit"):
        break
    history.append({"role": "user", "content": user_text})
    reply = client.chat.completions.create(model=MODEL, messages=history)
    answer = reply.choices[0].message.content or ""
    print(f"coach> {answer}\n")
    history.append({"role": "assistant", "content": answer})
    # The API remembers nothing between calls: resend the conversation every turn,
    # and trim it so the prompt (and what you pay for it) stops growing.
    history = history[:1] + history[1:][-KEEP:]

O modelo responde à última mensagem usando todas as anteriores: pergunte depois quais ingredientes você mencionou, e ele saberá. Modelos de raciocínio também retornam reasoning_content junto a content; para uma conversa comum, você não precisa reenviar esse raciocínio.

Roleplay com uma ficha de personagem

Um personagem é uma mensagem system escrita com cuidado. Como a API não guarda memória, a ficha acompanha todas as solicitações, o que também mantém o personagem consistente na quinquagésima interação. Uma boa ficha responde a seis perguntas:

ParteO que escreverExemplo
IdentidadeQuem é o personagem, em dois ou três fatos que orientam seu comportamentoUma detetive aposentada que agora cuida de um farol
VozComo fala: tamanho das frases, vocabulário, hábitos e forma de se dirigir ao usuárioFala ponderada, observa pequenos detalhes em voz alta e chama o usuário de “amigo”
MundoCenário e um ou dois fatos centrais da históriaUma vila de pescadores dos anos 1920, onde o sino de um navio afundado toca à meia-noite
LimitesO que o personagem faz ou não faz e como sair da históriaPermanece no cenário; responde a (OOC: ...) brevemente fora do personagem; mantém a história adequada ao público geral
NarraçãoPessoa e tempo verbal da narrativa e marcação das falasTerceira pessoa, passado; falas entre aspas
FormatoComprimento e forma de terminar cada respostaNo máximo 120 palavras, terminando com duas ou três opções numeradas

Dois hábitos deixam o roleplay mais natural. Coloque a fala de abertura do personagem no histórico como mensagem assistant, para que a primeira coisa que o usuário veja já esteja no personagem. Termine cada resposta com uma pergunta ou opção para que o usuário saiba como continuar; em ficção interativa, opções numeradas permitem responder com um único dígito.

Uma navegadora de dirigível em um mundo de fantasia. A resposta aparece gradualmente por streaming.

roleplay.py
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]

CARD = {
    "name": "Captain Ilsa Varn",
    "identity": "Navigator of the airship Kestrel. Forty years old, calm under pressure, dry sense of humour, fiercely loyal to her crew.",
    "voice": "Short, vivid sentences full of wind and weather. Calls the user 'deckhand'. No modern slang.",
    "world": "Islands drift on warm updrafts above an endless sea of cloud. Storms here are alive and hungry, and maps go out of date within a week.",
    "boundaries": (
        "Stay in character and inside this world. If the user asks about the real world, react as Ilsa would: puzzled but polite. "
        "If the user writes (OOC: ...), step out of the story for one short, plain answer, then return. "
        "Keep the story suitable for a general audience."
    ),
    "narration": "Describe the scene in the second person ('You feel the deck tilt...'). Put Ilsa's spoken lines in quotation marks.",
    "format": "At most 120 words. End every reply with a question or a choice, so the user always knows how to continue.",
}

system_prompt = "\n".join(
    [f"You are {CARD['name']}, a character in an interactive story."]
    + [f"{key.capitalize()}: {value}" for key, value in CARD.items() if key != "name"]
)
history = [
    {"role": "system", "content": system_prompt},
    {"role": "assistant", "content": "\"Welcome aboard, deckhand. Storm wall to the north, a lost island to the east. Which way do we steer?\""},
]
print(history[-1]["content"])

while True:
    try:
        user_text = input("\nyou> ").strip()
    except EOFError:  # Ctrl-D
        break
    if user_text in ("", "quit", "exit"):
        break
    history.append({"role": "user", "content": user_text})
    answer = ""
    stream = client.chat.completions.create(model=MODEL, messages=history, stream=True)
    for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            piece = chunk.choices[0].delta.content
            answer += piece
            print(piece, end="", flush=True)
    print()
    history.append({"role": "assistant", "content": answer})

As mesmas seis partes funcionam em qualquer gênero. O computador de uma nave de ficção científica, um companheiro que ajuda na rotina ou um narrador de aventura textual só precisam de outra ficha:

Another card: a daily companion
You are Juniper, a friendly running coach inside a fitness app.
Identity: a former trail runner, upbeat and practical, never judgemental.
Voice: warm and brief; uses the user's name when they give it; celebrates small wins.
World: the user's own week. Remember goals and aches they mention earlier in this conversation.
Boundaries: give general fitness encouragement only; for pain, injury or medical questions, suggest seeing a professional. Stay in character; answer (OOC: ...) plainly.
Narration: first person, present tense, talking directly to the user.
Format: at most 80 words; end with one concrete suggestion for today.

Em histórias longas, mantenha a ficha curta e estável. Quando o histórico crescer, substitua as interações mais antigas por uma breve nota de “história até agora” em uma segunda mensagem system, em vez de deixar o prompt crescer sem limite.

Streaming na sua própria interface

O streaming mostra a resposta à medida que é escrita, tornando a interface de chat mais ágil. Seis regras cobrem todos os modelos do catálogo:

  1. Solicite dados de uso se quiser contagens de tokens. Streams seguem as regras da OpenAI: sem stream_options.include_usage, nenhum bloco contém usage. Com "include_usage": true, um bloco extra chega imediatamente antes de [DONE], com o usage final e uma lista choices vazia; todos os demais têm usage: null.
  2. Confira choices antes de ler choices[0]. Todos os blocos, exceto o final de uso, têm pelo menos uma escolha. Isso só importa quando você solicita uso, mas a verificação não custa nada e mantém o laço seguro em ambos os casos.
  3. O raciocínio é separado. Modelos de raciocínio transmitem o raciocínio em delta.reasoning_content e a resposta em delta.content. Mostre o primeiro de forma discreta, recolhida ou não o mostre; só o segundo é a resposta.
  4. finish_reason: "length" significa interrupção por limite. O modelo atingiu max_tokens. Aumente-o ou reduza reasoning_effort.
  5. Erros podem chegar no meio do stream. Uma falha após o início do streaming chega como evento com objeto error, e as bibliotecas OpenAI lançam uma exceção. Essa solicitação com falha não é cobrada. Se o modelo já tinha concluído a resposta quando a conexão caiu, você não recebe erro: o stream termina normalmente, e a resposta é cobrada como qualquer outra. Detalhes: Conclusão, erros e desconexões.
  6. Interromper cedo não interrompe a cobrança. Fechar a conexão não interrompe o modelo; você é cobrado pelo uso informado por ele. Em vez disso, reduza max_tokens; veja Cobrança quando um stream termina cedo.

Um renderizador de terminal que segue as seis regras: raciocínio em tom discreto, resposta em cor normal e uma linha final com as contagens de tokens.

stream.py
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": "Write a four-line poem about a cat who lives in a lighthouse."}],
    stream=True,
    stream_options={"include_usage": True},
)

finish_reason = None
usage = None
thinking_shown = False
for chunk in stream:
    # With include_usage, only the last chunk before [DONE] carries usage; it has no choices.
    if chunk.usage:
        usage = chunk.usage
    if not chunk.choices:
        continue
    choice = chunk.choices[0]
    thinking = getattr(choice.delta, "reasoning_content", None)  # reasoning models only
    if thinking:
        thinking_shown = True
        print(f"\033[2m{thinking}\033[0m", end="", flush=True)  # dimmed
    if choice.delta.content:
        if thinking_shown:
            print("\n")
            thinking_shown = False
        print(choice.delta.content, end="", flush=True)
    if choice.finish_reason:
        finish_reason = choice.finish_reason
print()

if finish_reason == "length":
    print("[stopped at max_tokens: raise it, or lower reasoning_effort]")
if usage:
    print(f"[{usage.prompt_tokens} prompt tokens, {usage.completion_tokens} completion tokens]")

Chamadas de ferramentas

Ferramentas permitem que o modelo peça ao seu programa uma ação, como jogar dados, consultar um pedido ou ler um calendário, e depois responda com o resultado. O laço é sempre o mesmo:

  1. Envie as mensagens junto com tools, uma lista de funções descritas com JSON Schema.
  2. Se a resposta contiver tool_calls, adicione essa mensagem do assistente exatamente como recebida, execute cada chamada no seu código e adicione uma mensagem tool por chamada, com o tool_call_id correspondente.
  3. Reenvie tudo. Repita até que uma resposta não tenha tool_calls: essa é a resposta final.

Trate os argumentos como entrada não confiável: interprete e verifique os valores e devolva um erro como resultado da ferramenta, em vez de encerrar o programa inesperadamente. Limite a quantidade de rodadas para impedir um laço infinito quando o modelo se confundir. Todos os modelos de texto do catálogo atualmente aceitam tools; confira os valores de tool_choice antes de forçar uma ferramenta específica, pois alguns aceitam somente auto e none.

tools.py
import json
import os
import random
from openai import OpenAI

client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]


def roll_dice(count, sides):
    rolls = [random.randint(1, sides) for _ in range(count)]
    return {"rolls": rolls, "total": sum(rolls)}


LOCAL_TOOLS = {"roll_dice": roll_dice}
TOOLS = [{
    "type": "function",
    "function": {
        "name": "roll_dice",
        "description": "Roll dice for a tabletop game and return every roll and the total.",
        "parameters": {
            "type": "object",
            "properties": {
                "count": {"type": "integer", "minimum": 1, "maximum": 10, "description": "How many dice to roll"},
                "sides": {"type": "integer", "enum": [4, 6, 8, 10, 12, 20], "description": "Sides on each die"},
            },
            "required": ["count", "sides"],
        },
    },
}]

messages = [
    {"role": "system", "content": "You are the game master of a fantasy adventure. Whenever the rules call for a roll, use roll_dice; never invent a result. A roll of 12 or more on a d20 succeeds."},
    {"role": "user", "content": "I try to pick the lock on the old tower door. Roll a d20 for me."},
]

for _ in range(5):  # a hard stop, so a confused model cannot loop forever
    reply = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
    message = reply.choices[0].message
    # Send the assistant turn back exactly as it came, tool calls included.
    messages.append(message.model_dump(exclude_none=True))
    if not message.tool_calls:
        print(message.content)
        break
    for call in message.tool_calls:
        tool = LOCAL_TOOLS.get(call.function.name)
        try:
            result = tool(**json.loads(call.function.arguments or "{}")) if tool else {"error": "unknown tool"}
        except (TypeError, ValueError) as error:  # the model sent arguments the tool cannot use
            result = {"error": str(error)}
        print(f"[tool] {call.function.name}({call.function.arguments}) -> {result}")
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

A primeira resposta termina com finish_reason: "tool_calls" e content definido como null; a final termina com "stop". Modelos de raciocínio também podem incluir reasoning_content na mensagem do assistente: mantenha-o ao reenviar a mensagem, como os exemplos fazem ao adicioná-la sem alterações. Quando a resposta com chamada de ferramenta usa streaming, a chamada chega em partes; una-as por index, conforme Conversas e chamadas de ferramentas.

Saída JSON

Quando seu programa precisa de dados, não de texto livre, solicite JSON com response_format em modelos cujo inputSchema lista esse campo e descreva os campos no prompt. Sempre interprete a resposta com cuidado: uma resposta interrompida por max_tokens não é JSON válido.

json_output.py
import json
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": "system", "content": "Reply with a JSON object only, with the keys title (string), genre (string) and hook (one sentence)."},
        {"role": "user", "content": "Invent a cosy mystery novel set in a mountain bakery."},
    ],
    response_format={"type": "json_object"},
)
try:
    book = json.loads(reply.choices[0].message.content)
    print(book["title"], "-", book["hook"])
except (json.JSONDecodeError, KeyError, TypeError):
    print("Not the JSON we asked for:", reply.choices[0].message.content)

Em um modelo que não lista response_format, omita o campo e use somente o prompt de sistema; o bloco try acima importa ainda mais nesse caso.

Quando algo dá errado

Os erros usam o formato OpenAI, {"error": {"message", "type", "param", "code"}}, portanto as bibliotecas OpenAI lançam suas exceções habituais. Baseie a lógica no status HTTP e em error.code; mostre error.message às pessoas, pois informa o que mudar. Quando o erro se refere a um campo, param o identifica como caminho com pontos, por exemplo temperature ou messages.1.content.0.

Statuserror.codeO que aconteceuO que fazer
400unsupported_parameterO modelo não aceita o campo, como logprobs: is not a supported field, ou esse tipo de parte de mensagem, como image_url em um modelo somente de texto. Campos de amostragem como temperature não são recusados; são ignorados e indicados em X-Spicy-Ignored-ParamsRemova o campo. Confira os campos do modelo conforme Antes de começar
400invalid_requestUm campo está ausente ou tem um valor não aceito, como reasoning_effort: must be one of: "low", "high", "max"; ou o modelo não é de chat, com param igual a modelAltere o valor indicado ou escolha um modelo cujo ID termine em /chat
400content_rejectedA política de conteúdo do modelo recusou a solicitaçãoReformule; reenviar a mesma entrada provoca a mesma recusa
401invalid_api_keyA chave está ausente, foi digitada incorretamente ou foi revogadaConfira SPICY_API_KEY; chaves começam com sk-spicy-
402insufficient_balance, spend_cap_exceededO saldo ou um limite de gastos não cobre o valor reservado para a solicitaçãoAdicione saldo, aumente o limite ou reduza max_tokens; veja abaixo
404model_not_foundNenhum modelo tem esse ID, com param igual a model. A mensagem mostra o formato do ID e, quando existe, o modelo de chat disponível mais próximoCopie o ID exato do catálogo em tempo real
404not_foundO modelo existe, mas não pode ser chamado agoraEscolha outro modelo do catálogo em tempo real
409conflictUm Idempotency-Key foi reutilizado com outro corpo de solicitaçãoUse uma nova chave para cada nova mensagem; veja Chaves de idempotência em endpoints de texto
429rate_limit_exceededSolicitações demaisAguarde e tente novamente; o cabeçalho Retry-After, quando presente, informa quantos segundos esperar
503upstream_failed, model_unavailableO modelo falhou ou está temporariamente indisponível. Nada é cobradoTente mais tarde com outro Idempotency-Key

Por que uma solicitação pode falhar com 402 mesmo quando o saldo parece suficiente: antes da execução de uma solicitação de texto, é reservado um valor que cobre suas mensagens mais o max_tokens completo de saída, aos preços do modelo; quando omitido, usa-se o max_tokens padrão do modelo. Você só paga pelos tokens efetivamente usados, e o restante da reserva é liberado ao concluir, mas toda a reserva precisa caber no saldo disponível antes do início. Definir um max_tokens realista reduz a reserva. Veja Faturamento.

Consultar o custo de uma chamada

Toda solicitação aceita é executada como tarefa, e a resposta a identifica no cabeçalho X-Spicy-Task-Id, com ou sem streaming. Leia o cabeçalho e consulte a tarefa: cost é final quando settled é true. Uma solicitação recusada antes da aceitação, por erro de validação por exemplo, não cria tarefa nem tem esse cabeçalho.

task_id.py
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
raw = client.chat.completions.with_raw_response.create(
    model=os.environ["SPICY_MODEL"],
    messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
reply = raw.parse()  # the usual ChatCompletion object
print(reply.choices[0].message.content)
print("task:", raw.headers.get("x-spicy-task-id"))

Em streaming, o cabeçalho chega na resposta antes do primeiro bloco; with_raw_response em Python e .withResponse() em Node.js funcionam da mesma forma com stream: true. Os itens de GET /api/v1/jobs também contêm o requestId da chamada que os criou, igual ao cabeçalho de resposta X-Request-Id.

Continue lendo

Texto e streaming · Migrar de OpenAI ou OpenRouter · Catálogo de modelos e schemas · Faturamento · Erros

Nesta página