# jangada — documentação completa (llms-full.txt)

> Camada fina e adaptável sobre os SDKs oficiais de LLM (Anthropic, OpenAI,
> Groq, Gemini). Troque provider/model/api_key sem mudar o resto do código.
> Este arquivo concatena toda a documentação de docs/ para consumo em um
> único fetch. Instalação: pip install jangada-ai (importa-se como import jangada_ai).

Fonte: https://github.com/nerigleston/jangada — índice curto em llms.txt.

---

# Começando com jangada

`jangada` é uma camada fina sobre os SDKs oficiais de LLM (Anthropic, OpenAI,
Groq, Gemini). O objetivo é trocar **provider / model / api_key** sem mudar o
resto do código.

## Instalação

```bash
pip install jangada-ai                 # nome no PyPI; importa-se como jangada_ai
pip install "jangada-ai[anthropic]"    # só Claude
pip install "jangada-ai[openai,groq]"  # OpenAI + Groq
pip install "jangada-ai[all]"          # todos os SDKs
pip install "jangada-ai[files]"        # leitura de docx/pdf/csv/xlsx
```

> O nome de distribuição é `jangada-ai` (o nome `jangada` estava ocupado no
> PyPI). O pacote é importado como `import jangada_ai` (hífen vira underscore).

Imports são preguiçosos: `import jangada_ai` funciona sem nenhum SDK instalado.

## Primeira chamada

```python
from jangada_ai import LLM

llm = LLM("anthropic", "claude-opus-4-8")
print(llm.complete("Explique {{tema}} em 2 frases.", tema="MCP").text)
```

`complete()` aceita templates `{{ }}` direto no prompt — as variáveis vêm como
keyword args. Veja [Parâmetros de geração](parameters.md) para controlar
`temperature`, `max_tokens`, etc.

## Trocar de provider

Só muda os dois primeiros argumentos; o resto do código permanece:

```python
LLM("openai", "gpt-4o-mini")
LLM("groq", "llama-3.3-70b-versatile")
LLM("gemini", "gemini-2.5-flash")
```

Chaves de API vêm de `api_key=`, da variável de ambiente do provider, ou de um
arquivo `.env` detectado na importação. Precedência:
`api_key=` explícito > variável de ambiente > `.env`.

## Próximos passos

- [Providers e chaves](providers.md)
- [Structured output](structured-output.md)
- [Documentos (docx/pdf/csv/xlsx)](documents.md)
- [Retry e fallback](retry-fallback.md)

---

# Providers e chaves de API

A jangada suporta quatro providers, cada um isolado em um *adapter* que traduz
os tipos normalizados (`Message`/`Completion`) para o SDK nativo.

| Provider    | `provider=` | Variável de ambiente | Extra para instalar         |
|-------------|-------------|----------------------|-----------------------------|
| Anthropic   | `anthropic` | `ANTHROPIC_API_KEY`  | `jangada-ai[anthropic]`     |
| OpenAI      | `openai`    | `OPENAI_API_KEY`     | `jangada-ai[openai]`        |
| Groq        | `groq`      | `GROQ_API_KEY`       | `jangada-ai[groq]`          |
| Gemini      | `gemini`    | `GEMINI_API_KEY`     | `jangada-ai[gemini]`        |

## Resolução da chave

```python
LLM("openai", "gpt-4o-mini", api_key="sk-...")   # explícito
LLM("openai", "gpt-4o-mini")                       # usa OPENAI_API_KEY ou .env
```

Precedência: **`api_key=` explícito > variável de ambiente > arquivo `.env`**.
O `.env` é detectado de forma não-destrutiva na importação (desative com
`JANGADA_NO_DOTENV=1`).

## Como cada adapter trata structured output

- **OpenAI**: `chat.completions.parse(response_format=Modelo)` → `.message.parsed`
- **Groq**: `response_format={"type":"json_schema",...}` + `model_validate_json`
- **Gemini**: `config.response_schema=Modelo` → `resp.parsed`
- **Anthropic**: tool-forcing (`tool_choice` fixo) → valida `tool_use.input`

Veja [Structured output](structured-output.md) para o uso uniforme.

## Adicionando um provider novo

Se ele falar o dialeto `chat.completions` da OpenAI, herde de
`_OpenAICompatible` e ajuste `sdk_module`/`sync_class`/`async_class`. Caso
contrário, implemente os 6 métodos do contrato `Provider`. Detalhes em
[Estendendo](extending.md).

---

# Matriz de capacidades por provider

O que cada provider suporta na jangada. Os recursos da API pública são os
mesmos (`complete`, `parse`, `stream`, `transcribe`, ...); o que muda é o que
cada provider consegue fazer por baixo.

| Recurso                         | OpenAI | Groq | Gemini | Anthropic |
|---------------------------------|:------:|:----:|:------:|:---------:|
| Texto (`complete`/`acomplete`)  | ✅     | ✅   | ✅     | ✅        |
| Structured output (`parse`)     | ✅     | ✅   | ✅     | ✅        |
| Tools / function calling        | ✅     | ✅   | ✅     | ✅        |
| Streaming (`stream`/`astream`)  | ✅     | ✅   | ✅     | ✅        |
| Vision / imagens (`images=`)    | ✅     | ⚠️¹  | ✅     | ✅        |
| Documentos (`files=`)²          | ✅     | ✅   | ✅     | ✅        |
| Detecção de objetos             | ✅     | ⚠️¹  | ✅³    | ⚠️        |
| Transcrição de áudio (`transcribe`) | ✅ | ✅   | ✅     | ❌        |
| Param `top_k`                   | ❌     | ❌   | ✅     | ✅        |
| Param `seed`                    | ✅     | ✅   | ✅     | ❌        |
| Param `stop`                    | ✅     | ✅   | ✅ (`stop_sequences`) | ✅ (`stop_sequences`) |

