# Leona AI > API compatível com a OpenAI que escolhe o modelo certo para cada chamada. O roteador inteligente da Leona AI, um modelo de IA próprio, lê o pedido, decide a faixa (light, standard ou advanced) e a chamada vai para um modelo dessa faixa. Você paga por faixa e por tokens, em créditos pré-pagos. - Site: https://ai.leonasolutions.io - Documentação: https://ai.leonasolutions.io/docs - URL base da API: https://api-ai.leonasolutions.io/v1 - Preço por faixa (JSON público): https://ai.leonasolutions.io/api/billing/tiers ## Início rápido 1. Crie a conta em https://ai.leonasolutions.io/signup e compre créditos em Painel > Créditos (PIX ou cartão; entram na hora). Não há créditos grátis. 2. Gere uma chave em Painel > Chaves de API. A chave começa com `lai_` e aparece uma única vez. 3. Use qualquer SDK da OpenAI trocando a URL base. ```python from openai import OpenAI client = OpenAI(base_url="https://api-ai.leonasolutions.io/v1", api_key="lai_...") r = client.chat.completions.create( model="leona-auto", messages=[{"role": "user", "content": "Resuma este texto em 3 tópicos: ..."}], ) print(r.choices[0].message.content) ``` ```javascript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api-ai.leonasolutions.io/v1", apiKey: process.env.LEONA_API_KEY }); const r = await client.chat.completions.create({ model: "leona-auto", messages: [{ role: "user", content: "Classifique o sentimento: adorei o atendimento" }], }); console.log(r.choices[0].message.content); ``` ```bash curl https://api-ai.leonasolutions.io/v1/chat/completions \ -H "Authorization: Bearer $LEONA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"leona-auto","messages":[{"role":"user","content":"Olá"}]}' ``` ## SDKs e frameworks Qualquer cliente compatível com a OpenAI funciona trocando base URL (`https://api-ai.leonasolutions.io/v1`), chave (`lai_...`) e modelo (`leona-auto`): - OpenAI Python: `OpenAI(base_url="https://api-ai.leonasolutions.io/v1", api_key=os.environ["LEONA_API_KEY"])` - OpenAI Node.js: `new OpenAI({ baseURL: "https://api-ai.leonasolutions.io/v1", apiKey: process.env.LEONA_API_KEY })` - Go (openai-go): `openai.NewClient(option.WithBaseURL("https://api-ai.leonasolutions.io/v1"), option.WithAPIKey(os.Getenv("LEONA_API_KEY")))` - PHP (openai-php/client): `OpenAI::factory()->withApiKey(getenv('LEONA_API_KEY'))->withBaseUri('https://api-ai.leonasolutions.io/v1')->make()` - Java (openai-java): `OpenAIOkHttpClient.builder().baseUrl("https://api-ai.leonasolutions.io/v1").apiKey(System.getenv("LEONA_API_KEY")).build()` - .NET (OpenAI): `new ChatClient("leona-auto", new ApiKeyCredential(key), new OpenAIClientOptions { Endpoint = new Uri("https://api-ai.leonasolutions.io/v1") })` - LangChain: `ChatOpenAI(model="leona-auto", base_url="https://api-ai.leonasolutions.io/v1", api_key=...)` (JS: `configuration: { baseURL }`) - Vercel AI SDK: `createOpenAICompatible({ name: "leona", baseURL: "https://api-ai.leonasolutions.io/v1", apiKey })` - LlamaIndex: `OpenAILike(model="leona-auto", api_base="https://api-ai.leonasolutions.io/v1", api_key=..., is_chat_model=True, is_function_calling_model=True)` A resposta traz o objeto extra `leona` (`request_id`, `credits_charged`, `credits_remaining`); no SDK Python: `r.model_extra["leona"]`. ## Autenticação Envie `Authorization: Bearer lai_...` em toda chamada. A chave pertence à conta. Guarde no servidor; nunca no navegador ou em app mobile. Chave revogada para de funcionar na hora. ## Modelos | model | Comportamento | |---|---| | `leona-auto` | O roteador inteligente escolhe a faixa a cada chamada. Recomendado. | | `leona-light` | Força a faixa leve: classificação, extração, respostas curtas. | | `leona-standard` | Força a faixa intermediária. | | `leona-advanced` | Força a faixa de ponta: raciocínio, código, análise longa. | `GET /v1/models` lista os modelos. Qualquer outro nome responde `404 model_not_found`. O campo `model` da resposta mostra sempre a faixa que atendeu (`leona-light`, `leona-standard` ou `leona-advanced`). O modelo e o provedor reais não são expostos. ## POST /v1/chat/completions Mesmo contrato da OpenAI. | Parâmetro | Tipo | Descrição | |---|---|---| | `model` (obrigatório) | string | `leona-auto`, `leona-light`, `leona-standard` ou `leona-advanced` | | `messages` (obrigatório) | array | `system` (ou `developer`), `user`, `assistant`, `tool`; `content` é texto ou partes (texto, imagem, áudio, arquivo) | | `stream` | boolean | resposta em SSE; recomendado para contextos e respostas longas | | `stream_options` | object | `{"include_usage": true}`: último chunk com `usage` e créditos | | `max_tokens` / `max_completion_tokens` | integer | limite de saída; também limitado ao que o saldo cobre | | `temperature`, `top_p`, `stop`, `seed`, `presence_penalty`, `frequency_penalty` | | seguem para o modelo como vieram | | `tools`, `tool_choice`, `parallel_tool_calls` | | function calling (seção abaixo) | | `response_format` | object | `json_schema` ou `json_object` (seção abaixo) | | `reasoning_effort` | string | só vai para modelos com raciocínio; nos outros é ignorado | | `n` | integer | sempre 1 | A chamada só vai para modelos que suportam o que você pediu (tools, JSON, imagem, áudio, documento). Limites: corpo de até 10 MB, até 256 mensagens, até 200 mil caracteres de texto. `n` é sempre 1. Resposta: ```json { "id": "chatcmpl-01a1c1...", "object": "chat.completion", "created": 1762700000, "model": "leona-light", "choices": [{ "index": 0, "message": { "role": "assistant", "content": "..." }, "finish_reason": "stop" }], "usage": { "prompt_tokens": 812, "completion_tokens": 240, "total_tokens": 1052 }, "leona": { "request_id": "01a1c1...", "credits_charged": 1, "credits_remaining": 99 } } ``` O formato é o da OpenAI. A faixa que atendeu vem em `model`; o objeto `leona` traz só o id e a cobrança, e os SDKs da OpenAI o aceitam sem mudar nada. Headers da resposta: `X-Request-Id`, `X-Leona-Tier`, `X-Leona-Credits-Charged`, `X-Leona-Credits-Remaining`. Se o modelo da faixa cair antes de responder, outro assume automaticamente, da mesma faixa ou de uma acima. Nesse caso `model` mostra a faixa que respondeu, e é ela que é cobrada. ## Stream (SSE) Com `"stream": true` a resposta chega como `text/event-stream`, em chunks `chat.completion.chunk`, terminando com `data: [DONE]`. O campo `model` de cada chunk já mostra a faixa. Com `"stream_options": {"include_usage": true}`, o último chunk antes do `[DONE]` traz `usage` e os créditos cobrados: ``` data: {"object":"chat.completion.chunk","model":"leona-advanced","choices":[{"delta":{"role":"assistant"}}],"leona":{"request_id":"..."}} data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"..."}}]} data: {"object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":3500,"completion_tokens":1000,"total_tokens":4500},"leona":{"credits_charged":20}} data: [DONE] ``` Se a resposta for interrompida no meio, chega um evento com `error.code = "upstream_interrupted"` e a cobrança considera só o que foi gerado. **Contextos longos: use stream.** Com documentos grandes, conversas longas, várias imagens ou respostas longas, prefira `"stream": true`. Sem stream, a conexão fica parada até a resposta inteira ficar pronta e chamadas acima de ~100 s são cortadas no caminho (erro 524 da borda; nada é cobrado). Com stream os primeiros tokens chegam em segundos, a conexão se mantém viva e você mostra a resposta enquanto ela é escrita. ## Function calling (tools) Mesmo formato da OpenAI, em todas as faixas, com e sem stream. Só modelos que suportam tools recebem essas chamadas. - `tools`: lista de `{"type":"function","function":{"name","description","parameters"}}`, com `parameters` em JSON Schema. Use `required` e `"additionalProperties": false` para argumentos previsíveis. - `tool_choice`: `"auto"` (padrão), `"required"` (pelo menos uma chamada), `"none"` (nenhuma) ou `{"type":"function","function":{"name":"..."}}` (aquela função). - `parallel_tool_calls: false` pede uma chamada por vez. Sem ele, o modelo pode chamar várias na mesma resposta; alguns modelos chamam uma por vez de qualquer jeito. Trate sempre a lista inteira de `tool_calls`. - Resposta: `message.tool_calls` (`id`, `function.name`, `function.arguments` como texto JSON) e `finish_reason: "tool_calls"`. - Loop: devolva a mensagem do assistente com `tool_calls` como veio e uma `{"role":"tool","tool_call_id":"...","content":"..."}` por chamada (inclusive de erro). Repita até a resposta vir sem `tool_calls`. - Stream: as chamadas chegam em `delta.tool_calls`. O primeiro pedaço de cada uma traz `index`, `id` e `function.name`; os seguintes, só `index` e mais um trecho de `function.arguments`. Junte por `index` e faça o parse quando chegar o `finish_reason`. - Roteamento: o roteador inteligente vê as chamadas e os resultados depois da última mensagem do usuário e trata como continuação da mesma tarefa. Com `leona-auto` a faixa pode mudar entre os passos (o histórico de tools é aceito por qualquer modelo; cada passo é cobrado pela faixa que atendeu). Para manter uma faixa fixa, use `leona-light`, `leona-standard` ou `leona-advanced`. ```python import json tools = [{"type": "function", "function": { "name": "get_weather", "description": "Clima atual de uma cidade.", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], "additionalProperties": False}}}] def run(name, args): # suas funções de verdade if name == "get_weather": return {"city": args["city"], "temp_c": 27} return {"error": f"função desconhecida: {name}"} msgs = [{"role": "user", "content": "Qual é o clima em São Paulo e no Rio?"}] while True: r = client.chat.completions.create(model="leona-auto", messages=msgs, tools=tools) msg = r.choices[0].message if not msg.tool_calls: break msgs.append(msg) for tc in msg.tool_calls: out = run(tc.function.name, json.loads(tc.function.arguments)) msgs.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(out)}) print(msg.content) ``` ## Saída estruturada (JSON) `response_format` devolve JSON em `message.content`. Só modelos que suportam saída estruturada recebem essas chamadas. Funciona com e sem stream. - `{"type":"json_schema","json_schema":{"name":"...","strict":true,"schema":{...}}}`: JSON no formato do schema. Com `strict: true`, todos os campos em `required` e `"additionalProperties": false`. - `{"type":"json_object"}`: JSON válido sem schema. Peça JSON na mensagem e diga as chaves. - `finish_reason: "length"` = JSON cortado; aumente `max_tokens`. ```python r = client.chat.completions.create( model="leona-auto", messages=[{"role": "user", "content": "Extraia: 'Ana tem 31 anos.'"}], response_format={"type": "json_schema", "json_schema": {"name": "pessoa", "strict": True, "schema": { "type": "object", "properties": {"nome": {"type": "string"}, "idade": {"type": "integer"}}, "required": ["nome", "idade"], "additionalProperties": False}}}, ) pessoa = json.loads(r.choices[0].message.content) ``` ## Imagem, áudio e documentos Envie partes no formato da OpenAI dentro de `content`: - `{"type":"image_url","image_url":{"url":"https://... ou data:image/png;base64,..."}}` - `{"type":"input_audio","input_audio":{"data":"","format":"wav"}}` - `{"type":"file","file":{"filename":"contrato.pdf","file_data":"data:application/pdf;base64,..."}}` O roteador decide a faixa pelo texto do pedido; os arquivos não são enviados a ele. Só modelos que aceitam aquele tipo de conteúdo recebem a chamada. Se nenhum aceitar, a resposta é `422 unsupported_content` e nada é cobrado. Tipos de parte desconhecidos também respondem `422`. ```json { "model": "leona-auto", "messages": [{ "role": "user", "content": [ { "type": "text", "text": "Leia o comprovante e devolva valor, data e pagador em JSON." }, { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4AAQ..." } } ] }], "response_format": { "type": "json_object" } } ``` No histórico da conta guardamos o pedido sem os arquivos (só tipo e tamanho) e URLs sem parâmetros. ## Créditos e cobrança Cada faixa custa um número de créditos por unidade de tokens (entrada + saída). Toda chamada paga pelo menos uma unidade: ``` créditos = créditos_por_unidade[faixa] × max(1, ⌈tokens_totais ÷ tokens_por_unidade⌉) ``` A tabela atual está em `GET https://ai.leonasolutions.io/api/billing/tiers`. Exemplo com unidade de 4.000 tokens e light = 1, standard = 3, advanced = 10: uma chamada light de 2.000 tokens custa 1 crédito; uma advanced de 9.000 tokens custa 30. O quanto cada pacote rende está em `GET https://ai.leonasolutions.io/api/billing/pricing` (público, calculado da tabela em vigor): `tiers[]` com `credits_per_unit`, `unit_tokens` e a faixa de preço por 1 milhão de tokens (`per_million_min_cents`/`per_million_max_cents`), e `packages[]` com `credits`, `price_cents` e, por faixa, `yields[].tokens` e `yields[].per_million_cents`. - A escolha do modelo pelo roteador não é cobrada: os créditos pagam só a resposta. - Chamadas recusadas não consomem crédito: autenticação, limite, validação, `upstream_rejected`, `unsupported_content`, `upstream_error`. - Antes da chamada reservamos uma unidade; depois cobramos o uso real. A saída é limitada ao que o saldo cobre. Se mesmo assim a resposta passar do saldo, cobramos o uso real e o saldo fica negativo: as próximas chamadas recebem `429 insufficient_quota` até a recarga, que cobre primeiro esse valor. - Sem saldo, a API responde `429 insufficient_quota` até entrarem créditos. Volta a funcionar na hora em que o saldo é recarregado. - Validade: cada recarga vale 30 dias a partir do pagamento. O uso consome primeiro o que vence antes. Ao recarregar antes de vencer, a sobra passa para o novo ciclo até o valor da nova recarga; o resto vence na data original. - Cada chave tem limite por minuto e cada conta tem cota mensal (`429` com `Retry-After`). - Compra: no painel (Créditos → Comprar), com PIX ou cartão. Os créditos entram assim que o pagamento é confirmado; cada compra aparece no extrato ligada ao pedido. ## Erros Formato da OpenAI: ```json { "error": { "message": "...", "type": "invalid_request_error", "code": "...", "request_id": "..." } } ``` | Status | code | Quando | |---|---|---| | 400 | `invalid_json` | Corpo não é JSON válido. | | 400 | `invalid_request` | Campo inválido (sem mensagens, papel desconhecido). | | 400 | `upstream_rejected` | O modelo recusou o pedido: parâmetro não suportado ou contexto longo demais. Não é cobrado. | | 401 | `invalid_api_key` | Chave ausente, inválida, revogada ou de conta suspensa. | | 404 | `model_not_found` | Modelo que não existe. | | 413 | `body_too_large` | Corpo acima de 10 MB. | | 422 | `unsupported_content` | Tipo de conteúdo desconhecido ou nenhum modelo aceita a mídia enviada. Não é cobrado. | | 429 | `insufficient_quota` | Sem créditos. | | 429 | `rate_limit_exceeded` | Limite por minuto da chave. Respeite o `Retry-After`. | | 429 | `monthly_quota_exceeded` | Cota mensal da conta. | | 502 | `upstream_error` | Nenhum modelo respondeu, mesmo com fallback. Não é cobrado. Tente de novo com backoff. | | 503 | `no_model_available` | Nenhum modelo disponível agora. | | 503 | `server_busy` | Muitos envios grandes ao mesmo tempo. Tente de novo em instantes. | | 503 | `unavailable` | Instabilidade temporária. Tente de novo com backoff. | Informe o `request_id` ao suporte. Retry: `4xx` é para corrigir o pedido. `429 rate_limit_exceeded` e `monthly_quota_exceeded` passam esperando o `Retry-After`; `429 insufficient_quota` só passa com recarga. `502` e `503` são temporários: tente de novo com backoff exponencial (ex.: 1 s, 2 s, 4 s). Desligue o retry automático do SDK (`max_retries=0` / `maxRetries: 0`) se fizer o seu. ## Privacidade Por padrão guardamos por 30 dias o que você envia e o que respondemos, para você consultar em Painel > Requisições. Desligue em Meu perfil > Privacidade; as métricas (tokens, créditos, latência) continuam. Conteúdo nunca vai para log.