Visão geral
Projeto de IA raramente trava no modelo. Trava no que vem em volta: cada fornecedor com seu formato, suas chaves, seus limites e sua fatura; a prova de conceito que roda numa tarde e a migração inteira que aparece três meses depois, quando algo melhor entra no mercado. Essa parte deixa de ser sua.
A superfície é compatível com o que o mercado já fala — o mesmo corpo que você manda hoje para um cliente OpenAI ou Anthropic funciona aqui, streaming incluído. O que muda é o que passa a vir junto:
- Uma credencial, todas as capacidades. Texto, imagem, voz, vídeo e busca sob a mesma chave
kriou_live_…. Nenhuma chave de fornecedor circula no seu código. - Nada é gasto antes de valer. Tamanho de imagem, duração de vídeo, teto de caracteres, modelo habilitado — tudo conferido na entrada. Pedido malformado volta em milissegundos e custa zero crédito.
- Conta fechada por chamada. Tokens, segundos, caracteres e chamadas viram débito na carteira da chave que gastou, com o consumo real. Sem estimativa e sem surpresa no fim do mês.
- O modelo melhora sem você reescrever nada. Cada nome público é uma faixa de qualidade que medimos e revisamos. Quando aparece coisa melhor por trás dele, você recebe a troca — e o seu código continua igual.
A raiz é pública e serve como sonda de saúde: responde sem autenticação e diz qual build está no ar.
curl https://api.entelecy.ai/ \
-H "Accept: application/json"
# 200 OK
{
"service": "Entelecy.Api",
"environment": "Production",
"build": "c2-forwarding",
"forwarding": true,
"timestamp": "2026-08-11T18:55:49.231Z"
}Esta página é o que já está de pé. Se o seu produto pede uma rota que não existe, um modelo específico, um fluxo que o mercado não entrega pronto — isso é trabalho que fazemos, e é onde a conversa costuma ficar interessante. Fale com a gente
Autenticação
Toda chamada exige o header Authorization com uma chave da Entelecy no esquema Bearer. A chave nasce e vive na sua conta; o gateway a valida contra o Entelecy Account e mantém o resultado em cache curto, de modo que o custo dessa checagem é desprezível no caminho quente.
POST /v1/chat/completions HTTP/1.1
Host: api.entelecy.ai
Authorization: Bearer kriou_live_7Qb3xk9_M2pN-VtR4sLu8Z
Content-Type: application/jsonSobre as credenciais aceitas:
kriou_live_…— chave de produção. É a forma normal de integrar.kriou_test_…— chave de teste, útil para ambientes de homologação. Endpoints podem ser configurados para recusá-la.
Chave fora do formato, revogada ou expirada devolve 401 sem detalhar o motivo. A chave nunca aparece em log — nem inteira, nem parcial. Se ela vazar, revogue na conta: a validação em cache expira em segundos.
Convenções
Corpo e resposta em JSON UTF-8, salvo onde indicado (upload de áudio e mídia usa multipart/form-data; síntese de voz responde bytes de áudio; transcrição responde texto puro). Campos que o gateway não conhece são repassados ao fornecedor sem alteração — é o que permite usar recursos novos antes de eles aparecerem nesta página.
Headers
| Header | Onde | Descrição |
|---|---|---|
| Authorization | requisição | Bearer + chave da Entelecy. Obrigatório em todos os endpoints, menos na raiz. |
| Content-Type | requisição | application/json, ou multipart/form-data nos endpoints de upload. |
| X-Image-Provider | requisição | Vale só em /v1/images/generations: escolhe o fornecedor de imagem quando há mais de um habilitado. Em /v1/images/edits não tem efeito. Opcional — existe um padrão de servidor. |
Streaming
Com "stream": true a resposta vira text/event-stream: linhas data: …, uma por evento, sem buffer intermediário. O último evento útil carrega o consumo da chamada; depois dele vem data: [DONE].
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Ent"}}]}
data: {"id":"...","choices":[],"usage":{"prompt_tokens":812,"completion_tokens":214,"total_tokens":1026}}
data: [DONE]O gateway sempre pede o bloco de uso ao fornecedor, então o chunk final com usage chega mesmo que você não o solicite. Se o stream terminar sem [DONE], trate como resposta truncada e repita a chamada — a conexão foi cortada no meio.
O que a resposta sempre traz
Nas três rotas de conversa a resposta passa por uma normalização antes de chegar até você. Três garantias que valem em qualquer uma delas, com ou sem streaming:
- O
modelecoa o nome que você pediu. Vale na raiz do corpo, nomessage.modeldo primeiro evento do formato Anthropic e noresponse.modelde cada evento do formato Responses. O que você manda e o que volta falam a mesma língua. - O raciocínio vem em
reasoning. Aparece emchoices[].messagena resposta inteira e emchoices[].deltano stream. O nome antigoreasoning_contentcontinua ao lado dele enquanto os clientes migram — leiareasoning. - Campos internos do fornecedor não passam. O
system_fingerprint, que é id de build de terceiro, é removido. Omodeljá responde “quem atendeu”.
A normalização nunca quebra a resposta: corpo que não parseia, chunk partido no meio ou erro vindo do fornecedor passam intactos. Se algum campo novo aparecer, ele chega até você mesmo que o gateway ainda não o conheça.
Erros
Erros gerados pelo gateway têm sempre a mesma forma: um objeto error com type, message e, quando ajuda, code e param. Erros vindos do fornecedor são repassados com o status e o corpo originais.
{
"error": {
"type": "invalid_request_error",
"message": "Campo 'model' e obrigatorio."
}
}| Status | type | Quando acontece |
|---|---|---|
| 400 | invalid_request_error | Corpo vazio, JSON inválido, campo obrigatório ausente ou envelope recusado por uma das regras do gateway. |
| 401 | — | Header ausente, chave fora do formato, revogada ou de ambiente não permitido. |
| 402 | insufficient_credits | Saldo abaixo do mínimo da operação. O corpo traz saldo, quanto falta e a URL para recarregar. |
| 4xx / 5xx | repassado | Erro do fornecedor (limite de taxa, conteúdo recusado, indisponibilidade). Status e corpo chegam como vieram. |
| 503 | upstream_unavailable | O fornecedor caiu antes do primeiro byte e as retentativas do gateway se esgotaram. Em streaming, chega como evento SSE. |
Códigos do gateway
São as recusas decididas antes de chamar o fornecedor. Todas voltam em 400, custam zero crédito e trazem mensagem acionável — dá para corrigir o pedido e repetir sem adivinhação.
| code | Descrição |
|---|---|
| gateway_invalid_size | size fora do envelope do modelo: formato, múltiplo de pixels, área mínima ou máxima, aresta ou proporção. |
| gateway_unsupported_background | Fundo transparente pedido a um modelo que não suporta e sem alternativa configurada. |
| gateway_unknown_model | Modelo não habilitado para o endpoint. A mensagem lista os aceitos. |
| gateway_invalid_duration | duration ausente, não inteira ou fora da faixa permitida para vídeo. |
| gateway_invalid_resolution | Resolução de vídeo fora da lista aceita. |
| gateway_invalid_input | Texto acima do teto de caracteres da síntese de voz. Divida em partes. |
Códigos de imagem no chat
Recusas específicas de imagem dentro da conversa. Também são 400 decididos antes do fornecedor, e a mensagem sempre diz qual bloco de content causou o problema.
| code | Descrição |
|---|---|
| gateway_vision_unsupported | O modelo pedido não declara suporte a imagem nesta rota. É fail-closed de propósito: modelo novo que ainda não declarou visão recusa em vez de mandar a imagem para um destino que só lê texto. |
| gateway_image_invalid | O bloco de imagem não pôde ser lido: base64 corrompido, data URI malformado ou conteúdo que não é imagem. |
| gateway_image_mime_unsupported | Tipo de imagem fora dos aceitos. Use PNG, JPEG ou WebP. |
| gateway_image_too_large | Uma das imagens passa do teto por arquivo. |
| gateway_too_many_images | Imagens demais no mesmo turno. Divida em chamadas ou mande só as que importam. |
| gateway_payload_too_large | A soma do corpo passa do teto da requisição, mesmo com cada imagem dentro do limite individual. |
| gateway_remote_image_blocked | A URL https:// não pôde ser buscada: endereço interno, host recusado ou download que não completou. Em caso de dúvida, mande a imagem como data URI em base64. |
Corpo do 402
{
"error": {
"type": "insufficient_credits",
"message": "Saldo insuficiente pra chamar a API.",
"balance": 3,
"required": 10,
"missing": 7,
"plan_id": "starter",
"renews_at": "2026-09-01T00:00:00Z",
"upgrade_url": "https://account.entelecy.ai/plans"
}
}Este é o corpo das rotas de conversa, imagem, áudio e vídeo. O /v1/search devolve uma versão curta, só com message e upgrade_url — leia os campos de saldo com verificação, não como garantidos.
Endpoints
Quatorze rotas, agrupadas por capacidade. O caminho leva à seção que o detalha; a coluna da direita adianta a unidade de cobrança — o detalhe está em Cobrança.
| Endpoint | Descrição | Unidade cobrada |
|---|---|---|
| POST /v1/chat/completions | Conversa com texto e imagem, no formato Chat Completions. | tokens |
| POST /v1/messages | A mesma conversa, no formato Anthropic Messages — para clientes que já falam esse padrão. | tokens |
| POST /v1/responses | A mesma conversa, no formato Responses — é o que o Codex fala. | tokens |
| GET /v1/models | Catálogo dos nomes públicos e das rotas em que cada um vale. | sem cobrança |
| POST /v1/images/generations | Geração de imagem a partir de texto. | tokens |
| POST /v1/images/edits | Edição de imagem com máscara opcional. | tokens |
| POST /v1/audio/transcriptions | Transcrição de áudio (fala → texto). | segundos de áudio |
| POST /v1/audio/speech | Síntese de voz (texto → fala). | caracteres |
| GET /v1/audio/voices | Catálogo de vozes disponíveis. | sem cobrança |
| POST /v1/videos/generations | Geração de vídeo (submissão). | segundos gerados |
| GET /v1/videos/generations/{id} | Consulta do vídeo em processamento. | sem cobrança |
| POST /v1/videos/uploads | Upload de mídia de referência para vídeo a partir de imagem. | sem cobrança |
| POST /v1/search | Busca na web com resultados estruturados. | por chamada |
| GET / | Estado do serviço. Anônimo. | sem cobrança |
Conversa
A mesma conversa em três protocolos: /v1/chat/completions (formato OpenAI), /v1/messages (formato Anthropic) e /v1/responses (formato Responses, que o Codex fala). Os nomes de modelo são os mesmos nas três — o nome é a faixa de qualidade, não muda de significado conforme o protocolo. Escolha pela linguagem que o seu cliente já fala; o primeiro é o de uso geral.
POST/v1/chat/completionssuporta streaming (SSE)Campos que o gateway lê ou trata. Os demais seguem para o fornecedor sem alteração.
| Campo | Tipo | Descrição |
|---|---|---|
| modelobrigatório | string | Nome público do modelo: loom-flash ou loom-pro. |
| effort | string | Quanto o modelo deve pensar: none, high ou max. |
| messagesobrigatório | array | Turnos da conversa, com role (system, user, assistant, tool) e content. |
| stream | boolean | true troca a resposta por um stream SSE. Padrão false. |
| max_tokens | integer | Teto de tokens gerados na resposta. Omitido, vale o teto do modelo. |
| temperature | number | Aleatoriedade da amostragem: valores baixos deixam a resposta mais previsível, altos deixam mais variada. |
| tools | array | Até 128 ferramentas do tipo function, cada uma com name (letras, números, _ e -, até 64 caracteres), description e parameters em JSON Schema. Ver o formato abaixo. |
| tool_choice | string / object | A política de escolha: none, auto, required, ou um objeto apontando a ferramenta exigida. Ver abaixo. |
| response_format | object | text, o padrão, ou json_object. Ao pedir JSON, diga também no prompt que a saída deve ser JSON — sem isso o modelo pode gerar espaço em branco até bater o teto de tokens. |
| thinking | object | Eixo de pensamento do próprio protocolo, repassado como veio. Use-o para controlar na mão; para o atalho do gateway, use effort. |
| reasoning_effort | string | Idem: repassado como veio. A regra do fornecedor é high ou max; para desligar o pensamento, use thinking com { "type": "disabled" }. |
Ferramentas
O formato de tools e as quatro maneiras de dirigir a escolha com tool_choice:
"tools": [
{
"type": "function",
"function": {
"name": "buscar_pedido",
"description": "Busca um pedido pelo numero.",
"parameters": {
"type": "object",
"properties": { "numero": { "type": "string" } },
"required": ["numero"]
}
}
}
]
// tool_choice — as quatro formas
"tool_choice": "none" // responde sem chamar ferramenta
"tool_choice": "auto" // o modelo decide
"tool_choice": "required" // obriga a chamar alguma
"tool_choice": { "type": "function", "function": { "name": "buscar_pedido" } }Modelo e esforço
São dois eixos independentes, ambos no corpo: model escolhe a faixa e effort escolhe quanto o modelo pensa. Um não implica o outro — loom-flash com max e loom-pro com none são combinações válidas, e úteis.
| model | Perfil | Quando usar |
|---|---|---|
| loom-flash | Rápido e barato, para volume | Resposta curta, classificação, autocomplete, extração — onde a latência manda. |
| loom-pro | Mais capacidade, para trabalho difícil | Código, análise em várias etapas, redação longa, agentes com ferramentas. |
| effort | Perfil | Quando usar |
|---|---|---|
| none | Sem raciocínio explícito | O mais rápido e barato. Responde direto, sem etapa de pensamento. |
| high | Raciocínio ligado | O padrão recomendado quando a resposta precisa estar certa, não só rápida. |
| max | Raciocínio no teto | Problemas difíceis: refatoração grande, planejamento longo, correção acima de custo. |
Sem effort no corpo, thinking e reasoning_effort seguem como você os mandou — quem já controla o pensamento na mão continua funcionando igual. E a resposta traz de volta em model o mesmo nome que você pediu: o que você manda e o que volta falam a mesma língua.
A resposta, campo a campo
Formato Chat Completions. O gateway normaliza a resposta do fornecedor antes de devolver: model volta com o nome público que você pediu e campos internos do fornecedor não passam. Campos que o fornecedor acrescente e o gateway ainda não conheça são repassados — documentado aqui está o que é estável.
| Campo | Tipo | Descrição |
|---|---|---|
| id | string | Identificador único desta chamada. Opaco: não derive nada do formato. |
| object | string | chat.completion na resposta inteira; chat.completion.chunk em cada evento do stream. |
| created | integer | Momento da criação, em segundos Unix (UTC). |
| model | string | O mesmo nome que você pediu (loom-flash). |
| choices | array | Lista de respostas. Com n ausente, vem uma só. |
| choices[].index | integer | Posição desta escolha na lista. |
| choices[].message / delta | object | A mensagem gerada: role (assistant) e content. No stream este campo se chama delta e traz o pedaço novo, não o texto inteiro. |
| ….reasoning | string | O raciocínio, quando o modelo pensa antes de responder. É texto de diagnóstico, não resposta: não mostre no lugar do content, e não conte com idioma nem formato. |
| ….reasoning_content | string | Nome antigo do mesmo conteúdo, mantido em paralelo enquanto os clientes migram. Vai sair; use reasoning. |
| choices[].finish_reason | string | Por que parou: stop (fim natural), length (bateu o teto de tokens), tool_calls (quer chamar uma ferramenta). |
| usage | object | O consumo da chamada — é daqui que sai a cobrança. No stream vem no último evento, com choices vazio. |
| usage.prompt_tokens | integer | Tokens de entrada (o que você mandou). |
| usage.completion_tokens | integer | Tokens de saída (o que o modelo gerou), raciocínio incluído. |
| usage.total_tokens | integer | Soma dos dois. É o número que a cobrança usa. |
| …details.cached_tokens | integer | Caminho inteiro: usage.prompt_tokens_details.cached_tokens. A parte da entrada que bateu no cache de prompt — repetir um prefixo grande sai bem mais barato que reenviá-lo. |
| …details.reasoning_tokens | integer | Caminho inteiro: usage.completion_tokens_details.reasoning_tokens. Quanto da saída foi raciocínio. Já está dentro de completion_tokens — não some. |
O bloco usage é a base da cobrança: entrada, saída e a parte da entrada que bateu no cache de prompt — repetir um prefixo grande sai muito mais barato do que reenviá-lo do zero.
Enviando uma imagem
A imagem viaja dentro da conversa, como mais um bloco de content ao lado do texto — não há endpoint separado nem campo extra. Vários blocos enviam várias imagens.
IMG=$(base64 -w0 tela.png)
curl https://api.entelecy.ai/v1/chat/completions \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "loom-flash",
"messages": [
{ "role": "user", "content": [
{ "type": "text", "text": "O que ha de errado neste layout?" },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,'"$IMG"'" } }
]}
]
}'{ "type": "image_url", "image_url": { "url": "https://exemplo.com/tela.png", "detail": "high" } }O url aceita um data URI em base64, como acima, ou um endereço https:// público. O detail (low, high, auto) controla o quão fino é o exame da imagem.
O mesmo bloco vale no /v1/messages, no formato Anthropic: {"type":"image","source":{"type":"base64","media_type":"image/png","data":"..."}}. A imagem é cobrada dentro do turno, não como unidade à parte.
Requisição
curl https://api.entelecy.ai/v1/chat/completions \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "loom-flash",
"effort": "high",
"messages": [
{ "role": "system", "content": "Responda em pt-BR, direto ao ponto." },
{ "role": "user", "content": "Explique entelequia em tres linhas." }
]
}'const res = await fetch('https://api.entelecy.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'loom-flash',
effort: 'high',
messages: [
{ role: 'system', content: 'Responda em pt-BR, direto ao ponto.' },
{ role: 'user', content: 'Explique entelequia em tres linhas.' },
],
}),
});
const data = await res.json();
console.log(data.choices[0].message.content);import os, requests
res = requests.post(
"https://api.entelecy.ai/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
json={
"model": "loom-flash",
"effort": "high",
"messages": [
{"role": "system", "content": "Responda em pt-BR, direto ao ponto."},
{"role": "user", "content": "Explique entelequia em tres linhas."},
],
},
timeout=(10, 600),
)
res.raise_for_status()
print(res.json()["choices"][0]["message"]["content"])Resposta200
{
"id": "bf1b8201-5085-455c-9556-8ebd39a0a34e",
"object": "chat.completion",
"created": 1786061432,
"model": "loom-flash",
"choices": [
{ "index": 0, "finish_reason": "stop",
"message": { "role": "assistant", "content": "Entelequia e…" } }
],
"usage": {
"prompt_tokens": 812,
"completion_tokens": 214,
"total_tokens": 1026,
"prompt_tokens_details": { "cached_tokens": 640 },
"completion_tokens_details": { "reasoning_tokens": 159 }
}
}POST/v1/messagessuporta streaming (SSE)Formato Anthropic Messages, o mesmo que o Claude Code e outros clientes já falam. Por trás é a mesma conversa e o mesmo modelo do /v1/chat/completions — o que muda é o contrato de fio: system sai de dentro de messages e vira campo próprio, e max_tokens passa a ser obrigatório.
| Campo | Tipo | Descrição |
|---|---|---|
| modelobrigatório | string | Nome público do modelo: loom-flash ou loom-pro. |
| messagesobrigatório | array | Turnos da conversa, com role (user ou assistant) e content — texto ou blocos. A instrução de sistema vai no campo system, ao lado. |
| max_tokensobrigatório | integer | Teto de tokens gerados. Sempre envie: o formato Messages pede este campo em toda chamada. |
| system | string / array | Instrução de sistema, como campo da raiz — é aqui que ela entra neste formato. |
| stream | boolean | true troca a resposta por um stream SSE, no formato de eventos da Anthropic. Padrão false. |
A resposta
Formato Messages. O texto vem em content, que é uma lista de blocos — e não uma string, como no Chat Completions:
| Campo | Tipo | Descrição |
|---|---|---|
| id | string | Identificador desta mensagem. |
| type | string | message. |
| role | string | assistant. |
| model | string | O mesmo nome que você pediu. |
| content | array | Lista de blocos. Cada bloco tem type (text) e text — o texto da resposta é a junção deles. |
| stop_reason | string | Por que parou. end_turn é o fim natural. |
| stop_sequence | string | A sequência que interrompeu a geração, quando foi uma delas que parou. |
| usage | object | Consumo da chamada, com input_tokens e output_tokens. |
O erro desta rota vem no envelope da Anthropic: {"type":"error","error":{"type":…,"message":…}}, com um type a mais por fora. Nas outras rotas o corpo começa direto em error. Se o seu tratamento de erro é compartilhado entre as rotas, leia os dois formatos.
Requisição
curl https://api.entelecy.ai/v1/messages \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "loom-flash",
"max_tokens": 512,
"system": "Responda em pt-BR, direto ao ponto.",
"messages": [
{ "role": "user", "content": "Explique entelequia em tres linhas." }
]
}'const res = await fetch('https://api.entelecy.ai/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}`,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'loom-flash',
max_tokens: 512,
system: 'Responda em pt-BR, direto ao ponto.',
messages: [
{ role: 'user', content: 'Explique entelequia em tres linhas.' },
],
}),
});
const data = await res.json();
console.log(data.content[0].text);import os, requests
res = requests.post(
"https://api.entelecy.ai/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}",
"anthropic-version": "2023-06-01",
},
json={
"model": "loom-flash",
"max_tokens": 512,
"system": "Responda em pt-BR, direto ao ponto.",
"messages": [
{"role": "user", "content": "Explique entelequia em tres linhas."},
],
},
timeout=(10, 600),
)
res.raise_for_status()
print(res.json()["content"][0]["text"])POST/v1/responsessuporta streaming (SSE)Formato Responses. É o que o Codex fala: ele não conversa em Chat Completions com provedor externo, e exige wire_api = "responses". O corpo segue o padrão da Responses API.
Use loom-flash nesta rota — é o nome que ela atende. Nas outras duas rotas de conversa, loom-pro também está disponível.
| Campo | Tipo | Descrição |
|---|---|---|
| modelobrigatório | string | loom-flash. |
| input | string / array | O que enviar ao modelo: um texto solto ou a lista de itens da conversa. Envie input, instructions, ou os dois. |
| instructions | string | Instrução de sistema, como campo da raiz. |
| stream | boolean | true troca a resposta por um stream SSE de eventos. Padrão false. |
| reasoning | object | Eixo de pensamento do formato Responses — é aqui que vai o effort deste protocolo. |
| max_output_tokens | integer | Teto de tokens gerados na resposta. |
| tools | array | Ferramentas disponíveis ao modelo, dos tipos function e web_search. |
A resposta
O corpo vem dentro de um envelope response — é a diferença de forma que mais pega quem vem do Chat Completions:
| Campo | Tipo | Descrição |
|---|---|---|
| response | object | Envelope que embrulha a resposta inteira. É dentro dele que vivem model, usage e a saída. |
| response.model | string | O mesmo nome que você pediu, aninhado no envelope. |
| response.output_text | string | O texto gerado. |
| response.usage | object | Consumo da chamada, com input_tokens, output_tokens e o cacheado em input_tokens_details.cached_tokens. |
Cada chamada é independente: store volta sempre false e previous_response_id volta sempre null. Para encadear turnos, mande o histórico no input.
Fim do stream
Com stream: true, o fluxo termina num evento terminal — e não em [DONE]. Trate os três:
response.completed— terminou normalmente, e é neste evento que ousagevem completo.response.incomplete— parou antes do fim, por exemplo ao bater o teto de tokens.response.failed— falhou, com o detalhe emerror.
Três diferenças em relação ao /v1/chat/completions que aparecem no seu código:
- O consumo tem outros nomes:
input_tokenseoutput_tokens, com o cacheado dentro deinput_tokens_details. - O stream não termina em
[DONE]: termina num evento terminal —response.completed,response.incompleteouresponse.failed. O consumo vem nocompleted, sem precisar pedir. - O
modelda resposta vem aninhado emresponse.model, tanto no corpo inteiro quanto em cada evento do stream. O nome que volta continua sendo o público que você pediu.
Requisição
curl https://api.entelecy.ai/v1/responses \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "loom-flash",
"input": "Explique entelequia em tres linhas."
}'const res = await fetch('https://api.entelecy.ai/v1/responses', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'loom-flash',
input: 'Explique entelequia em tres linhas.',
}),
});
const data = await res.json();
// o usage aqui e input_tokens / output_tokens, nao prompt_tokens
console.log(data.usage.input_tokens, data.usage.output_tokens);import os, requests
res = requests.post(
"https://api.entelecy.ai/v1/responses",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
json={
"model": "loom-flash",
"input": "Explique entelequia em tres linhas.",
},
timeout=(10, 600),
)
res.raise_for_status()
# o model da resposta vive aninhado em response.model
print(res.json())Resposta200
{
"usage": {
"input_tokens": 812,
"input_tokens_details": { "cached_tokens": 640 },
"output_tokens": 214
}
}effort vale só em /v1/chat/completions. Nas outras duas rotas ele não tem efeito. Para controlar o pensamento ali, use o eixo do próprio protocolo: thinking no formato Anthropic, reasoning.effort no Responses.
Imagem
Geração e edição de imagem no formato de mercado, com validação de envelope no gateway. O retorno traz a imagem em base64 e o consumo em tokens da chamada.
POST/v1/images/generations| Campo | Tipo | Descrição |
|---|---|---|
| modelobrigatório | string | Nome público do modelo de imagem: loom-image. Ver Modelos. |
| promptobrigatório | string | Descrição do que gerar. Prompts específicos rendem mais que adjetivos empilhados. |
| size | string | LARGURAxALTURA em pixels, ou auto. Não há lista fixa: vale o que passar nas regras do envelope, abaixo. Válidos, por exemplo: 1024x1024, 1536x1024, 1024x1536, 1920x1088, 3840x2160. |
| quality | string | low, medium, high ou auto. Qualidade maior gasta mais tokens de saída, e a cobrança acompanha. |
| background | string | auto, opaque ou transparent. |
| n | integer | Quantidade de imagens por chamada, de 1 a 10. Cada uma é gerada e cobrada à parte. |
| output_format | string | png, jpeg ou webp. Omitido, sai em png. jpeg é mais rápido — vale a troca quando a latência pesa. |
| output_compression | integer | Nível de compressão, de 0 a 100. Vale para jpeg e webp. |
Envelope validado no gateway
Não há lista fixa de tamanhos. Vale qualquer um que satisfaça as quatro regras abaixo, conferidas antes de a chamada sair — erro de envelope volta em milissegundos e custa zero. Omitir size, ou mandar auto, pula a checagem e deixa o padrão decidir.
- Os dois lados precisam ser múltiplos de 16.
- A área total fica entre 655.360 e 8.294.400 pixels, e a maior aresta não passa de 3840.
- A proporção entre os lados não passa de 3:1.
- Fundo transparente pedido a um modelo sem suporte é reencaminhado ao modelo alternativo — e a cobrança segue o modelo efetivamente usado.
A resposta
A imagem volta em base64 no corpo, não como URL para baixar depois:
| Campo | Tipo | Descrição |
|---|---|---|
| created | integer | Momento da criação, em segundos Unix. |
| data | array | Lista com as imagens geradas. Cada item traz b64_json, a imagem em base64 — decodifique e grave. |
| usage | object | Consumo da chamada: input_tokens (o prompt), output_tokens (a imagem) e total_tokens. É daqui que sai a cobrança. |
| …cached_tokens | integer | Em input_tokens_details.cached_tokens, a parte da entrada que bateu no cache. |
Para 16:9, use 1920x1088 ou 3840x2160 — em Full HD o 1088 entra no lugar do 1080, que não é múltiplo de 16. Para quadrado, comece em 1024x1024: é o menor múltiplo de 16 que passa da área mínima. A mensagem do 400 sempre diz qual regra pegou e com que número.
Requisição
curl https://api.entelecy.ai/v1/images/generations \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "loom-image",
"prompt": "Fachada de uma padaria de bairro ao amanhecer, luz quente, fotografia",
"size": "1536x1024",
"quality": "high",
"n": 1
}'import { writeFile } from 'node:fs/promises';
const res = await fetch('https://api.entelecy.ai/v1/images/generations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'loom-image',
prompt: 'Fachada de uma padaria de bairro ao amanhecer, luz quente, fotografia',
size: '1536x1024',
quality: 'high',
n: 1,
}),
});
const body = await res.json();
await writeFile('saida.png', Buffer.from(body.data[0].b64_json, 'base64'));import base64, os, requests
res = requests.post(
"https://api.entelecy.ai/v1/images/generations",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
json={
"model": "loom-image",
"prompt": "Fachada de uma padaria de bairro ao amanhecer, luz quente, fotografia",
"size": "1536x1024",
"quality": "high",
"n": 1,
},
timeout=(10, 300),
)
res.raise_for_status()
with open("saida.png", "wb") as f:
f.write(base64.b64decode(res.json()["data"][0]["b64_json"]))Resposta200
{
"created": 1785969142,
"data": [{ "b64_json": "iVBORw0KGgoAAAANSUhEUg…" }],
"usage": {
"input_tokens": 42,
"input_tokens_details": { "cached_tokens": 0 },
"output_tokens": 1568,
"total_tokens": 1610
}
}POST/v1/images/editsA edição recebe JSON: a imagem-base (uma ou mais) e a máscara opcional em base64. O gateway converte para o formato multipart que o fornecedor espera, então você não precisa montar o upload.
| Campo | Tipo | Descrição |
|---|---|---|
| modelobrigatório | string | Nome público do modelo de imagem: loom-image. Ver Modelos. |
| promptobrigatório | string | O que mudar na imagem enviada. Descreva a alteração, não a cena inteira. |
| imageobrigatório | array | Imagem-base em base64. Aceita mais de uma; a primeira é a que a máscara acompanha. |
| mask | string | Máscara em base64, opcional. A área transparente é a que pode mudar. |
| size | string | LARGURAxALTURA em pixels, ou auto. Não há lista fixa: vale o que passar nas regras do envelope, abaixo. Válidos, por exemplo: 1024x1024, 1536x1024, 1024x1536, 1920x1088, 3840x2160. |
A máscara marca o que pode mudar: a área transparente é a editável. Envie máscara e imagem-base com as mesmas dimensões.
Requisição
{
"model": "loom-image",
"prompt": "Troque o fundo por um ceu limpo no fim da tarde",
"image": ["iVBORw0KGgo…"],
"mask": "iVBORw0KGgo…",
"size": "1024x1024"
}import { readFile } from 'node:fs/promises';
const b64 = async p => (await readFile(p)).toString('base64');
const res = await fetch('https://api.entelecy.ai/v1/images/edits', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'loom-image',
prompt: 'Troque o fundo por um ceu limpo no fim da tarde',
image: [await b64('base.png')],
mask: await b64('mascara.png'), // area transparente = o que pode mudar
size: '1024x1024',
}),
});import base64, os, requests
def b64(caminho):
with open(caminho, "rb") as f:
return base64.b64encode(f.read()).decode()
res = requests.post(
"https://api.entelecy.ai/v1/images/edits",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
json={
"model": "loom-image",
"prompt": "Troque o fundo por um ceu limpo no fim da tarde",
"image": [b64("base.png")],
"mask": b64("mascara.png"), # area transparente = o que pode mudar
"size": "1024x1024",
},
timeout=(10, 300),
)
res.raise_for_status()Áudio
Dois caminhos: transcrever fala em texto e sintetizar texto em voz. A escolha do motor de transcrição é feita pelo campo model; sem ele, vale o padrão do servidor.
POST/v1/audio/transcriptionsmultipart/form-data| Campo | Tipo | Descrição |
|---|---|---|
| fileobrigatório | file | Arquivo de áudio: m4a, mp3, wav, ogg ou webm. |
| model | string | loom-audio. Pode omitir — a rota já sabe que aqui é fala→texto. É o mesmo nome da voz: quem decide a direção é o endpoint. |
| language | string | Código ISO-639 do idioma falado: pt, en, es. Omitido, o idioma é detectado no áudio. |
A resposta é texto puro (text/plain), não JSON — é o contrato que os clientes da Entelecy já consomem. A duração do áudio, medida na transcrição, é a base da cobrança.
Requisição
curl https://api.entelecy.ai/v1/audio/transcriptions \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-F "[email protected]" \
-F "model=loom-audio" \
-F "language=pt"import { openAsBlob } from 'node:fs';
const form = new FormData();
form.set('file', await openAsBlob('reuniao.m4a'), 'reuniao.m4a');
form.set('model', 'loom-audio');
form.set('language', 'pt');
const res = await fetch('https://api.entelecy.ai/v1/audio/transcriptions', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}` },
body: form,
});
// a resposta e texto puro, nao JSON
console.log(await res.text());import os, requests
with open("reuniao.m4a", "rb") as f:
res = requests.post(
"https://api.entelecy.ai/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
files={"file": ("reuniao.m4a", f, "audio/m4a")},
data={"model": "loom-audio", "language": "pt"},
timeout=(10, 300),
)
res.raise_for_status()
print(res.text) # texto puro, nao JSONResposta200 · text/plain
Bom dia. Comecando a reuniao de quinta…POST/v1/audio/speechresponde bytes de áudio| Campo | Tipo | Descrição |
|---|---|---|
| inputobrigatório | string | Texto a ser falado, de até 5.000 caracteres por chamada. Para textos maiores, divida em partes e junte os áudios — cada parte é uma chamada. |
| voice | string | Identificador da voz, tirado de /v1/audio/voices. Omitido, sai na voz padrão da conta. |
| model | string | loom-audio. Pode omitir — a rota já sabe qual motor usar. Ver Modelos. |
| response_format | string | mp3, atalho para o padrão, ou um formato no padrão codec_taxa_bitrate. Omitido, sai em mp3_44100_128. |
| voice_settings | object | Ajuste fino da voz. Campos e padrões: stability (0.5) — quanto menor, mais variação emocional; similarity_boost (0.75) — fidelidade à voz original; style (0) — exagero do estilo; use_speaker_boost (true); speed (1.0) — de 0.7 a 1.2, abaixo de 1 fica mais lento, acima mais rápido. |
Formatos de áudio
O nome do formato segue codec_taxa_bitrate — mp3_44100_128 é MP3 a 44,1 kHz e 128 kbps. Os codecs disponíveis:
mp3_22050_32,mp3_44100_32,mp3_44100_64,mp3_44100_96,mp3_44100_128,mp3_44100_192opus_48000_32,opus_48000_64,opus_48000_96,opus_48000_128,opus_48000_192pcm_8000,pcm_16000,pcm_22050,pcm_24000,pcm_32000,pcm_44100,pcm_48000wav_8000,wav_16000,wav_22050,wav_24000,wav_32000,wav_44100,wav_48000ulaw_8000ealaw_8000— os formatos de telefonia, usados por centrais como a Twilio.
Requisição
curl https://api.entelecy.ai/v1/audio/speech \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "loom-audio",
"input": "A entrega de quinta esta confirmada. Qualquer mudanca, aviso por aqui.",
"response_format": "mp3_44100_128"
}' \
--output aviso.mp3import { writeFile } from 'node:fs/promises';
const res = await fetch('https://api.entelecy.ai/v1/audio/speech', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'loom-audio',
input: 'A entrega de quinta esta confirmada. Qualquer mudanca, aviso por aqui.',
response_format: 'mp3_44100_128',
}),
});
// a resposta e o audio em bytes, nao JSON
await writeFile('aviso.mp3', Buffer.from(await res.arrayBuffer()));import os, requests
res = requests.post(
"https://api.entelecy.ai/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
json={
"model": "loom-audio",
"input": "A entrega de quinta esta confirmada. Qualquer mudanca, aviso por aqui.",
"response_format": "mp3_44100_128",
},
timeout=(10, 300),
)
res.raise_for_status()
with open("aviso.mp3", "wb") as f:
f.write(res.content) # bytes de audio, nao JSONGET/v1/audio/voicessem cobrançaLista as vozes disponíveis na conta, incluindo as prontas do fornecedor. Exige autenticação, não gera cobrança e é a origem correta dos identificadores usados em voice.
O catálogo é repassado como vem do fornecedor — o gateway não reescreve o corpo. Por isso esta página não fixa um formato aqui: leia a lista da resposta e use o identificador que ela traz. É a mesma razão de o campo voice não ter lista fechada na tabela acima.
Requisição
curl https://api.entelecy.ai/v1/audio/voices \
-H "Authorization: Bearer $ENTELECY_API_KEY"const res = await fetch('https://api.entelecy.ai/v1/audio/voices', {
headers: { 'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}` },
});
const catalogo = await res.json();import os, requests
res = requests.get(
"https://api.entelecy.ai/v1/audio/voices",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
timeout=(10, 60),
)
res.raise_for_status()
catalogo = res.json()Vídeo
Geração de vídeo é assíncrona: você submete o pedido, recebe um identificador e consulta até o estado ficar terminal. Vídeo a partir de imagem usa uma mídia de referência enviada antes.
POST/v1/videos/generationsassíncrono (submit + poll)| Campo | Tipo | Descrição |
|---|---|---|
| model | string | Nome público do modelo de vídeo: loom-video. Opcional — omitido, o modo é escolhido pela forma do pedido (ver Modos, abaixo). |
| promptobrigatório | string | Descrição da cena, do movimento de câmera e do ritmo. |
| durationobrigatório | integer | Duração em segundos, de 4 a 15. Sempre envie um número: é ele que define a cobrança. |
| resolution | string | 480p ou 720p. Omitida, vale o padrão do modelo. |
| ratio | string | Proporção do quadro: 16:9, 9:16, 4:3, 3:4, 1:1, 21:9 ou adaptive. Omitida, vale adaptive — o modelo escolhe pelo conteúdo. |
| generate_audio | boolean | true gera trilha junto do vídeo, false devolve mudo. Omitido, vem com áudio — o padrão do modelo é true. |
| bitrate_mode | string | standard ou high. Omitido, vale standard. |
| watermark | boolean | true marca o vídeo. Omitido, vale false. |
| return_last_frame | boolean | true devolve também o último quadro, útil para emendar um clipe no seguinte. Omitido, vale false. |
| image_url | string | URL de uma imagem para animar. A presença deste campo é o que liga o modo imagem→vídeo. |
| image_urls | array | URLs de imagens de referência — personagem, objeto ou estilo a manter na cena. |
| video_urls | array | URLs de vídeos de referência, para movimento ou continuidade. |
| audio_urls | array | URLs de áudio de referência, quando a geração deve acompanhar uma trilha. |
Modos
A rota é uma só e atende três caminhos. Quem escolhe é a forma do corpo, não um campo de modo — por isso model pode ficar de fora:
| Modo | O que dispara | Descrição |
|---|---|---|
| texto → vídeo | nenhum dos outros | Só prompt. A cena nasce inteira da descrição. |
| imagem → vídeo | image_url | Anima uma imagem que já existe. A URL vem de /v1/videos/uploads ou de um endereço público. |
| referência → vídeo | image_urls video_urls audio_urls | Gera mantendo elementos de mídias que você fornece — personagem, estilo, movimento ou trilha. |
Referência ganha de imagem simples: se o corpo trouxer image_urls, video_urls ou audio_urls, o pedido vai por esse caminho mesmo que também tenha um image_url solto. Mandar os dois não combina os modos — escolha um.
A cobrança é armada na submissão e disparada na primeira consulta que vê o vídeo pronto. Consultar várias vezes não cobra em duplicidade; se o pedido falhar, não há débito.
Requisição
# 1) submit — devolve o id da predicao
curl https://api.entelecy.ai/v1/videos/generations \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Plano aereo de uma feira livre ao amanhecer, camera avancando devagar",
"duration": 6,
"resolution": "720p"
}'
# 2) poll — ate status completed
curl https://api.entelecy.ai/v1/videos/generations/pred_01J8Z… \
-H "Authorization: Bearer $ENTELECY_API_KEY"const KEY = process.env.ENTELECY_API_KEY;
const BASE = 'https://api.entelecy.ai';
const sub = await fetch(`${BASE}/v1/videos/generations`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' },
// sem `model`: o modo sai da forma do corpo — so prompt = texto para video
body: JSON.stringify({
prompt: 'Plano aereo de uma feira livre ao amanhecer, camera avancando devagar',
duration: 6,
resolution: '720p',
}),
});
const { data: { id } } = await sub.json();
// poll ate um estado terminal
let job;
do {
await new Promise(r => setTimeout(r, 5000));
const res = await fetch(`${BASE}/v1/videos/generations/${id}`, {
headers: { 'Authorization': `Bearer ${KEY}` },
});
({ data: job } = await res.json());
} while (job.status === 'queued' || job.status === 'processing');
console.log(job.outputs?.[0]?.url);import os, time, requests
KEY = os.environ["ENTELECY_API_KEY"]
BASE = "https://api.entelecy.ai"
H = {"Authorization": f"Bearer {KEY}"}
# sem 'model': o modo sai da forma do corpo — so prompt = texto para video
sub = requests.post(
f"{BASE}/v1/videos/generations",
headers=H,
json={
"prompt": "Plano aereo de uma feira livre ao amanhecer, camera avancando devagar",
"duration": 6,
"resolution": "720p",
},
timeout=(10, 120),
)
sub.raise_for_status()
pred = sub.json()["data"]["id"]
while True:
time.sleep(5)
job = requests.get(f"{BASE}/v1/videos/generations/{pred}", headers=H, timeout=(10, 60))
job.raise_for_status()
estado = job.json()["data"]
if estado["status"] not in ("queued", "processing"):
break
print(estado.get("outputs", [{}])[0].get("url"))Resposta200
// 1) submit
{ "data": { "id": "pred_01J8Z…", "status": "queued" } }
// 2) poll, ja pronto
{ "data": { "status": "completed", "outputs": [{ "url": "https://…/video.mp4" }] } }GET/v1/videos/generations/{id}sem cobrançaO submit devolve um identificador; esta rota diz em que pé está. Consulte até chegar a um estado terminal — a consulta em si não é cobrada.
| Campo | Tipo | Descrição |
|---|---|---|
| data.id | string | Identificador da geração, o mesmo que vai no caminho da consulta. Vem em data.id. |
| data.status | string | Estado atual. Pronto é completed ou succeeded — trate os dois como fim de sucesso. |
| data.outputs | array | Lista com a mídia gerada; a URL do vídeo está em outputs[].url. |
É a primeira consulta que vê o estado terminal que dispara a cobrança dos segundos pedidos no submit. Consultar de novo depois disso não cobra outra vez.
Resposta200
{
"data": {
"id": "pred_01J8Z…",
"status": "completed",
"outputs": [{ "url": "https://…/video.mp4" }]
}
}POST/v1/videos/uploadsmultipart/form-dataEnvia a mídia de referência e devolve a URL que vai em image_url (ou nos campos de referência) no corpo da geração. É multipart/form-data com um campo só, e não gera cobrança.
| Campo | Tipo | Descrição |
|---|---|---|
| fileobrigatório | file | A mídia a enviar. É o único campo do formulário. |
Requisição
curl https://api.entelecy.ai/v1/videos/uploads \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-F "[email protected]"
# a URL devolvida aqui e a que vai em image_url no submit da geracaoimport { openAsBlob } from 'node:fs';
const form = new FormData();
form.set('file', await openAsBlob('referencia.png'), 'referencia.png');
const res = await fetch('https://api.entelecy.ai/v1/videos/uploads', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}` },
body: form,
});
// esta URL vai em image_url no submit da geracao
const { url } = await res.json();import os, requests
with open("referencia.png", "rb") as f:
res = requests.post(
"https://api.entelecy.ai/v1/videos/uploads",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
files={"file": ("referencia.png", f, "image/png")},
timeout=(10, 120),
)
res.raise_for_status()
# esta URL vai em image_url no submit da geracao
url = res.json()Busca
Busca na web com resultados estruturados — pensado para dar contexto atual a um agente antes de ele responder. O corpo é repassado ao mecanismo, então parâmetros de região, idioma e quantidade funcionam como na origem.
POST/v1/search| Campo | Tipo | Descrição |
|---|---|---|
| qobrigatório | string | A consulta. É o único campo que a busca pede sempre. |
| gl | string | País dos resultados, em código de duas letras: br, us, pt. Muda o que conta como local. |
| hl | string | Idioma dos resultados, em código de duas letras: pt, en, es. |
| num | integer | Quantidade de resultados. Omitido, vêm 10. |
| page | integer | Página dos resultados, para paginar a partir da primeira. |
A resposta chega como veio do mecanismo: bloco de resposta direta quando existe, resultados orgânicos, painéis de conhecimento e afins. A cobrança é por chamada bem-sucedida, independentemente do número de resultados.
Requisição
curl https://api.entelecy.ai/v1/search \
-H "Authorization: Bearer $ENTELECY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "relatorio anual industria de embalagens brasil",
"gl": "br",
"hl": "pt",
"num": 10
}'const res = await fetch('https://api.entelecy.ai/v1/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
q: 'relatorio anual industria de embalagens brasil',
gl: 'br',
hl: 'pt',
num: 10,
}),
});
const { organic } = await res.json();import os, requests
res = requests.post(
"https://api.entelecy.ai/v1/search",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
json={
"q": "relatorio anual industria de embalagens brasil",
"gl": "br",
"hl": "pt",
"num": 10,
},
timeout=(10, 60),
)
res.raise_for_status()
resultados = res.json()Modelos
O campo model recebe um nome público da Entelecy. Ele descreve a capacidade que você quer, e vale nas rotas listadas ao lado dele:
| model | Endpoint | Descrição |
|---|---|---|
| loom-flash | /v1/chat/completions /v1/messages /v1/responses | Texto e imagem, rápido e barato para volume. Aceita effort. |
| loom-pro | /v1/chat/completions /v1/messages | Texto e imagem, com mais capacidade para trabalho difícil. Aceita effort. |
| loom-image | /v1/images/generations /v1/images/edits | Geração de imagem. Fundo transparente é resolvido pelo gateway, sem você trocar de nome. |
| loom-audio | /v1/audio/speech | Síntese de voz com prosódia natural em português. |
| loom-audio | /v1/audio/transcriptions | Transcrição de áudio. Mesmo nome da voz: a rota decide a direção. |
| loom-video | /v1/videos/generations | Geração de vídeo curto, a partir de texto ou de uma imagem de referência. |
GET/v1/modelssem cobrançaOs nomes de texto e imagem da tabela acima, servidos pela API: se um nome aparece aqui, ele funciona nas rotas listadas junto dele. O catálogo não lista loom-audio nem loom-video — eles valem nas rotas de áudio e vídeo do mesmo jeito, mas não passam por este registro. Exige autenticação e não gera cobrança.
É por esta rota que o Codex valida o provedor: ele chama GET {base_url}/models e só aceita 2xx, 401 ou 403 antes de tentar qualquer turno.
Requisição
curl https://api.entelecy.ai/v1/models \
-H "Authorization: Bearer $ENTELECY_API_KEY"const res = await fetch('https://api.entelecy.ai/v1/models', {
headers: { 'Authorization': `Bearer ${process.env.ENTELECY_API_KEY}` },
});
const { data } = await res.json();
for (const m of data) console.log(m.id, m.endpoints.join(' '));import os, requests
res = requests.get(
"https://api.entelecy.ai/v1/models",
headers={"Authorization": f"Bearer {os.environ['ENTELECY_API_KEY']}"},
timeout=(10, 60),
)
res.raise_for_status()
for m in res.json()["data"]:
print(m["id"], m["endpoints"])Resposta200
{
"object": "list",
"data": [
{
"id": "loom-flash",
"object": "model",
"owned_by": "entelecy",
"endpoints": ["/v1/chat/completions", "/v1/messages", "/v1/responses"]
},
{
"id": "loom-image",
"object": "model",
"owned_by": "entelecy",
"endpoints": ["/v1/images/edits", "/v1/images/generations"]
}
]
}Como escolhemos
Não somos fiéis a um fornecedor. Cada segmento é um campo em movimento — modelo novo todo mês, preço caindo, capacidade mudando — e o que faz sentido hoje pode não fazer no trimestre que vem. Acompanhamos as famílias relevantes de cada um, medimos no nosso próprio material e trocamos quando compensa.
O critério, em ordem:
- Qualidade no português do Brasil. Avaliamos em conteúdo real de clientes, não em benchmark traduzido.
- Custo por resultado aceito. Não o preço por token: o preço da resposta que passou na revisão.
- Latência previsível. Modelo bom que oscila de 3 a 40 segundos não serve para produto interativo.
- Estabilidade de contrato. Fornecedor que muda formato sem aviso custa caro no longo prazo.
Agentes
Raciocínio, código e ferramentas
Conversa, geração e extração de texto, leitura de imagens e documentos, e o ciclo de agente com ferramentas. É onde a diferença entre um modelo e outro mais aparece — e onde a escala de esforço rende mais.
Famílias acompanhadas no segmento
Imagem
Geração e edição
Peça de marca, cena fotográfica, ilustração, edição com máscara e fundo transparente. Avaliamos aderência ao prompt, tipografia legível dentro da imagem e consistência entre variações.
Famílias acompanhadas no segmento
Voz
Síntese de fala
Narração longa, resposta curta interativa e leitura expressiva. O corte aqui é a naturalidade em português do Brasil — a maioria das opções ainda soa traduzida — junto com latência e controle de estilo.
Famílias acompanhadas no segmento
Transcrição
Fala para texto
Reunião, ditado, áudio de campo com ruído. Pesam acurácia em português, marcação de tempo por palavra, separação de interlocutores e o custo por hora de áudio.
Famílias acompanhadas no segmento
Vídeo
Texto para vídeo e imagem para vídeo
Clipe curto para peça de conteúdo, animação de uma imagem existente, movimento de câmera dirigido. Coerência temporal, aderência ao prompt e custo por segundo mandam na escolha.
Famílias acompanhadas no segmento
Busca
Web com resultado estruturado
Contexto atual para um agente responder sem alucinar: resultado orgânico, resposta direta e painéis, em JSON limpo. Latência baixa importa mais que volume — o agente busca várias vezes por tarefa.
Famílias acompanhadas no segmento
Isto é o panorama que acompanhamos em cada segmento, não um catálogo de disponibilidade. O que está ativo por trás de cada nome público é curadoria nossa e muda quando aparece coisa melhor — sem quebrar quem integrou, porque o nome não muda junto.
Cobrança
Tudo é cobrado em créditos, na carteira do dono da credencial. Cada capacidade tem a unidade que corresponde ao trabalho real:
| Endpoint | Unidade cobrada | De onde sai a medida |
|---|---|---|
| /v1/chat/completions /v1/messages | tokens | usage da resposta, com entrada, saída e cache separados. |
| /v1/responses | tokens | usage da resposta, com os nomes do formato Responses: input_tokens, output_tokens e o cacheado dentro de input_tokens_details. |
| /v1/images/* | tokens | usage da resposta, incluindo os tokens de imagem gerados. |
| /v1/audio/transcriptions | segundos de áudio | Duração do áudio medida na transcrição, arredondada para cima. |
| /v1/audio/speech | caracteres | Quantidade de caracteres do texto enviado. |
| /v1/videos/generations | segundos gerados | Duração pedida na submissão, cobrada quando o vídeo fica pronto. |
| /v1/search | por chamada | Uma unidade por chamada bem-sucedida. |
Antes de encaminhar, o gateway confere um saldo mínimo para a operação — maior em imagem e vídeo, que custam mais. Sem saldo, a resposta é 402 com quanto falta e o link para recarregar, e nada é gasto com o fornecedor.
O débito acontece depois da resposta, com o consumo real, e é idempotente por chamada: uma retentativa de rede não cobra duas vezes. Chamadas que falham no fornecedor não geram débito.
Se o saldo acabar entre a checagem inicial e o débito, a resposta já foi entregue e o débito é registrado mesmo assim. É a única situação em que a carteira pode ficar negativa — a próxima chamada volta em 402.
Exemplos de implementação
Código pronto para colar, com os mesmos modelos que o Loom — nosso próprio produto — roda em produção, e com o tratamento que costuma faltar: erro de saldo, chunk partido no meio do stream e leitura do consumo no fim da chamada.
Chat com streaming
Sem dependência: fetch e TextDecoder nativos do Node 18+. O parser guarda a sobra do buffer porque um chunk de rede pode cortar uma linha SSE ao meio.
const BASE = 'https://api.entelecy.ai';
const KEY = process.env.ENTELECY_API_KEY;
/**
* Chat com streaming. Devolve o texto completo e o usage do ultimo chunk.
* O gateway sempre pede usage no fim do stream — nao e preciso configurar nada.
*/
export async function chatStream(messages, { effort = 'high', onDelta } = {}) {
const res = await fetch(`${BASE}/v1/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${KEY}`,
'Content-Type': 'application/json',
},
// model e effort sao eixos separados: o nome escolhe a faixa, o effort a profundidade
body: JSON.stringify({ model: 'loom-flash', effort, stream: true, messages }),
});
if (!res.ok) {
// 402 = saldo insuficiente; o corpo traz balance/required/upgrade_url
const err = await res.json().catch(() => ({}));
throw new Error(`${res.status} ${err?.error?.type ?? 'erro'}: ${err?.error?.message ?? ''}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '', text = '', usage = null;
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE: eventos separados por linha; so nos importam as linhas "data: "
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const payload = line.slice(6).trim();
if (payload === '[DONE]') continue;
let chunk;
try { chunk = JSON.parse(payload); } catch { continue; } // chunk partido
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) { text += delta; onDelta?.(delta); }
if (chunk.usage) usage = chunk.usage; // ultimo chunk
}
}
return { text, usage };
}Gerar imagem e salvar em disco
import { writeFile } from 'node:fs/promises';
export async function gerarImagem(prompt, { size = '1024x1024' } = {}) {
const res = await fetch(`${BASE}/v1/images/generations`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'loom-image', prompt, size, n: 1 }),
});
const body = await res.json();
if (!res.ok) {
// gateway_invalid_size chega aqui ANTES de custar credito
throw new Error(`${body.error?.code ?? res.status}: ${body.error?.message}`);
}
await writeFile('saida.png', Buffer.from(body.data[0].b64_json, 'base64'));
return body.usage;
}Chat com streaming
Com requests. Note o timeout em par: conexão curta, leitura longa — geração com raciocínio pode passar de um minuto.
import json, os, requests
BASE = "https://api.entelecy.ai"
KEY = os.environ["ENTELECY_API_KEY"]
def chat_stream(messages, effort: str = "high"):
"""Chat com streaming. Retorna (texto, usage)."""
with requests.post(
f"{BASE}/v1/chat/completions",
headers={
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json",
},
json={"model": "loom-flash", "effort": effort, "stream": True, "messages": messages},
stream=True,
timeout=(10, 600), # conexao curta, leitura longa
) as res:
if res.status_code == 402:
raise RuntimeError(f"saldo insuficiente: {res.json()['error']}")
res.raise_for_status()
texto, usage = [], None
for raw in res.iter_lines(decode_unicode=True):
if not raw or not raw.startswith("data: "):
continue
payload = raw[6:].strip()
if payload == "[DONE]":
break
chunk = json.loads(payload)
if chunk.get("choices"):
delta = chunk["choices"][0].get("delta", {}).get("content")
if delta:
texto.append(delta)
print(delta, end="", flush=True)
if chunk.get("usage"):
usage = chunk["usage"]
return "".join(texto), usageTranscrever um arquivo
def transcrever(caminho: str, language: str = "pt") -> str:
with open(caminho, "rb") as f:
res = requests.post(
f"{BASE}/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {KEY}"},
files={"file": (os.path.basename(caminho), f, "audio/m4a")},
data={"model": "loom-audio", "language": language},
timeout=(10, 300),
)
res.raise_for_status()
return res.text # o endpoint devolve texto puro, nao JSONChat com streaming
Com HttpClient e System.Text.Json, lendo o stream conforme ele chega em vez de esperar o corpo inteiro.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
public sealed class EntelecyClient(HttpClient http, string apiKey)
{
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
/// <summary>Chat com streaming: entrega cada delta no callback e devolve o usage final.</summary>
public async Task<JsonElement?> ChatStreamAsync(
object[] messages, Action<string> onDelta, string effort = "high", CancellationToken ct = default)
{
var body = JsonSerializer.Serialize(new { model = "loom-flash", effort, stream = true, messages });
using var req = new HttpRequestMessage(HttpMethod.Post, "/v1/chat/completions")
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct);
if (!res.IsSuccessStatusCode)
throw new InvalidOperationException(
$"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync(ct)}");
await using var stream = await res.Content.ReadAsStreamAsync(ct);
using var reader = new StreamReader(stream);
JsonElement? usage = null;
while (await reader.ReadLineAsync(ct) is { } line)
{
if (!line.StartsWith("data: ", StringComparison.Ordinal)) continue;
var payload = line[6..].Trim();
if (payload == "[DONE]") break;
JsonDocument doc;
try { doc = JsonDocument.Parse(payload); } catch (JsonException) { continue; }
using (doc)
{
if (doc.RootElement.TryGetProperty("choices", out var choices)
&& choices.GetArrayLength() > 0
&& choices[0].TryGetProperty("delta", out var delta)
&& delta.TryGetProperty("content", out var content))
{
onDelta(content.GetString() ?? "");
}
if (doc.RootElement.TryGetProperty("usage", out var u))
usage = u.Clone();
}
}
return usage;
}
}Integrações
A API é compatível com os formatos que as CLIs de código já falam — dá para apontar a ferramenta que você usa para cá sem adaptador no meio. Abaixo, o que está testado.
Claude Code
Fala o formato Anthropic (/v1/messages), que a API expõe. Duas variáveis apontam a CLI para cá; as outras duas escolhem o modelo, porque o seletor do /model lista só os modelos nativos dele.
export ANTHROPIC_BASE_URL=https://api.entelecy.ai
export ANTHROPIC_AUTH_TOKEN=$ENTELECY_API_KEY
# O modelo principal e o auxiliar (tarefas de fundo) vem por variavel:
# o seletor do /model lista so os modelos nativos dele.
export ANTHROPIC_MODEL=loom-flash
export ANTHROPIC_DEFAULT_HAIKU_MODEL=loom-flash
claudeAntes de abrir a CLI, confirme a chave e o endereço com uma requisição de um token. Uma resposta que começa em {"id":"msg_ prova que os dois estão certos:
curl -X POST "$ANTHROPIC_BASE_URL/v1/messages" \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"loom-flash","max_tokens":1,"messages":[{"role":"user","content":"."}]}'OpenCode
Usa o pacote @ai-sdk/openai-compatible sobre /v1/chat/completions. Coloque o bloco abaixo no seu opencode.json e exporte ENTELECY_API_KEY:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"entelecy": {
"npm": "@ai-sdk/openai-compatible",
"name": "Entelecy",
"options": {
"baseURL": "https://api.entelecy.ai/v1",
"apiKey": "{env:ENTELECY_API_KEY}"
},
"models": {
"loom-flash": { "name": "Loom Flash" }
}
}
}
}Codex
Fala a API de Responses (/v1/responses). O perfil vai num arquivo à parte, para o seu config.toml do dia a dia não ser tocado — no Codex 0.142.0 a tabela [profiles.x] virou legado. Salve como ~/.codex/entelecy.config.toml:
# ~/.codex/entelecy.config.toml (Windows: C:\Users\<user>\.codex\...)
# Arquivo A PARTE: seu config.toml do dia a dia nao e tocado.
# ORDEM IMPORTA: em TOML, chave escrita depois de um [cabecalho] pertence
# aquela tabela; por isso as de raiz vem primeiro.
model = "loom-flash"
model_provider = "entelecy"
model_context_window = 128000
model_catalog_json = 'C:\Users\<user>\.codex\entelecy_models.json'
forced_login_method = "api"
[model_providers.entelecy]
name = "Entelecy"
base_url = "https://api.entelecy.ai/v1" # COM /v1: o Codex monta as rotas a partir daqui
env_key = "ENTELECY_API_KEY"
wire_api = "responses" # obrigatorio; o valor "chat" foi removido do Codex
supports_websockets = false # o gateway fala HTTP/SSE, nao WebSocket
stream_idle_timeout_ms = 360000 # turno longo streama muito tempo sem byte novoBaixe o catálogo de modelos para ~/.codex/entelecy_models.json (sem ele o Codex não sabe a janela de contexto nem os níveis de raciocínio), exporte a chave e chame o perfil:
setx ENTELECY_API_KEY "kriou_live_..." # Windows; no bash: export ENTELECY_API_KEY=...
codex --profile entelecyLimites e boas práticas
- Use streaming em resposta longa. Além da percepção de velocidade, evita timeout de proxy em geração demorada.
- Repita com recuo exponencial. Erros de limite de taxa e indisponibilidade do fornecedor são transitórios; erros
4xxdo gateway não melhoram com retentativa. - Marque o prefixo estável com cache. Em prompts com base de conhecimento repetida, é a economia mais fácil de conquistar.
- Valide tamanho de imagem no seu formulário. O gateway recusa de graça, mas a viagem até ele custa tempo do usuário.
- Divida texto longo antes da síntese de voz. Há teto por requisição, e trechos menores ficam melhores de ouvir.
- Guarde o identificador do vídeo. A consulta é a única forma de recuperar o resultado, e é ela que dispara a cobrança quando fica pronto.
Precisa de limite maior, endpoint dedicado ou modelo fora desta lista? Fale com a gente em [email protected].