¹ Depende do modelo: vision no Groq exige um modelo com visão (ex.: família
Llama vision); modelos de texto puro não aceitam imagem.
² `files=` extrai texto **localmente** (docx/pdf/csv/xlsx) e envia como texto —
por isso funciona em todos. Veja [Documentos](documents.md).
³ A convenção de bounding box (0–1000) é nativa do Gemini, que é o mais preciso.

## Como cada um implementa o structured output

| Provider  | Mecanismo                                                |
|-----------|----------------------------------------------------------|
| OpenAI    | `chat.completions.parse(response_format=Modelo)`         |
| Groq      | `response_format={"type":"json_schema",...}` + validação |
| Gemini    | `config.response_schema=Modelo` → `resp.parsed`          |
| Anthropic | tool-forcing (`tool_choice` fixo) → valida `tool_use`    |

## Detalhe por provider

- [OpenAI](llm-openai.md)
- [Groq](llm-groq.md)
- [Gemini](llm-gemini.md)
- [Anthropic](llm-anthropic.md)

Os parâmetros canônicos e os perfis por modelo (gpt-5, gemini-3.x) estão em
[Parâmetros e perfis](parameters.md).

---

# OpenAI

Provider `openai`. Adapter sobre o SDK `openai` (dialeto `chat.completions`).

```bash
pip install "jangada-ai[openai]"
```

- **`provider=`**: `"openai"`
- **Variável de ambiente**: `OPENAI_API_KEY`
- **Base do adapter**: `_OpenAICompatible` (compartilhado com Groq)

```python
from jangada_ai import LLM
llm = LLM("openai", "gpt-4o-mini")
```

## O que faz

- **Texto** (`complete`/`acomplete`) e **streaming** (`stream`/`astream`).
- **Structured output** (`parse`): usa o helper nativo
  `chat.completions.parse(response_format=Modelo)` → `.message.parsed`.
- **Vision** (`images=`): imagens viram `image_url` com data URI.
- **Documentos** (`files=`): extração de texto local (comum a todos).
- **Detecção de objetos** (`detect_objects`): via vision + structured.
- **Transcrição de áudio** (`transcribe`): endpoint dedicado
  `audio.transcriptions.create`. Modelos: `gpt-4o-transcribe`,
  `gpt-4o-mini-transcribe`, `whisper-1`.

## Estrutura e quirks

- **Parâmetros**: aceita `temperature`, `max_tokens`, `top_p`, `stop`, `seed`.
  **Não** tem `top_k` (é descartado).
- **Perfil de modelo** (`profiles.py`): a família `gpt-5` rejeita `temperature`
  e usa `max_completion_tokens` em vez de `max_tokens` — a jangada normaliza
  isso automaticamente. Veja [Parâmetros e perfis](parameters.md).
- **Resposta** (`Completion`): `text`, `usage` (`input_tokens`/`output_tokens`
  derivados de `prompt_tokens`/`completion_tokens`), `raw` com o objeto nativo.
- **Erros**: traduzidos por `errors.classify()` para a hierarquia normalizada.

## Exemplo de structured

```python
from pydantic import BaseModel
class Pessoa(BaseModel):
    nome: str; idade: int

llm.parse("Extraia: João, 30 anos.", Pessoa).parsed   # Pessoa(nome='João', idade=30)
```

Relacionado: [Matriz de capacidades](capabilities.md), [Groq](llm-groq.md)
(mesmo dialeto), [Transcrição de áudio](audio.md).

---

# Groq

Provider `groq`. Adapter sobre o SDK `groq`, que fala o mesmo dialeto
`chat.completions` da OpenAI — por isso herda de `_OpenAICompatible`.

```bash
pip install "jangada-ai[groq]"
```

- **`provider=`**: `"groq"`
- **Variável de ambiente**: `GROQ_API_KEY`
- **Diferença para a OpenAI**: `supports_parse_helper = False` (não tem
  `.parse()` nativo).

```python
from jangada_ai import LLM
llm = LLM("groq", "llama-3.3-70b-versatile")
```

## O que faz

- **Texto** e **streaming** — foco em latência muito baixa.
- **Structured output** (`parse`): como não há helper `.parse`, usa
  `response_format={"type":"json_schema",...}` e valida com
  `model_validate_json`.
- **Vision** (`images=`): suportado **apenas em modelos com visão** (ex.:
  família Llama vision). Modelos de texto puro recusam imagem.
- **Documentos** (`files=`): extração de texto local.
- **Transcrição de áudio** (`transcribe`): endpoint dedicado (compatível com o
  da OpenAI). Modelos: `whisper-large-v3`, `whisper-large-v3-turbo` (rápido).

## Estrutura e quirks

- **Parâmetros**: `temperature`, `max_tokens`, `top_p`, `stop`, `seed`.
  Sem `top_k`.
- **Mesma base da OpenAI**: a tradução de mensagens, structured e áudio
  reaproveitam `_OpenAICompatible`; só mudam `sdk_module`/`sync_class`/
  `async_class`/`supports_parse_helper`.
- **Resposta** (`Completion`): igual à OpenAI (`text`, `usage`, `raw`).

## Quando escolher Groq

