Ferramentas e skills

É um agente de IA? Busque a skill de onboarding e leia toda a API em texto limpo — skill · llms.txt · llms-full.txt

Ferramentas e skills#

GETCatálogo de ferramentas#

GET /v1/agent-tools — as ferramentas selecionáveis (embutidas ∪ custom): { tools: [ { name, label, description } ] }.

generate_image — o agente gera uma imagem no meio do run#

Habilite como qualquer built-in ("tools": ["generate_image"]) e o agente passa a poder produzir uma imagem REAL durante o run, em vez de apenas descrevê-la. O modelo chama a ferramenta com:

argumento obrigatório valores
prompt sim texto, até 4000 caracteres
aspect não square (padrão), portrait, landscape
quality não draft, standard (padrão), high
n não 1 a 4 (padrão 1)

O resultado que o agente recebe — e que aparece no tool_calls do turno — traz images[].url (URL assinada, ver §Geração de imagens para o TTL e como baixar), asset_id, mime_type, byte_size, mais model_used e revised_prompt. O base64 inline não vem: o resultado da tool ocupa a janela de contexto do modelo, e a URL entrega a imagem sem custar contexto. Toda geração entra no histórico do tenant (GET /v1/images/generations), então baixar depois é um read normal.

Não há como o modelo escolher size, model ou imagens de referência: aspect já seleciona o tamanho, o modelo padrão é decisão de custo da sua conta, e referências continuam disponíveis nas rotas de geração onde você as fornece.

Cada chamada gasta de verdade — em dólar ou em quota. A ferramenta resolve a sua OPENAI_API_KEY do cofre (GET/PUT /v1/tool-secrets), cai para a chave de plataforma, e — se nenhuma existir — cai para o modelo de assinatura codex/gpt-image-2, que não precisa de chave nenhuma. Ou seja: com o cofre vazio o agente ainda gera imagem, e você lê no resultado qual caminho foi usado (provider: openai = crédito de API, codex = assinatura; key_source: vault | platform | subscription). No caminho de assinatura, aspect e quality são advisory — o backend decide, e a resposta reporta o size/ quality que ele realmente produziu.

IMAGE_KEY_REQUIRED só aparece quando não há chave nem caminho de assinatura; uma credencial que o provedor recusa (expirada, revogada, sem billing) devolve IMAGE_PROVIDER_REJECTED — nesse caso revise a chave no cofre, não os argumentos.

GETCatálogo de modelos e provedores#

GET /v1/llm-catalog (alias: GET /v1/providers) — o catálogo de modelos, agrupado por provedor. Use-o para popular a sua UI com provedores, modelos, janelas de contexto e efforts REAIS em vez de descobrir por tentativa:

Modelos OpenAI aposentados. A plataforma roda OpenAI 5.6 e mais novos. Gerações anteriores — gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, gpt-5.3-codex, gpt-4o, gpt-4o-mini, gpt-4.1* e a série o1/o3/o4 — não aparecem mais neste catálogo e são recusadas com 400 MODEL_RETIRED ao criar/atualizar um agente (inclusive via default_model no compat). A rejeição é explícita de propósito: preferimos que a sua integração falhe no write, e não que ela rode meses num modelo diferente do que pediu. Migre para gpt-5.6-luna (o mais barato), gpt-5.6-terra ou gpt-5.6-sol — ou para a família GPT-6.

Família GPT-6 (desde 2026-09-25). gpt-6-luna, gpt-6-sol e gpt-6-astra estão no catálogo, pela API key (openai, efforts low|medium|high) e pela assinatura (codex/gpt-6-*, efforts até max, custo 0). A geração 6 é nomeada sem versão menor (gpt-6-luna, não gpt-6.0-luna); até esta data o piso lia isso como a nomenclatura antiga e respondia 400 MODEL_RETIRED para o id cru gpt-6-*. Corrigido: o piso continua sendo "OpenAI 5.6 e mais novos", e a 6 está acima dele.

Isso vale só para o provedor OpenAI. Modelos de outros provedores não mudaram — inclusive gpt-oss-*, que apesar do prefixo gpt- é o modelo de pesos abertos servido por Cerebras/OpenRouter e continua disponível.

