Exemplo de chamada de ferramentas da API Ollama com o SDK OpenAI
A chamada de ferramentas permite que o modelo peça ao seu código para executar uma função e, em seguida, use o resultado em sua resposta. Abaixo está um exemplo completo com o SDK OpenAI contra um endpoint hospedado, incluindo streaming e os erros que causam a maioria das falhas.
Atualizado

Pontos principais
- Envie sua função como um esquema JSON em tools; o modelo responde com tool_calls em vez de texto.
- Você executa a função, anexa o resultado como uma mensagem de role tool e chama a API novamente.
- Sempre analise os argumentos de forma defensiva, pois um modelo pode retornar JSON malformado ou inesperado.
- As chamadas de ferramentas em streaming chegam em fragmentos que você deve juntar por índice.
Como é o loop
Se você procura um exemplo de chamada de ferramentas da API Ollama que funcione contra um endpoint hospedado, o formato é o padrão da OpenAI. Você descreve suas funções em um array tools. O modelo ou responde em texto ou retorna uma lista tool_calls nomeando a função e seus argumentos. O modelo nunca executa nada. Seu código executa a função, envia a saída de volta e o modelo escreve a resposta final.
Isso resulta em uma ida e volta de uso de ferramentas equivalente a duas chamadas de API:
- Envie a mensagem do usuário e as definições de ferramentas.
- Receba uma mensagem do assistente com
tool_calls. Execute cada chamada localmente. - Envie as mensagens originais, a mensagem do assistente e uma mensagem
role: "tool"por chamada com seu resultado. - Receba a resposta final em texto.
Os exemplos usam a base URL https://api.ollamaapi.com/v1 e o ID do modelo uncensored. O endpoint é um serviço hospedado, então a chave vem da página da chave de API. O mesmo código também funciona contra um servidor Ollama local se você alterar a base URL, a chave e o nome do modelo, desde que o modelo local suporte ferramentas.
Um exemplo completo em Python
O exemplo fornece ao modelo uma função, get_order_status, apoiada por um pequeno dicionário. Um aplicativo real consultaria um banco de dados ou um serviço interno nesse ponto.
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.ollamaapi.com/v1", api_key="YOUR_KEY")
ORDERS = {"A1001": "shipped", "A1002": "processing", "A1003": "delivered"}
def get_order_status(order_id: str) -> str:
return ORDERS.get(order_id, "not found")
tools = [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up the status of an order by its ID.",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "Order ID such as A1001"}
},
"required": ["order_id"],
},
},
}]
messages = [{"role": "user", "content": "Where is order A1002?"}]
first = client.chat.completions.create(
model="uncensored",
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = first.choices[0].message
if msg.tool_calls:
messages.append(msg) # keep the assistant turn with its tool_calls
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = get_order_status(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
final = client.chat.completions.create(
model="uncensored",
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)
else:
print(msg.content)Três detalhes importam aqui. A mensagem do assistente que contém tool_calls deve voltar para o histórico antes dos resultados da ferramenta, ou a próxima requisição será rejeitada ou confundida. Cada mensagem de ferramenta deve conter o tool_call_id da chamada que ela responde. E a segunda requisição ainda deve incluir tools, porque o modelo pode querer chamar outra função após ver o primeiro resultado.
A mesma requisição com curl
Testar com curl remove o SDK da equação, o que ajuda quando você não tem certeza se um problema está no seu código ou na requisição.
curl https://api.ollamaapi.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "uncensored",
"messages": [{"role": "user", "content": "Where is order A1002?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up the status of an order by its ID.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]
}
}
}],
"tool_choice": "auto"
}'Uma primeira resposta bem-sucedida tem finish_reason definido como tool_calls, um content vazio ou nulo e um array tool_calls. O campo arguments é uma string contendo JSON, não um objeto aninhado, é por isso que o exemplo Python chama json.loads.
O campo tool_choice controla o quão forçada é a requisição. auto permite que o modelo decida. Passar um objeto que nomeia uma função pede essa função específica. Use auto a menos que você tenha um motivo, pois forçar uma chamada em uma mensagem que não precisa disso gera argumentos inventados.
O mesmo loop em Node
A versão Node usa o pacote oficial openai e segue os mesmos passos. Esta lida com mais de uma chamada por vez com Promise.all.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.ollamaapi.com/v1",
apiKey: process.env.API_KEY,
});
const handlers = {
get_order_status: async ({ order_id }) =>
({ A1001: "shipped", A1002: "processing" })[order_id] ?? "not found",
};
const tools = [{
type: "function",
function: {
name: "get_order_status",
description: "Look up the status of an order by its ID.",
parameters: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
},
},
}];
const messages = [{ role: "user", content: "Where is order A1001?" }];
const first = await client.chat.completions.create({
model: "uncensored", messages, tools,
});
const msg = first.choices[0].message;
if (msg.tool_calls?.length) {
messages.push(msg);
const results = await Promise.all(msg.tool_calls.map(async (call) => {
const args = JSON.parse(call.function.arguments);
const out = await handlers[call.function.name](args);
return { role: "tool", tool_call_id: call.id, content: String(out) };
}));
messages.push(...results);
const final = await client.chat.completions.create({
model: "uncensored", messages, tools,
});
console.log(final.choices[0].message.content);
} else {
console.log(msg.content);
}Chamadas de ferramentas em streaming
Com stream: true a resposta chega via SSE e as chamadas de ferramentas chegam em pedaços. Cada chunk carrega uma entrada delta.tool_calls com um index. O primeiro fragmento para um índice contém o ID da chamada e o nome da função. Fragmentos posteriores adicionam fatias da string arguments. Você concatena as fatias por índice e analisa o JSON apenas após o fim do stream.
stream = client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": "Where is order A1003?"}],
tools=tools,
stream=True,
)
calls = {}
for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
for part in (delta.tool_calls or []):
slot = calls.setdefault(part.index, {"id": "", "name": "", "args": ""})
if part.id:
slot["id"] = part.id
if part.function and part.function.name:
slot["name"] = part.function.name
if part.function and part.function.arguments:
slot["args"] += part.function.arguments
for slot in calls.values():
print(slot["name"], json.loads(slot["args"]))Lembre-se de que uma requisição em streaming mantém sua slot de concorrência até que o stream termine. Uma chave completa permite 8 requisições simultâneas e 300 por minuto; uma chave de teste permite 2 e 60. Se você executar loops de ferramentas para muitos usuários em paralelo, conte cada stream aberto contra o primeiro número.
Armadilhas e como corrigi-las
Argumentos malformados ou ausentes
O modelo é um modelo de pesos abertos e modelos abertos às vezes retornam argumentos que não são JSON válido, omitem um campo obrigatório ou inventam um extra. Envolva json.loads em um bloco try, valide contra seu schema e, quando falhar, envie o erro de volta como uma mensagem de ferramenta. O modelo geralmente se corrige na próxima vez. Limite o loop a algumas iterações para que uma falha teimosa não rode para sempre.
Descrições muito vagas
O modelo escolhe uma função lendo seu nome e descrição. Uma descrição como "obtém dados" não dá nada para ele trabalhar. Diga o que a função faz, o que os inputs significam e inclua um valor de exemplo na descrição do parâmetro.
Muitas ferramentas de uma vez
Cada definição de ferramenta conta como tokens de entrada em cada requisição. A $0,15 por 1M tokens de entrada, dez ferramentas de 150 tokens cada adicionam 1.500 tokens, o que é $0,000225 por requisição, pouco mas não zero em volume. Mais importante, um menu longo torna a seleção menos confiável. Ofereça apenas as ferramentas relevantes para a etapa atual.
Resultados de ferramentas muito grandes
Tudo o que você retorna é alimentado de volta como entrada. Reduza linhas grandes de banco de dados ou páginas web aos campos que o modelo precisa. A janela de contexto é de 100.000 tokens no total, então um resultado muito grande pode ocupar espaço na conversa.
Misturar ferramentas com modo JSON
O modo JSON (response_format com json_object) está disponível, mas use-o para a resposta final, não para voltas onde você quer que o modelo chame uma função. Teste a combinação em seus próprios prompts antes de confiar nela.
Migrando do Ollama local e o que verificar
Se você construiu seu primeiro loop de ferramentas contra um servidor Ollama local, quase nada muda. Substitua a base URL por https://api.ollamaapi.com/v1, use uma chave real e defina o modelo para uncensored. Rotas nativas do Ollama como /api/chat não existem aqui. Apenas POST /v1/chat/completions e GET /v1/models estão disponíveis, então o código escrito para a API nativa precisa da chamada no estilo OpenAI.
Antes de enviar, execute esta lista de verificação rápida:
- Confirme que a chave funciona com
GET /v1/models. - Registre o cabeçalho
X-Request-Idpara cada chamada, para que uma execução ruim possa ser rastreada. - Acompanhe
X-Balance-USDnos cabeçalhos de resposta se você executar loops de ferramentas sem supervisão. - Trate HTTP 429: aguarde
rate_limitedouconcurrencye honreRetry-After: 5em caso de sobrecarga. - Defina um limite rígido de iterações do loop.
Um loop de ferramentas faz pelo menos duas requisições por pergunta do usuário, então os custos escalam com isso. Por exemplo, 2.000 loops com 1.200 tokens de entrada e 150 tokens de saída por requisição, duas requisições cada, resultam em 4.000 requisições, 4,8M tokens de entrada e 0,6M tokens de saída. Isso dá $0,72 mais $0,30, totalizando $1,02. A página de preços tem as opções de recarregar, e a documentação lista os campos de requisição suportados. Novos usuários podem testar tudo isso com o crédito de teste de $0,50, que não precisa de cartão.
Mais guias
Perguntas e respostas
O endpoint suporta chamada de funções?
Sim. Ele aceita ferramentas e tool_choice, retorna tool_calls, faz streaming delas como deltas e recebe os resultados de volta como mensagens com role tool.
Por que a segunda requisição falha após uma chamada de ferramenta?
A causa mais comum é a ausência de uma mensagem do assistente ou de uma mensagem de ferramenta sem o tool_call_id correspondente. Adicione primeiro o turno do assistente que contém tool_calls e, em seguida, uma mensagem de ferramenta para cada chamada.
O modelo pode chamar várias ferramentas em um único turno?
Ele pode retornar uma lista com mais de uma entrada, então itere sobre todas elas e envie um resultado para cada uma. Não assuma que a lista tenha apenas um item.
Preciso da biblioteca Ollama para isso?
Não. Os SDKs oficiais da OpenAI são suficientes. Aponte a base URL para o endpoint e use o modelo uncensored.
Sua chave está a um formulário de distância
Crie uma conta, copie a chave e altere a URL base. Essa é toda a configuração.