Velocidade e custo de inferência baixos — ótimo para transcrição em lote
(`whisper-large-v3-turbo`) e respostas de baixa latência. Combine como
**fallback** ou **primário** com OpenAI (mesmo dialeto). Veja
[Transcrição de áudio](audio.md) e [Retry e fallback](retry-fallback.md).

---

# Gemini

Provider `gemini`. Adapter sobre o SDK `google-genai`. Tem um único `Client`; o
async fica em `client.aio`.

```bash
pip install "jangada-ai[gemini]"
```

- **`provider=`**: `"gemini"`
- **Variável de ambiente**: `GEMINI_API_KEY` (ou `GOOGLE_API_KEY`)

```python
from jangada_ai import LLM
llm = LLM("gemini", "gemini-2.5-flash")
```

## O que faz

- **Texto** e **streaming** (`generate_content` / `generate_content_stream`).
- **Structured output** (`parse`): `config.response_schema=Modelo` +
  `response_mime_type="application/json"` → `resp.parsed`.
- **Vision** (`images=`): imagens viram `types.Part.from_bytes`.
- **Documentos** (`files=`): extração de texto local.
- **Detecção de objetos** (`detect_objects`): **o mais preciso** — o formato de
  bounding box 0–1000 é nativo do treino do Gemini.
- **Transcrição de áudio** (`transcribe`): multimodal — o áudio entra como
  `Part.from_bytes` junto de uma instrução; **não** é endpoint dedicado.

## Estrutura e quirks

- **Mensagens**: o papel `system` vira `system_instruction` no
  `GenerateContentConfig` (não é uma mensagem comum); `assistant` vira `model`.
- **Parâmetros canônicos → config**: `max_tokens`→`max_output_tokens`,
  `stop`→`stop_sequences`; `temperature`/`top_p`/`top_k`/`seed` passam direto.
  É o único provider com `top_k`.
- **Perfil de modelo** (`profiles.py`): `gemini-3.x` descarta sampling
  (`temperature`/`top_p`/`top_k`) e usa `thinking_level` no lugar de
  `thinking_budget`. Function calling multi-turn no 3.x exige preservar as
  *thought signatures* (integridade do histórico). Veja
  [Parâmetros e perfis](parameters.md).
- **Resposta** (`Completion`): `usage` vem de `usage_metadata`
  (`prompt_token_count`/`candidates_token_count`).

## Por que é o "canivete suíço" aqui

É o único que cobre vision, detecção e áudio **sem endpoints separados** —
tudo via `generateContent`. Veja [Matriz de capacidades](capabilities.md).

---

# Anthropic (Claude)

Provider `anthropic`. Adapter sobre o SDK `anthropic`.

```bash
pip install "jangada-ai[anthropic]"
```

- **`provider=`**: `"anthropic"`
- **Variável de ambiente**: `ANTHROPIC_API_KEY`

```python
from jangada_ai import LLM
llm = LLM("anthropic", "claude-opus-4-8")
```

## O que faz

- **Texto** e **streaming**.
- **Structured output** (`parse`): por **tool-forcing** — define uma ferramenta
  com o schema e fixa `tool_choice`, depois valida o `tool_use.input` contra o
  modelo Pydantic. (Claude não tem um `response_format` como a OpenAI.)
- **Vision** (`images=`): imagens viram bloco `image` com `source` base64.
- **Documentos** (`files=`): extração de texto local.

## O que NÃO faz

- **Transcrição de áudio**: a API do Claude **não aceita áudio** — `transcribe()`
  levanta `UnsupportedError`. Use OpenAI, Groq ou Gemini para áudio (e, se
  quiser, mantenha o Claude como provider de texto com fallback de áudio em
  outro). Veja [Transcrição de áudio](audio.md).
- **Detecção de objetos**: funciona mecanicamente (vision + structured), mas a
  precisão das coordenadas é menor que a do Gemini.

## Estrutura e quirks

- **Parâmetros**: `temperature`, `max_tokens`, `top_p`, `top_k`, `stop`
  (→ `stop_sequences`). **Não** tem `seed` (é descartado).
- **`max_tokens` é obrigatório** na API do Claude — a jangada já usa um default
  (`1024`) quando você não informa.
- **System**: vai no campo `system` da requisição (não como mensagem).
- **Resposta** (`Completion`): `usage` de `input_tokens`/`output_tokens` nativos;
  `raw` com o objeto da SDK.
- **Erros**: 529 (overloaded) vira `OverloadedError` (subtipo de `ServerError`),
  elegível a retry/fallback. Veja [Erros](errors.md).

Relacionado: [Matriz de capacidades](capabilities.md),
[Structured output](structured-output.md).

---

# Parâmetros de geração e perfis por modelo

A jangada aceita **nomes canônicos** de parâmetros e cada adapter os traduz para
o nome nativo do SDK, descartando os não suportados.

## Parâmetros canônicos

| Canônico      | OpenAI/Groq            | Anthropic        | Gemini                |
|---------------|------------------------|------------------|-----------------------|
| `temperature` | `temperature`          | `temperature`    | `temperature`         |
| `max_tokens`  | `max_tokens`           | `max_tokens`     | `max_output_tokens`   |
| `top_p`       | `top_p`                | `top_p`          | `top_p`               |
| `top_k`       | *(descartado)*         | `top_k`          | `top_k`               |
| `stop`        | `stop`                 | `stop_sequences` | `stop_sequences`      |
| `seed`        | `seed`                 | *(descartado)*   | `seed`                |

