AvalonOpsDocumentação

Data plane

Cinco superfícies, um só pipeline de governança.

As rotas abaixo entram pela mesma porta: autenticação, permissões, orçamento e taxa, guardrails, roteamento e trilha valem para todas. O que muda é o protocolo de cada uma.

Completions

POST /v1/chat/completions aceita o corpo da OpenAI e o repassa ao provedor resolvido para o modelo, depois de aplicar os portões da organização. A resposta de sucesso é a do provedor — exceto pela máscara de PII que um guardrail de saída em modo mascarar aplica sobre choices[].message.content antes de o cliente, o cache e a trilha verem o corpo (ver Guardrails).

Streaming

Com "stream": true a resposta É text/event-stream: uma linha data: <json do chunk> por quadro e um data: [DONE] final. O x-avalon-request-id sai no primeiro byte, antes de qualquer quadro — é o que permite correlacionar um stream que termina mal. Guardrails de entrada rodam antes de qualquer byte sair: uma recusa sai como resposta HTTP comum, em JSON, e o cliente nunca chega a ver cabeçalho de stream nenhum.

Peça "stream_options": {"include_usage": true} para o quadro final de usage — sem ele o provedor normalmente não manda tokens, e a linha da trilha fica com consumo não medido.

246 e 446: os dois status que só existem aqui

Status próprios do pipeline de guardrails
StatusQuando acontece
246Sucesso COM AVISO: um guardrail em modo monitor (deny: false) falhou sem bloquear. O corpo é a completion do provedor, igual a um 200 — só o status muda. O detalhe de qual guardrail falhou fica na trilha, nunca no corpo. Embeddings roda guardrails de entrada como as completions, mas a rota sempre devolve o status cru do provedor (ou do cache) — nunca promove para 246, mesmo quando um guardrail de entrada em modo monitor falhou.
446Um guardrail bloqueante recusou o conteúdo, na entrada ou na saída. Fora da faixa que os SDKs compatíveis com OpenAI interpretam, de propósito: nunca é confundido com sucesso parcial nem com erro do provedor. O corpo diz qual guardrail e qual check, nunca o conteúdo detectado.

Embeddings

POST /v1/embeddings tem o mesmo pipeline de governança das completions — permissões, orçamento, taxa, roteamento, cache simples, trilha — para vetores de texto. As diferenças de contrato que valem ler antes de integrar:

  • input aceita uma string ou um array de até 2048 strings (o mesmo teto da OpenAI). Array vazio, string vazia ou item vazio recusam 400 input_invalido.
  • encoding_format só aceita 'float'; qualquer outro valor recusa 400 encoding_nao_suportado.
  • user alimenta metadata._user, o mesmo campo que o braço do titular da LGPD usa para achar (e apagar) o que uma pessoa gerou.
  • Guardrails valem só na direção de entrada — a saída é um vetor numérico, sem texto. Um check de saída configurado não quebra a rota: é ignorado e anotado como direcao_inaplicavel na trilha.
  • A trilha grava um resumo da resposta ({object, itens, dims, usage}), nunca o vetor. O cliente recebe a resposta íntegra. Guardar 2048 × 1536 floats por linha custaria dezenas de MB para armazenar o que ninguém audita.
  • Não existe streaming no protocolo de embeddings: stream e stream_options são removidos do corpo antes de sair para o motor.

Prompt library

POST /v1/prompts/{referencia}/completions resolve a referência no workspace da chave, renderiza as mensagens da versão com variables (sintaxe Mustache, incluindo {{>partial}}) e entrega o resultado ao MESMO pipeline das completions — orçamento, taxa, guardrails, roteamento, streaming e trilha idênticos.

Campos do corpo além de variables são overrides, aplicados campo a campo sobre o que a versão define (model, temperature, e até messages). variables é vocabulário nosso e nunca atravessa para o provedor. A trilha marca a linha com metadata._prompt e, quando o corpo sobrescreveu as mensagens renderizadas, com metadata._prompt_override.

O prompt existe mas não renderiza? 422, com partial_nao_encontrado ou partial_ciclico — nada foi chamado no provedor. Referência inexistente, versão fora do range, label sem versão, ou prompt de outro workspace: os quatro respondem 404 prompt_nao_encontrado, igual, para que quem varre referências não aprenda o que existe.

Gateway MCP

Proxy governado de Model Context Protocol, no transporte Streamable HTTP. O corpo é uma requisição JSON-RPC 2.0 — initialize, tools/list, tools/call, ou um batch — e atravessa intacto para o servidor MCP cadastrado sob o slug. A resposta do upstream volta como veio, com uma allowlist estrita de cabeçalhos.

Os três verbos do transporte
VerboPapel
POST /mcp/{slug}A requisição JSON-RPC.
GET /mcp/{slug}O canal SSE por onde o servidor envia mensagens não solicitadas. O Last-Event-Id do cliente é repassado, para uma reconexão retomar de onde parou.
DELETE /mcp/{slug}Encerra a sessão identificada pelo Mcp-Session-Id. Um encerramento limpo costuma ser 204, sem corpo.

Sessão

O Mcp-Session-Id vale nos dois sentidos: o que o servidor MCP emitir volta ao cliente, e o que o cliente mandar é repassado ao servidor. É isso que faz um SDK de MCP manter sessão através do gateway sem saber que ele existe. O Mcp-Protocol-Version do cliente também é repassado.