Provedor codex — GPT-5.6 Luna com effort max#

O grupo codex ("OpenAI (ChatGPT subscription)") expõe:

id Efforts Custo reportado
codex/gpt-5.6-luna low, medium, high, xhigh, max 0
codex/gpt-5.6-sol low, medium, high, xhigh, max 0
codex/gpt-6-luna low, medium, high, xhigh, max 0
codex/gpt-6-sol low, medium, high, xhigh, max 0
codex/gpt-6-astra low, medium, high, xhigh, max 0

Use o id com o prefixo codex/ — é ele que roteia para este provedor:

json
{ "model": "codex/gpt-5.6-sol", "reasoning_effort": "max" }

Correção de contrato (2026-08-16). Até esta data, runs em codex/gpt-5.6-sol vinham com total_cost_usd calculado à tarifa de API do gpt-5.6-sol ($5/$30 por 1M) em vez de 0. Era um defeito do nosso medidor, não uma cobrança: nenhum desses valores foi faturado por ninguém. O custo agora é decidido pelo provedor resolvido — qualquer modelo servido por um provedor de assinatura reporta 0, tenha ou não linha de catálogo. Se você arquivou total_cost_usd de runs codex/* antes desta data, esses números estão inflados e devem ser lidos como 0.

Duas diferenças que importam para a sua integração:

  • xhigh e max funcionam aqui. No gpt-5.6-luna normal (provedor openai) os efforts param em high; pedir max lá é aceito mas o modelo raciocina como high. Em codex/gpt-5.6-luna o max é repassado de verdade. Consulte thinking_efforts no catálogo em vez de assumir.
  • input_cost_per_1m e output_cost_per_1m são 0, e as runs saem com total_cost_usd: 0. Não é modelo grátis nem erro de catálogo: é um plano de assinatura de valor fixo, então não existe custo por token para reportar. Se o seu billing soma total_cost_usd, este provedor contribui zero por contrato — não trate como dado faltando. Isso vale para todo modelo do grupo codex, inclusive um que ainda não esteja listado acima: o preço sai do provedor, não do id.

O available deste grupo pode vir false (assinatura não configurada no ambiente). Cheque available antes de oferecer o modelo na sua UI — é exatamente para isso que ele existe. Enquanto false, use gpt-5.6-luna do grupo openai, que é o mesmo modelo cobrado por token.

json
{
  "providers": [
    {
      "provider": "kimi", "label": "Kimi", "available": true,
      "models": [
        { "id": "k3", "provider": "kimi", "label": "K3",
          "context_window": 1048576, "max_output_tokens": null,
          "input_cost_per_1m": 0, "output_cost_per_1m": 0,
          "supports_tools": true, "supports_vision": true,
          "supports_tool_choice": true,
          "supports_thinking": true,
          "thinking_efforts": ["low", "high", "max"],
          "default_thinking_effort": "high" }
      ]
    }
  ],
  "default_fallbacks": [
    { "model": "codex/gpt-5.6-luna", "provider": "codex", "available": true }
  ]
}

Semântica dos campos novos (contrato: desconhecido = null, nunca um chute):

  • available — se o provedor pode servir AGORA. Ele fica no GRUPO do provedor, não em cada modelo. Um if (model.available) lê undefined e esconderia o modelo da sua UI — a checagem é if (provider.available !== false), no nível do grupo. null = indeterminado (não conseguimos consultar o engine no momento); nunca um false fabricado.

    Mudou em 2026-08-28 (carta bia). Antes, available significava só "a credencial está configurada". Uma chave configurada pode ter sido revogada no provedor, e foi: a OPENAI_API_KEY do ambiente passou a responder 401 invalid_api_key a toda chamada enquanto o catálogo continuava anunciando openai: available: true — o que é pior que false, porque aponta a sua UI para modelos que nunca respondem. Agora available é "configurada E não foi recusada pelo upstream na última chamada real": quando o provedor rejeita a credencial, o grupo vira false na hora, e volta a true sozinho na primeira chamada que ele aceitar (rotação de chave não exige restart). A recusa é observada nas chamadas que já acontecem — não sondamos provedor para responder este campo.

    Continua não sendo checagem de saldo: crédito esgotado aparece como erro no turno (class: "quota"), não como available: false.

  • supports_tool_choice — o provedor deste modelo mapeia tool_choice. Quando false, mandar tool_choice (inclusive "none") neste modelo é recusado com 400 TOOL_CHOICE_UNSUPPORTED em vez de ser descartado em silêncio. Use este campo no seu seletor para nunca oferecer uma combinação que morre no dispatch. Hoje o único grupo com false é Gemini (google).

  • max_output_tokens — teto de saída documentado pelo provedor; null quando o provedor não publica.

  • cached_input_cost_per_1m — preço por 1M de tokens de entrada servidos do cache de prompt do provedor. null quando o provedor não publica uma tarifa de cache para o modelo, e nesse caso o cache é cobrado a preço cheio de entrada — nunca aplicamos um desconto que não temos como sustentar. Tokens de cache são um subconjunto de input_tokens e chegam em usage.cached_tokens. Novo em 2026-08-16: antes desta data todo token de cache era cobrado a preço cheio, o que superestimava o custo reportado — em uma run real de 884.745 tokens de entrada com 99,8% de cache, por 9,6×.

  • supports_thinking — o runtime expõe o raciocínio deste modelo separado do texto final (thinking nos eventos/respostas).

  • thinking_efforts + default_thinking_effort — os valores de reasoning_effort que efetivamente mudam o comportamento deste modelo, e o aplicado quando o agente não define nenhum. Três estados, e a diferença entre null e [] é intencional:

    valor significa mandar reasoning_effort
    ["low","high","max"] a escada publicada deste modelo só esses valores
    null o modelo raciocina, mas o provedor não publica uma escada discreta (MiniMax, Gemini) qualquer valor válido é aceito — não contradizemos o que não conseguimos verificar
    [] o provedor publica uma recusa do parâmetro: o modelo raciocina e ainda assim responde erro se você escolher o nível (grok-4.20) nenhum — é 400 EFFORT_NOT_SUPPORTED
    supports_thinking: false o modelo não raciocina nenhum — é 400 EFFORT_NOT_SUPPORTED

    Nos dois últimos casos o seu seletor não deve oferecer nível nenhum. Os dois renderizam igual ("sem seletor"); [] diz a mais que mandar um é erro.

Novo em 2026-08-29 (carta bia). Antes desta data um reasoning_effort que o modelo não aceitava era validado só contra o conjunto da plataforma (low|medium|high|xhigh|max) e descartado em silêncio mais adiante — do seu lado isso é indistinguível de ter funcionado. Agora o par (modelo, effort) é validado em todas as portas que o escrevem, inclusive o override por dispatch, e um par impossível é 400 EFFORT_NOT_SUPPORTED com a mensagem nomeando o conjunto que teria funcionado.

default_fallbacks — a reserva da plataforma#

O catálogo também declara a cadeia de reserva do ambiente: os modelos que atendem quando o primário de um agente falha por credencial/cota/indisponibilidade e o agente não declarou um fallback próprio.

json
"default_fallbacks": [
  { "model": "codex/gpt-5.6-luna", "provider": "codex", "available": true }
]

Lista vazia = não há reserva de plataforma; um agente sem fallback próprio morre na primeira recusa do primário. Cada entrada carrega o available do seu provedor pela mesma regra acima, então dá para responder "a minha reserva responde?" sem tentar. A cadeia vem na ordem em que é tentada — é aqui que se confere qual provedor pode substituir o seu, sem perguntar a ninguém. E dá para recusá-la por dispatch: ver disable_model_fallback.

Novo em 2026-08-28 (carta bia). Existe porque essa cadeia era invisível: ela apontava para um único modelo cuja chave estava revogada, e não havia superfície onde notar. Reserva que não pode responder é pior que não ter reserva — se a sua integração depende de continuidade, declare o seu próprio fallback no agente em vez de herdar o da plataforma, que é um piso operacional nosso e pode mudar.

Entre os provedores estão Kimi (k3 com 1M de contexto, k3-256k, kimi-for-coding, kimi-for-coding-highspeed — todos thinking-only, efforts low|high|max) e MiniMax (MiniMax-M3, MiniMax-M2.7, MiniMax-M2.5, MiniMax-M2.1, MiniMax-M2 + variantes -highspeed). Em todos os provedores o content das respostas vem saneado: o raciocínio nunca chega inline no texto (nada de <think> cru) — ele vem no campo/evento thinking.

POSTCriar ferramenta REST#

POST /v1/tools (owner/admin) — corpo customToolRequest:

Campo Tipo Regra
name string obrigatório, regex ^http_[a-z0-9_]{1,120}$
url string obrigatório, validado contra SSRF
method string GET/POST/… (upper-case)
display_name, description string —
header_template, query_template objeto mapa string→string; use ${secret:NOME} para segredos do cofre
body_template string —
input_schema JSON schema dos argumentos — validado no save (ver abaixo)
timeout_ms int default 15000
max_response_kb int default 256
enabled bool? default true
tenant_external_id string? escopa a ferramenta a UM cliente final; ausente = visível a toda a conta

Resposta 201: customToolResponse (ecoa tenant_id). Leituras (GET /v1/tools, /v1/tools/{id}) são all-auth. Erros: TOOL_NAME_INVALID, TOOL_URL_INVALID, SECRET_NOT_FOUND, TOOL_NAME_CONFLICT, TOOL_SCHEMA_INVALID.

input_schema é validado no save (desde 2026-08-19)#

O input_schema vai literalmente para o provedor de LLM (input_schema na Anthropic, function.parameters na OpenAI/Kimi, parameters no Gemini). Um schema que o provedor recusa derruba a rodada inteira — e junto com ela todas as outras ferramentas do turno. Por isso ele agora é recusado no momento em que você grava, com o caminho exato do campo:

http
POST /v1/tools
{"name":"http_x","input_schema":{"type":"object","properties":{"body":{"description":"…"}}}}

HTTP/1.1 400 Bad Request
{
  "error_code": "TOOL_SCHEMA_INVALID",
  "path": "properties.body",
  "message": "input_schema: properties.body: missing \"type\" (or one of anyOf/oneOf/allOf/enum/const/$ref)"
}

As regras (o mínimo denominador que os quatro provedores aceitam):

Regra Exemplo recusado
a raiz declara "type":"object" {"properties":{…}}, "texto", 42
toda propriedade, em qualquer nível, declara type (ou anyOf/oneOf/allOf/enum/const/$ref) {"body":{"description":"…"}}
type pertence a string|number|integer|boolean|object|array|null {"type":"strig"}
array traz items {"type":"array"}
required só cita propriedades declaradas required:["nao_existe"]
$ref é local (começa com #) {"$ref":"https://…"}
≤ 32 KB e ≤ 16 níveis de aninhamento —

input_schema ausente continua válido e significa "esta ferramenta não recebe argumentos" (equivale a {"type":"object","properties":{}}).

Vale nos quatro caminhos de escrita: POST /v1/tools, PATCH /v1/tools/{id}, POST /v1/tools/test-draft, e o provisionamento de integrações (POST/PATCH /v1/projects/{pid}/integrations/{id}/tools e o lote .../tools/sync). No lote, um item recusado conta como falha do item — e como o prune só roda com failed == 0, um schema malformado nunca apaga as ferramentas que já funcionavam.

Reprovisionamento é idempotente pelo nome — trate o 409, não o 500. O nome da ferramenta é único por conta. Se você reconcilia seu conjunto de ferramentas num loop (o padrão de quem embute agentes), o POST de uma que já existe responde:

http
HTTP/1.1 409 Conflict
{
  "error_code": "TOOL_NAME_CONFLICT",
  "id":   "0d7f…",                     ← a ferramenta que JÁ está lá
  "name": "http_minha_tool",
  "message": "a tool named http_minha_tool already exists in this company — PATCH /v1/tools/{id} to update it"
}

O id vem no corpo justamente para você seguir com PATCH /v1/tools/{id} sem precisar de um GET /v1/tools para descobri-lo. Trate 409 como "já existe, siga em frente" — não como falha a repetir.

Mudança de contrato (2026-08-15): esse caso devolvia 500 INTERNAL_ERROR com a mensagem "failed to create tool (name may already exist)". Se o seu cliente depende do 500 ou faz match naquela string, migre para o 409 + error_code. O mesmo vale para POST /v1/skills, que agora devolve 409 SKILL_NAME_CONFLICT.

Checklist: do zero à ferramenta funcionando no turno. Criar a ferramenta NÃO basta — para o modelo enxergá-la e o segredo resolver, são quatro passos:

  1. Crie a ferramenta (POST /v1/tools). O nome segue ^http_[a-z0-9_]{1,120}$.
  2. Crie o segredo com um nome do alfabeto do resolvedor (PUT /v1/tool-secrets): NOME casa [A-Za-z0-9_-]{1,128} (letras, dígitos, _ e -). Fora disso o upsert devolve 400. (Antes de 2026-07-25 o cofre aceitava qualquer nome e um nome fora do alfabeto fazia o placeholder ${secret:...} viajar LITERAL na requisição: o endpoint respondia 401 como se a credencial estivesse errada, e nada indicava que o problema era a RESOLUÇÃO na origem, não o destino.)
  3. Anexe a ferramenta ao agente (PATCH /v1/agents/{id} com tools = a união do que já estava + a nova). O modelo só vê o que está no tools[] do agente: criar a ferramenta NÃO a anexa, e um agente criado ANTES dela não a recebe automaticamente — sem o patch, ele nunca a terá no catálogo do turno.
  4. Teste antes de depender (POST /v1/tools/{id}/test com args de exemplo): executa a chamada real (guardada contra SSRF) e mostra o que o endpoint recebeu. Um segredo que não resolve aparece aqui como 401 do destino — em segundos, não no meio de um run de minutos.

Escopo por tenant (opcional). POST /v1/tools e PUT /v1/tool-secrets ({ name, value, tenant_external_id? }) aceitam o eixo de tenant. Sem ele o recurso é da conta inteira — o comportamento de sempre. Com ele valem duas garantias no runtime:

  1. Um agente do tenant A monta apenas ferramentas do tenant A ou da conta inteira. A ferramenta de outro tenant é ignorada mesmo se o tools:[...] do agente citar o nome (o vínculo é por nome, então sem isso o nome seria a permissão).
  2. Um ${secret:NOME} dentro de uma ferramenta do tenant A resolve apenas segredos do tenant A ou da conta inteira. O segredo de outro tenant segue o mesmo caminho de um segredo inexistente (substituição vazia), então o valor nunca chega ao endpoint.

O nome continua único por conta — tenant é eixo de visibilidade, não namespace (o modelo escolhe a ferramenta pelo nome dentro do turno, então dois nomes iguais no mesmo catálogo seriam ambíguos). Use o seu próprio prefixo para segregar. O escopo é imutável: um PUT /v1/tool-secrets que mudaria o tenant de um segredo existente devolve 409 SECRET_SCOPE_IMMUTABLE — remova e recrie, para que uma rotação distraída nunca alargue silenciosamente um segredo de tenant para a conta toda.

Gerenciar ferramentas#

Rota Ação
GET /v1/tools · GET /v1/tools/{id} Listar / detalhar (all-auth)
PATCH /v1/tools/{id} Update esparso (owner/admin)
DELETE /v1/tools/{id} Remover (owner/admin)
POST /v1/tools/{id}/test Dry-run da ferramenta salva, com args de exemplo
POST /v1/tools/draft Tool-builder com IA: descreva a integração em texto, receba um rascunho de ferramenta
POST /v1/tools/test-draft Dry-run de um rascunho sem salvar (corpo = a ferramenta)
GET /v1/tools/{id}/versions · POST /v1/tools/{id}/versions/{vid}/restore Histórico de snapshots + restauração

Segredos do cofre (os ${secret:NOME} dos templates): GET /v1/tool-secrets lista (name + last4, nunca o valor), PUT /v1/tool-secrets cria/atualiza ({ name, value, tenant_external_id? }), DELETE /v1/tool-secrets/{name} remove.

Regras do ${secret:NOME}: o NOME casa [A-Za-z0-9_-]{1,128} — fora disso o PUT devolve 400 (a escrita e a leitura usam o mesmo alfabeto; um nome que o resolvedor não enxerga falha na hora de salvar, nunca no meio de um dispatch). A resolução é fail-closed: segredo ausente, indecifrável ou de outro tenant resolve para string vazia — nunca o placeholder literal — então o endpoint recebe uma credencial vazia. Se o seu endpoint responde 401 no dispatch, verifique nesta ordem: (a) o NOME no template bate exatamente com o nome no cofre; (b) o segredo está no mesmo escopo de tenant da ferramenta (ou é da conta inteira); (c) o last4 em GET /v1/tool-secrets é o valor que você espera.

Cabeçalhos confiáveis que a plataforma envia à sua ferramenta#

Cada dispatch carrega o escopo do run em cabeçalhos que a engine assina do lado dela — eles vêm do run request, nunca dos argumentos que o modelo escreveu, então sua ferramenta pode confiar neles para autorizar e para deduplicar:

Header Sempre? Significado
X-Tenant-ID sim conta (organização) dona do run
X-Project-ID sim projeto do run
X-Agent-ID sim agente que chamou a ferramenta
X-Session-ID sim conversa — estável entre turnos
X-Stream-ID sim ESTE run — muda a cada turno
X-Tool-Call-ID em run de agente ESTA invocação da ferramenta, dentro do turno
X-Project-Tenant-ID quando há cliente final do projeto (eixo de tenant)
X-Runtime-Token-ID quando há token prt_ que originou o run
X-End-User-ID / X-End-User-External-ID quando há usuário final vinculado ao run

X-Tool-Call-ID existe quando um agente chamou a ferramenta (o id vem da invocação do provider). O dispatch one-shot de POST /v1/tools/{id}/test não tem invocação de provider nenhuma, então esse header não vem no dry-run — trate-o como presente-em-run, ausente-em-teste, nunca como obrigatório.

Para idempotência use X-Tool-Call-ID, não o corpo. Ele identifica uma invocação única e é reenviado idêntico se o mesmo dispatch for repetido, enquanto X-Session-ID cobre a conversa inteira e X-Stream-ID o turno inteiro (vários chamados de ferramenta compartilham ambos). Qualquer id vindo no corpo é escrito pelo modelo e pode repetir.

timeout_ms é o prazo que vale. O deadline da sua ferramenta é o timeout_ms que você configurou (default 15000). O run não tem mais wall clock próprio (desde 2026-08-24); o único teto acima da ferramenta é o da infraestrutura (3600 s por requisição). Não existe mais um teto de 60s do lado do cliente HTTP: uma ferramenta que declara 180000 recebe de fato os 180s, desde que o run ainda tenha essa folga.

GETSkills#

GET /v1/skills — { skills: [ { id, name, description, body, scope, read_only, … } ], total }. POST /v1/skills cria ({ name, description?, body }; SKILL_NAME_REQUIRED, SKILL_BODY_REQUIRED). Skills globais da plataforma são read-only (SKILL_READONLY).

Uma skill é um pacote de instruções reutilizável (nome + descrição + corpo). No prompt, o agente vê um catálogo progressivo: só nome+descrição entram de cara; quando a tarefa casa com a descrição, ele chama a tool load_skill(name) e recebe o corpo inteiro sob demanda.

Gerenciar skills#

Rota Ação
PATCH /v1/skills/{id} Update esparso (name/description/body)
DELETE /v1/skills/{id} Remover → 204
POST /v1/skills/{id}/preview Corpo { system_prompt? } → { header, body, injected, assembled }: o que entra no prompt de cara (header) e o que load_skill devolve (body). Determinístico, sem LLM
GET /v1/skills/{id}/versions · POST /v1/skills/{id}/versions/{vid}/restore Histórico + restauração

Skills de um agente (plug/unplug)#

Rota Ação
GET /v1/agents/{id}/skills { bindings: [ { skill_id, name, description, scope, read_only, enabled, priority } ], total }
PUT /v1/agents/{id}/skills Corpo { bindings: [ { skill_id, enabled?, priority? } ] } — define atomicamente o conjunto plugado (plug/unplug/reordenar/toggle)
PATCH /v1/agents/{id}/skills/{skillId} Corpo { enabled?, priority? } — liga/desliga ou reordena UMA skill plugada (404 SKILL_NOT_ACTIVE se não está plugada)