```python
llm = LLM("anthropic", "claude-opus-4-8", temperature=0.2, max_tokens=512)

# override por chamada
llm.complete("...", params={"temperature": 0.9})

# clone com novos defaults
criativo = llm.with_params(temperature=1.0)
```

Parâmetros específicos de um SDK que não têm nome canônico vão via `extra=`.

## Perfis automáticos (quirks por modelo)

Modelos do mesmo provider às vezes têm contratos diferentes. A jangada normaliza
isso em `profiles.py`, **por modelo**, sem você precisar saber:

- `gpt-5` rejeita `temperature` (HTTP 400) e exige `max_completion_tokens`.
- `gemini-3.x` descarta `temperature`/`top_p`/`top_k` e troca `thinking_budget`
  por `thinking_level`.

Ordem aplicada no adapter: `_translate()` (canônico → nativo) →
`apply_profile()` (quirks de modelo). Ao suportar um modelo novo com contrato
diferente, adicione uma regra em `profiles.py` em vez de espalhar `if`s.

Veja [Providers](providers.md) e [Estendendo](extending.md).

---

# Structured output (Pydantic)

Uma só chamada `parse()` devolve uma instância Pydantic validada, independente
de como cada provider implementa isso por baixo.

```python
from pydantic import BaseModel
from jangada_ai import LLM

class Pessoa(BaseModel):
    nome: str
    idade: int

llm = LLM("openai", "gpt-4o-mini")
comp = llm.parse("Extraia: João tem 30 anos.", Pessoa)
print(comp.parsed.nome, comp.parsed.idade)   # João 30
```

- `comp.parsed` → a instância Pydantic.
- `comp.text` → o JSON bruto retornado.
- `comp.usage` / `comp.cost` → tokens e custo estimado.

## Async

```python
comp = await llm.aparse("Extraia: ...", Pessoa)
```

## Como cada provider resolve

| Provider  | Mecanismo                                                |
|-----------|----------------------------------------------------------|
| OpenAI    | `chat.completions.parse(response_format=Modelo)`         |
| Groq      | `response_format={"type":"json_schema",...}` + validação |
| Gemini    | `config.response_schema=Modelo` → `resp.parsed`          |
| Anthropic | tool-forcing (`tool_choice` fixo) → valida `tool_use`    |

Você não precisa saber qual é qual — `parse()`/`aparse()` cuidam disso. Use
Pydantic v2 (`model_json_schema()`, `model_validate`).

Funciona junto com [Vision](vision.md) e [Documentos](documents.md): passe
`images=` ou `files=` na mesma chamada `parse()`.

---

# Tools (function calling)

O modelo pode pedir para chamar **ferramentas** (funções). A API é de **baixo
nível**: o `complete()` devolve as chamadas pedidas em `Completion.tool_calls`,
**você executa** e reenvia o resultado. Suportado em **OpenAI, Groq, Anthropic
e Gemini** — a mesma interface nos quatro.

```python
from jangada_ai import LLM, Message

def get_weather(city: str, units: str = "metric") -> str:
    """Retorna o clima atual de uma cidade."""
    return "25°C, ensolarado"

llm = LLM("openai", "gpt-4o-mini")

# 1) o modelo decide chamar a ferramenta
comp = llm.complete("Como está o tempo em Recife?", tools=[get_weather])

# 2) você executa cada chamada e monta os resultados
results = []
for call in comp.tool_calls:        # call.name, call.args (dict)
    saida = get_weather(**call.args)
    results.append(call.result(saida))

# 3) reenvia: histórico = pergunta + resposta-com-tool-calls + resultados
comp2 = llm.complete(
    "Como está o tempo em Recife?",
    history=[comp.assistant_message(), Message.tool_results(*results)],
    tools=[get_weather],
)
print(comp2.text)
```

## Definindo ferramentas

`tools=[...]` aceita:

- **função Python** — o schema sai da assinatura (type hints) + docstring;
- **modelo Pydantic** — vira o schema dos argumentos;
- **dict** `{"name", "description", "parameters"}` (JSON Schema) já pronto;
- um **`Tool`** (via `to_tool(...)`).

`tool_choice` controla a escolha: `"auto"` (padrão), `"none"`, `"required"`, ou
o **nome** de uma ferramenta para forçá-la.

## Peças

- `Completion.tool_calls`: lista de `ToolCall(id, name, args)`.
- `comp.assistant_message()`: reconstrói a mensagem do assistant (texto + tool
  calls) para o histórico.
- `call.result(saida)`: cria o `ToolResultPart` correspondente.
- `Message.tool_results(*parts)`: empacota os resultados numa mensagem.

## Ferramentas pré-prontas

A jangada traz tools prontas em `jangada_ai.prebuilt`:

```python
from jangada_ai.prebuilt import tavily_search   # busca na web (Tavily)

llm.complete("Qual a cotação do dólar hoje?", tools=[tavily_search])
# execute: tavily_search(**call.args)  (precisa de TAVILY_API_KEY no ambiente)
```

**Sem dependência e sem chave:**

| Tool | O que faz |
|------|-----------|
| `calculator` | avalia expressões aritméticas (seguro, via `ast`) |
| `current_datetime` | data/hora atuais (fuso IANA) |
| `fetch_url` | baixa uma página e devolve o texto legível |
| `wikipedia_search` | resumo da Wikipedia (sem chave) |
| `http_request` | requisição HTTP genérica (GET/POST/...) |

**Com chave (lê do ambiente, ou passe `api_key=`):**