O que o gateway decide antes de repassar

  • A chave precisa do escopo mcp.invoke.
  • O servidor precisa estar PROVISIONADO para o workspace da chave — o gate real de alcance, mais forte que o escopo.
  • A credencial do servidor é injetada por nós e nunca pode ser sobrescrita por um cabeçalho do cliente.
  • Ferramenta fora da allowlist da organização é recusada no protocolo, não no HTTP: 200 com envelope de erro JSON-RPC (code: -32603, data.codigo: ferramenta_nao_permitida). Derrubar a sessão de um cliente de SDK por política seria pior do que responder o que ele sabe ler.
  • Corpo de erro do upstream NUNCA é repassado (pode carregar credencial nossa) — vira 502 upstream_mcp_indisponivel.

Gateway A2A (agentes)

Proxy governado de A2A (Agent-to-Agent). O corpo é uma requisição JSON-RPC 2.0 e atravessa intacto para o agente cadastrado sob o slug — inclusive quando a resposta é streaming: message/stream e tasks/resubscribe respondem em SSE, e o relay é incremental (cada quadro sai para o cliente no instante em que chega do agente, com contrapressão — nunca bufferizado e despejado no fim).

Quickstart: apontar um agente existente para o gateway

Não há SDK novo a instalar. O cliente A2A troca a URL base do agente pela nossa e autentica com uma chave gov_ que tenha o escopo agent.invoke — o mesmo cabeçalho Authorization de sempre. O SDK resolve o card em GET /a2a/{slug}/.well-known/agent-card.json e passa a falar JSON-RPC em POST /a2a/{slug}.

antes → depois
# antes: o cliente fala direto com o agente
https://meu-agente.exemplo.com

# depois: mesma conversa, agora governada
https://api.avalonops.com.br/a2a/ag-meu-agente-3f8a1c

Authorization: Bearer gov_...   # chave com escopo agent.invoke

O endereço real do agente sai de supported_interfaces[] do card dele — o gateway nunca assume que a URL base é o endpoint RPC. E o card servido por nós é o do agente, como ele o publicou (com provider, signatures, ícone e extensões que nem lemos), filtrado pela política de skill do workspace da chave. A descoberta exige a MESMA chave e o MESMO escopo do POST: um card público seria um enumerador de slugs.

O que o gateway decide antes de repassar

  • A chave precisa do escopo agent.invoke.
  • O limite de taxa por requisições do workspace vale (com contagem própria). Orçamento não — uma chamada de agente não consome tokens.
  • O agente precisa estar PROVISIONADO para o workspace da chave — o gate real de alcance, mais forte que o escopo. Slug inexistente e slug não provisionado devolvem o MESMO 404 servidor_agent_nao_encontrado, para que quem varre slugs não aprenda o que existe.
  • A credencial do agente é injetada por nós e nunca vem do cliente; a chave gov_ do cliente nunca atravessa para o agente.
  • Corpo de erro do upstream NUNCA é repassado (pode carregar credencial nossa) — vira 502 upstream_agent_indisponivel. Já um erro A2A do próprio agente (-32001 TaskNotFound) é resposta legítima do protocolo e volta crua, dentro de um 200.

skillId é convenção da Avalon, opt-in — não é campo do protocolo

Declarada a skill e estando ela fora da allowlist do par agente×workspace, a recusa é no protocolo, não no HTTP: 200 com envelope de erro JSON-RPC (code: -32603, data.codigo: skill_nao_permitida), com o mesmo id que o cliente mandou. Derrubar a conversa de um SDK por política seria pior do que responder o que ele sabe ler. A mesma allowlist filtra skills[] do card servido àquele workspace.

Webhooks: mediação total

tasks/pushNotificationConfig/set e tasks/pushNotificationConfig/get são os ÚNICOS métodos interceptados (não repassados crus). No set, o gateway guarda a URL que o cliente configurou e registra no agente uma URL nossa, com um token opaco por task: o agente nunca conhece a URL do cliente, e o cliente nunca vê a nossa. O get responde com a config DO CLIENTE, sem consultar o agente (que só conhece a nossa).

  • URL do cliente inválida (mesma política de SSRF do resto do produto) recusa webhook_url_invalida — em envelope JSON-RPC, como a recusa de skill. A URL é validada duas vezes: ao gravar e de novo no instante da entrega (o DNS pode ter mudado).
  • set sem params.taskId ou sem pushNotificationConfig.url recusa webhook_config_invalida.
  • QUALQUER lote JSON-RPC (um array no corpo) que contenha uma chamada set ou get é recusado inteiro (webhook_batch_nao_suportado), mesmo que essa seja a única chamada do lote — um array de um item só ainda é um array, e a mediação de webhook não sabe reescrever metade de um corpo que o cliente mandou como uma unidade. Mande o set sozinho, fora do lote.
  • Recebida a notificação em POST /a2a/webhook/{token}, a entrega ao webhook do cliente sai assinada com HMAC SHA-256 do corpo no cabeçalho x-avalon-assinatura, com retentativas em 1s, 10s e 60s. O segredo é gerado no console e exibido uma única vez.

Rotacionar o segredo substitui o atual na hora, sem período de graça: as entregas ainda não enviadas passam a assinar com o segredo novo (ele é lido na hora de assinar, uma vez por tentativa). O que já saiu não é alcançado por rotação nenhuma.

Cabeçalhos

requisição
x-metadata: {"cliente":"acme","feature":"suporte"}   # pares texto->texto na trilha
x-portkey-config: cfg_...                            # vence a config padrão da chave
x-portkey-cache-force-refresh: 1                     # presença basta: ignora e regrava
resposta
x-avalon-request-id: <uuid>          # o mesmo id da linha da trilha
x-avalon-retry-attempt-count: 2      # tentativas do roteamento antes do passo final
x-origem-da-falha: plataforma        # ou "provedor", em toda resposta de erro
x-portkey-cache-status: HIT          # HIT | MISS | REFRESH | DISABLED