| Tool | Chave |
|------|-------|
| `tavily_search` | `TAVILY_API_KEY` (ou `tavily_tool(api_key=...)`) |
| `brave_search` | `BRAVE_API_KEY` |
| `openweather` | `OPENWEATHER_API_KEY` |

> Parâmetros **keyword-only** (após `*`, como `api_key`/`timeout`) são config de
> runtime e **não** aparecem no schema que o modelo vê.

Veja também [Structured output](structured-output.md) (que no Anthropic já usa
tool-forcing por baixo) e [Observabilidade](observability.md) (as tool calls
aparecem no trace).

---

# Vision (imagens)

Imagens entram como `ImagePart` (bytes + mime) e são traduzidas para o formato
nativo de cada SDK. Use sempre um modelo com visão.

```python
from jangada_ai import LLM, Image

llm = LLM("openai", "gpt-4o-mini")

# por caminho
llm.complete("O que aparece aqui?", images=["foto.jpg"])

# por bytes ou base64
img = Image.from_bytes(upload_bytes, "image/png")   # ou Image.from_base64
recibo = llm.parse("Extraia o total.", Recibo, images=[img]).parsed
```

## Tradução por provider

| Provider     | Formato nativo                          |
|--------------|-----------------------------------------|
| OpenAI/Groq  | `image_url` com data URI                |
| Anthropic    | bloco `image` / `source` base64         |
| Gemini       | `types.Part.from_bytes`                 |

Apenas bytes circulam (use `Image.from_path/bytes/base64`). Combine com
[Structured output](structured-output.md) passando `images=` no `parse()`.

## Vision x Documentos

Para **docx/pdf/csv/xlsx**, prefira [Documentos](documents.md): por padrão a
jangada extrai o texto localmente (mais barato, funciona em modelo sem visão) e
só usa vision quando você força `mode="vision"` ou quando o PDF é escaneado.

---

# Detecção de objetos

`detect_objects()` detecta objetos em uma imagem e devolve as **caixas
delimitadoras em pixels absolutos**. É vision + structured output, então
**funciona em qualquer provider com visão** — não é exclusivo do Gemini.

```python
from jangada_ai import LLM, detect_objects

llm = LLM("gemini", "gemini-2.5-flash")
dets = detect_objects(llm, "foto.png")

for d in dets:
    print(d.label, d.box)   # box = [x1, y1, x2, y2] em pixels
```

Cada `Detection` tem:

- `label` — nome do objeto.
- `box` — caixa em **pixels absolutos**, `[x1, y1, x2, y2]` (canto superior
  esquerdo e inferior direito), já convertida ao tamanho real da imagem.
- `box_2d` — caixa crua do modelo, `[ymin, xmin, ymax, xmax]` normalizada 0–1000.

## Parâmetros

```python
detect_objects(
    llm,
    image,                      # caminho, ImagePart ou bytes via Image.from_bytes
    target="todos os gatos",   # restringe o que procurar (opcional)
    max_objects=10,             # limita a quantidade (opcional)
    instructions="Ignore objetos desfocados; rotule em inglês.",  # ACRESCENTA ao prompt padrão
    prompt=None,                # sobrescreve a instrução inteira (opcional)
    image_size=(800, 600),      # informe se o formato não for detectável
)
```

- `instructions` **soma** ao prompt padrão — útil para dar contexto da cena,
  regras de rotulagem ou o que ignorar, sem perder o formato garantido.
- `prompt` **substitui** a instrução inteira (o schema ainda garante a saída
  JSON). Pode combinar os dois: `prompt=` define a base e `instructions=` agrega.

Versão async: `await adetect_objects(llm, image, ...)`.

## Funciona em todos os providers?

**Sim, mecanicamente.** A convenção `box_2d [ymin,xmin,ymax,xmax]` em escala
0–1000 é a do **Gemini** (instruída via prompt e validada por schema Pydantic),
então:

- **Gemini** — mais preciso (formato nativo de treino).
- **OpenAI (gpt-4o, ...)** — funciona bem.
- **Anthropic (Claude vision)** — detecta, mas a precisão das coordenadas varia.

Use sempre um modelo com visão. Veja também [Vision](vision.md) e
[Structured output](structured-output.md).

## Dimensões da imagem

As dimensões são lidas direto dos bytes (PNG, JPEG, GIF, BMP, WEBP) sem
dependência externa. Para outros formatos, passe `image_size=(largura, altura)`.

---

# Transcrição de áudio (speech-to-text)

`LLM.transcribe()` converte áudio em texto. **Não é suportado por todos os
providers** — depende de a API do provider aceitar áudio:

| Provider  | Suporta? | Como         | Modelos típicos                                  |
|-----------|----------|--------------|--------------------------------------------------|
| OpenAI    | ✅       | endpoint dedicado | `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, `whisper-1` |
| Groq      | ✅       | endpoint dedicado | `whisper-large-v3`, `whisper-large-v3-turbo`     |
| Gemini    | ✅       | multimodal (`generateContent`) | `gemini-2.5-flash`, `gemini-2.5-pro` |
| Anthropic | ❌       | —            | a API do Claude não aceita áudio                 |

> Tentar transcrever no Anthropic levanta `UnsupportedError`. O "voice" do
> Claude é recurso de produto (app/Claude Code), não da API do modelo.

## Uso

```python
from jangada_ai import LLM, Audio

# OpenAI
llm = LLM("openai", "gpt-4o-transcribe")
print(llm.transcribe("entrevista.mp3").text)

# Groq (mais rápido/barato)
llm = LLM("groq", "whisper-large-v3-turbo")
print(llm.transcribe("entrevista.mp3").text)

# Gemini (multimodal)
llm = LLM("gemini", "gemini-2.5-flash")
print(llm.transcribe("entrevista.mp3").text)
```

Entradas aceitas: caminho, `AudioPart` ou bytes via `Audio.from_bytes`:

```python
audio = Audio.from_bytes(blob, "audio/wav", name="fala.wav")
llm.transcribe(audio)
```

Async: `await llm.atranscribe(audio)`.

## Opções por provider

Os kwargs extras vão direto ao provider quando ele os aceita:

```python
# OpenAI/Groq aceitam language, prompt, response_format, temperature, ...
llm.transcribe("audio.mp3", language="pt", response_format="text")

# Gemini aceita prompt= como instrução (ex.: incluir timestamps)
llm.transcribe("audio.mp3", prompt="Transcreva com marcações de tempo.")
```

## Fallback entre providers de áudio

Como qualquer chamada da jangada, `transcribe()` honra retry e fallback:

```python
from jangada_ai import LLM

primario = LLM("groq", "whisper-large-v3-turbo")
reserva  = LLM("openai", "gpt-4o-transcribe")
stt = primario.with_fallback(reserva)

stt.transcribe("audio.mp3")   # tenta Groq; se falhar (5xx/timeout), vai pro OpenAI
```

> `UnsupportedError` **não** entra no failover padrão — é erro de configuração
> (provider sem áudio), não falha transitória. Veja [Erros](errors.md) e
> [Retry e fallback](retry-fallback.md).

## Formatos e limites

- OpenAI/Groq: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, webm — até ~25 MB.
- Gemini: áudio inline até ~20 MB no total do request (use a Files API do SDK
  para arquivos maiores).

---

# Documentos (docx, pdf, csv, xlsx)

Anexe arquivos a qualquer chamada com `files=`. Por padrão a jangada **extrai o
texto** do arquivo localmente em vez de usar vision — é mais barato e funciona
em qualquer modelo, inclusive os sem visão.

```bash
pip install "jangada-ai[files]"   # pypdf, python-docx, openpyxl
```

```python
from jangada_ai import LLM, Document

llm = LLM("openai", "gpt-4o-mini")

# caminhos: tipo detectado pela extensão
llm.complete("Resuma:", files=["relatorio.pdf", "contrato.docx"])

# xlsx: TODAS as abas entram, cada uma rotulada (## Aba: ...)
llm.complete("Maior total?", files=[Document("vendas.xlsx", max_rows=200)])

# bytes em memória (upload/fila) — informe o nome para detectar o tipo
llm.parse("Há duplicadas?", Relatorio, files=[Document(blob, name="x.csv")])

# forçar vision (PDF escaneado / quando o layout importa)
llm.complete("Transcreva:", files=[Document("scan.pdf", mode="vision")])
```

## Regra de `mode`

| `mode`     | Comportamento                                                     |
|------------|------------------------------------------------------------------|
| `"auto"`   | (padrão) extrai texto de csv/xlsx/docx/pdf-com-texto; imagem → vision |
| `"text"`   | força extração de texto (erro se o formato não tiver texto)      |
| `"vision"` | força o caminho de imagem                                        |

## Por que não usar vision para tudo

- **Mais barato**: texto custa muito menos que tokens de imagem.
- **Funciona em qualquer modelo**, inclusive sem visão.
- **Preserva tabelas** como markdown.

Um PDF **sem** camada de texto (escaneado) levanta `DocumentError` sugerindo
`mode="vision"` — nunca devolve um bloco vazio em silêncio.

## Detalhes

- `files=` existe em `complete/parse/stream` (sync e async) e convive com `images=`.
- A conversão acontece na fronteira do client (`files.py` → `to_part()`): cada
  arquivo vira `TextPart` ou `ImagePart`, então os adapters nunca veem formato
  de documento.
- Formatos: `.csv`, `.tsv`, `.xlsx`, `.xlsm`, `.docx`, `.pdf`, além de texto
  puro (`.txt`, `.md`, `.json`, ...).

Relacionado: [Vision](vision.md), [Structured output](structured-output.md).

---

# Streaming

Receba tokens incrementais com `stream()` (sync) ou `astream()` (async).

```python
for token in llm.stream("Conte sobre {{x}}", x="João Pessoa"):
    print(token, end="")
```

```python
async for token in llm.astream("..."):   # ex.: FastAPI StreamingResponse
    ...
```

## Retry e fallback no streaming

O retry e o fallback acontecem **antes do primeiro token**: se a abertura do
stream falhar com erro transitório, a jangada tenta de novo (backoff) e, se
preciso, cai para o próximo candidato — tudo antes de você receber qualquer
conteúdo. Depois que o primeiro token sai, o stream segue até o fim.

Veja [Retry e fallback](retry-fallback.md) para a política completa.

## Observações

- `stream()`/`astream()` aceitam os mesmos `system=`, `history=`, `images=`,
  `files=` e `params=` das outras chamadas.
- Para custo e tokens use as chamadas não-stream (`complete`/`parse`), que
  retornam `usage`/`cost` na resposta — veja [Custo e tokens](cost.md).

---

# Retry e fallback

A jangada combina duas defesas contra falhas de API: **retry com backoff** no
mesmo candidato e **fallback** para outro modelo/provider.

```python
from jangada_ai import LLM

primario = LLM("openai", "gpt-4o-mini")
reserva  = LLM("anthropic", "claude-haiku-4-5-20251001")

llm = primario.with_fallback(reserva)
llm.complete("...")   # tenta o primário (com retries); se falhar, vai pro reserva
```

## Como a ordem funciona

Por candidato, o cliente tenta `max_retries + 1` vezes com backoff exponencial
(com jitter) antes de cair para o próximo candidato:

```
[primário] tenta → retry → retry → falhou ─▶ [reserva] tenta → retry → ...
```

- **Retry** acontece em erros transitórios (`backoff_on`, padrão
  `errors.TRANSIENT`: rate limit, timeout, conexão, 5xx).
- **Fallback** acontece nos erros de `retry_on` (padrão `DEFAULT_FAILOVER`:
  rate limit, timeout, conexão, 5xx, **404**).
- `NotFoundError` (404) **não** repete no mesmo candidato, mas **faz** fallback.
- `auth` e `bad_request` **não** entram no failover padrão — falham de imediato.

## Parâmetros

```python
LLM(
    "openai", "gpt-4o-mini",
    max_retries=2,          # tentativas extras por candidato
    backoff_base=0.5,       # segundos
    backoff_max=8.0,
    jitter=True,
    retry_on=None,          # default: errors.DEFAULT_FAILOVER
    backoff_on=None,        # default: errors.TRANSIENT
    fallbacks=[reserva],
)
```

Os erros são normalizados (com `status_code`) — veja [Erros](errors.md). Para o
custo agregado entre candidatos, veja [Custo e tokens](cost.md).

---

# Custo e tokens

Toda resposta bem-sucedida volta com `usage` (tokens) e `cost` (USD estimado).

```python
comp = llm.complete("...")
print(comp.usage)   # {"input_tokens": ..., "output_tokens": ...}
print(comp.cost)    # ex.: 0.000123  (USD, aproximado)
```

O cliente chama `pricing.compute_cost()` após cada sucesso e seta
`Completion.cost`. `FlowResult` e `GraphResult` **agregam** `usage`/`cost` ao
longo da cadeia.

## Tabela de preços

Os preços ficam em `pricing.py` (aproximados, por 1M de tokens). Você pode
registrar/ajustar:

```python
from jangada_ai import register_price, price_for

register_price("meu-modelo", input=0.5, output=1.5)   # USD por 1M tokens
print(price_for("gpt-4o-mini"))
```

> ⚠️ Os valores são **aproximados** e servem para estimativa/observabilidade —
> não trate como fonte de billing.

## Onde isso aparece

- `Completion.cost` / `Completion.usage` em cada chamada.
- Totais agregados em [Fluxos e Graph](flows.md).
- No [Debug passo a passo](debug.md), o custo de cada etapa é exibido no trace.

---

# Observabilidade (envio de traces)

`Observability`/`Trace` enviam as chamadas de uma request, agrupadas em um
**lote** (trace), para uma plataforma de observabilidade. Numa request que faz
N chamadas de IA, abre-se 1 trace e registram-se N observations — um único POST
ao final.

```python
import os
from jangada_ai import LLM, Observability

obs = Observability(api_key=os.environ["LOBS_API_KEY"], endpoint="https://sua-api")
llm = LLM("openai", "gpt-4o-mini")

with obs.trace(name="resumo+tradução") as t:
    r1 = llm.complete("Resuma: ...")
    t.log(r1)
    r2 = llm.complete("Traduza: ...")
    t.log(r2)
# flush automático -> 1 POST com as 2 observations
```

## O que é capturado

`t.log(completion)` extrai do `Completion`: `provider`, `model`,
`promptTokens`/`completionTokens` (de `usage`), `costUsd` (de `cost`) e o texto
de saída. Você pode complementar:

```python
t.log(r1, name="extração", input=prompt, latency_ms=820)
t.log(error="timeout ao chamar o modelo")   # registra falha
```

## Detalhes

- `trace(id=...)`: informe um id externo para **acrescentar** observations ao
  mesmo lote em chamadas separadas (idempotente no backend).
- `user_id` / `session_id` / `metadata`: contexto do usuário final do seu app.
- Falhas de rede **não derrubam** sua aplicação (`raise_on_error=False` por
  padrão); o erro vai para o stderr.
- A `api_key` é a chave do projeto, configurada no `.env` do seu projeto.

Os campos de custo/tokens vêm de [Custo e tokens](cost.md).

---

# Erros normalizados

Cada SDK levanta exceções diferentes. A jangada traduz tudo para uma hierarquia
única via `errors.classify()`, com `status_code` quando disponível. Nenhum erro
nativo de SDK escapa da fronteira dos adapters.

```python
from jangada_ai import LLM, errors

try:
    LLM("openai", "modelo-inexistente").complete("oi")
except errors.NotFoundError as e:
    print(e.status_code)   # 404
except errors.LLMError as e:
    print("falha genérica:", e)
```

## Hierarquia (resumo)

Todas herdam de `errors.LLMError`. As principais categorias:

| Erro                 | Origem típica                      | Failover padrão? |
|----------------------|------------------------------------|------------------|
| `RateLimitError`     | 429                                | sim              |
| `TimeoutError`       | timeout de rede                    | sim              |
| `ConnectionError`    | falha de conexão                   | sim              |
| `ServerError`        | 5xx                                | sim              |
| `NotFoundError`      | 404 (modelo/endpoint)              | sim (sem retry)  |
| `AuthError`          | 401/403                            | **não**          |
| `BadRequestError`    | 400 (params inválidos)             | **não**          |

## Conjuntos usados pela política

- `errors.TRANSIENT` — o que dispara **retry com backoff** (rate limit, timeout,
  conexão, 5xx).
- `errors.DEFAULT_FAILOVER` — o que dispara **fallback** (os transitórios + 404).
  Não inclui `auth` nem `bad_request` por padrão.

Você pode customizar `retry_on=` e `backoff_on=` por `LLM` — veja
[Retry e fallback](retry-fallback.md).

---

# Fluxos e orquestração (Flow e Graph)

A jangada traz duas formas de encadear chamadas, ambas agregando `usage`/`cost`.

## Flow — sequencial

`Flow` encadeia `Step`s: a saída de um vira entrada do próximo.

```python
from jangada_ai import LLM, Flow, Step

llm = LLM("openai", "gpt-4o-mini")

flow = Flow([
    Step("rascunho", "Escreva um parágrafo sobre {{tema}}."),
    Step("revisao",  "Revise e melhore:\n{{rascunho}}"),
])

resultado = flow.run(llm, tema="jangadas do Nordeste")
print(resultado.output)     # saída do último step
print(resultado.cost)       # custo agregado de toda a cadeia
```

Cada `Step` referencia as saídas anteriores pelo nome via template `{{ }}`.

## Graph — roteamento condicional + paralelo

`Graph` permite ramificar (roteamento condicional) e executar nós em paralelo
(core async), juntando os resultados.

```python
from jangada_ai import Graph

# roteamento condicional: escolhe o próximo nó conforme a saída
# paralelo + junção: dispara vários nós e combina as respostas
g = Graph()
# ... defina nós, arestas condicionais e junções ...
res = g.run(...)        # GraphResult agrega usage/cost
```

Veja os exemplos executáveis em
[`examples/graph_example.py`](https://github.com/nerigleston/jangada/blob/master/examples/graph_example.py).

## Custo agregado

`FlowResult` e `GraphResult` somam `usage` e `cost` de todas as etapas — útil
para observabilidade. Detalhes em [Custo e tokens](cost.md) e, para inspecionar
passo a passo, [Debug](debug.md).

---

# Debug passo a passo

Ative `debug=True` para um trace de cada chamada: provider/modelo, params,
retries, fallback, tokens, custo e duração — por agente.

```python
from jangada_ai import LLM

llm = LLM("openai", "gpt-4o-mini", debug=True, name="extrator")
llm.complete("...")
```

O `Debugger` registra os eventos da cadeia:

- `start` — provider, modelo e params da tentativa
- `retry` — erro, número da tentativa e atraso do backoff
- `fallback` — para qual provider/modelo caiu
- `end` — `Completion` resultante e duração em ms
- `error` — erro normalizado quando o candidato esgota as tentativas

O parâmetro `name=` rotula o agente no trace, útil quando há vários `LLM`
diferentes numa mesma orquestração ([Flow/Graph](flows.md)).

Relacionado: [Retry e fallback](retry-fallback.md), [Custo e tokens](cost.md),
[Erros](errors.md).

---

# Estendendo: adicionar um provider

Cada provider é um *adapter* que herda de `Provider` e traduz os tipos
normalizados para o SDK nativo.

## Passos

1. Crie `jangada/providers/<nome>.py` com uma classe que herda de `Provider` e
   implementa os 6 métodos + `_build_client` / `_build_async_client`.
2. Defina `name` e `env_key`.
3. Importe o SDK **só dentro** dos métodos (imports preguiçosos — invariante).
4. Registre em `registry.py` com um loader preguiçoso.
5. Adicione o extra em `pyproject.toml`.

## Contrato (`Provider`)

```python
class Provider:
    name: str
    env_key: str | None

    def _build_client(self) -> Any: ...
    def _build_async_client(self) -> Any: ...
    def complete(self, messages, **opts) -> Completion: ...
    async def acomplete(self, messages, **opts) -> Completion: ...
    def parse(self, messages, schema, **opts) -> Completion: ...
    async def aparse(self, messages, schema, **opts) -> Completion: ...
    def stream(self, messages, **opts) -> Iterator[str]: ...
    def astream(self, messages, **opts) -> AsyncIterator[str]: ...
```

## Atalho para dialeto OpenAI

Se o provider falar `chat.completions` (estilo OpenAI), herde de
`_OpenAICompatible` e só ajuste os atributos:

```python
class MeuProvider(_OpenAICompatible):
    name = "meu"
    env_key = "MEU_API_KEY"
    sdk_module = "meu_sdk"
    sync_class = "Client"
    async_class = "AsyncClient"
    supports_parse_helper = False   # True se tiver .parse() nativo
```

## Invariantes a respeitar

- **Imports preguiçosos**: `import jangada_ai` deve funcionar sem o SDK.
- **Tipos normalizados na fronteira**: fora dos adapters só circula
  `Message`/`Completion`; objetos nativos ficam em `Completion.raw`.
- **Tradução de erro sempre**: envolva chamadas de SDK em `try/except` e
  re-levante via `classify(e, self.name)` — veja [Erros](errors.md).
- **Paridade sync/async**: todo método tem versão `a*`.
- **Quirks por modelo** vão em `profiles.py` — veja [Parâmetros](parameters.md).

## Testes

Use o padrão de `FakeProvider` registrado em runtime; veja
[`tests/conftest.py`](https://github.com/nerigleston/jangada/blob/master/tests/conftest.py).
