# Spec: Dual LLM Provider (reranker_llm separado)

> Generated via /vibeflow:gen-spec on 2026-03-24
> Budget: ≤ 6 files

## Objective

Permitir que o dev injete um LLM provider separado pro reranker (tipicamente mais barato, ex: gpt-4o-mini), enquanto o provider principal roda extração, entity resolution e reconciliação com modelo mais capaz (ex: gpt-4o).

## Context

Hoje o `MemoryClient` recebe um único `llm: LLMProvider`. Todas as chamadas LLM — extração (3 calls), entity resolution (1 call), reconciliação (N calls), planner (1 call), reranker (1 call) — usam o mesmo provider/modelo.

Os campos `extraction_model` e `reranker_model` no `MemoryConfig` são dead code — existem no config mas nunca são usados pra trocar modelo. O provider injetado decide o modelo.

Com GPT-4o a $2.50/1M input e GPT-4o-mini a $0.15/1M input (16x mais barato), separar o reranker pro modelo mais barato reduz ~40% do custo de read.

## Definition of Done

1. **[ ] `MemoryClient` aceita `reranker_llm` opcional** — `MemoryClient(llm=..., reranker_llm=..., embeddings=...)`. Se não fornecido, faz fallback pro `llm` principal.
2. **[ ] Read pipeline usa `reranker_llm` no reranker** — `rerank_with_llm()` recebe o provider correto.
3. **[ ] Dead code removido** — `extraction_model` e `reranker_model` removidos do `MemoryConfig`.
4. **[ ] Token tracking separado** — `RetrieveResult.tokens_used` acumula tokens de ambos os providers. Se `reranker_llm` for diferente, os tokens do reranker vêm dele.
5. **[ ] Docs EN + PT-BR atualizados** — Getting Started, Configuration, Read Pipeline, llms-full.txt refletem a nova API.
6. **[ ] Sem violações de conventions.md** — Naming, error handling, fail-safe patterns seguidos.

## Scope

### 1. `src/arandu/client.py` — Adicionar `reranker_llm` no constructor

```python
class MemoryClient:
    def __init__(
        self,
        database_url: str,
        llm: LLMProvider,
        embeddings: EmbeddingProvider,
        config: MemoryConfig | None = None,
        reranker_llm: LLMProvider | None = None,  # NEW
    ) -> None:
        self._llm = llm
        self._reranker_llm = reranker_llm or llm  # fallback
```

No `retrieve()`, passar `self._reranker_llm` pro read pipeline (que repassa pro reranker).

### 2. `src/arandu/config.py` — Remover dead code

Remover `extraction_model` e `reranker_model` do `MemoryConfig`.

### 3. `src/arandu/read/pipeline.py` — Aceitar `reranker_llm`

`run_read_pipeline()` recebe `reranker_llm: LLMProvider | None = None`. Se fornecido, usa no `rerank_with_llm()`. Se não, usa o `llm` principal.

### 4. `src/arandu/write/extract.py` — Remover parâmetro `model`

`extract_from_message()` perde o parâmetro `model` (dead code).

### 5-6. Docs EN + PT-BR

Atualizar Getting Started (exemplo com 2 providers), Configuration (remover extraction_model/reranker_model), Read Pipeline (mencionar reranker_llm), llms-full.txt.

## Anti-scope

- **Não adicionar `extraction_llm` separado** — Write pipeline usa 1 provider pra tudo. Separar extração/reconciliação/entity_resolution é overkill.
- **Não adicionar pricing/cost calculation** — O SDK expõe tokens. O dev calcula custo.
- **Não mudar `LLMProvider` protocol** — Já foi mudado pra `LLMResult`. Não mexer de novo.
- **Não mudar write pipeline** — `write()` continua usando `self._llm` pra tudo.
- **Não adicionar model name no resultado** — Complexidade desnecessária. O dev sabe qual provider injetou.

## Technical Decisions

### TD-1: Fallback pro `llm` principal quando `reranker_llm` não é fornecido
**Trade-off:** Poderia exigir `reranker_llm` obrigatório. Mas isso quebra 100% dos usos existentes.
**Decisão:** Opcional com fallback. Zero breaking change na API pública.

### TD-2: Remover `extraction_model` e `reranker_model` do config
**Trade-off:** Breaking change pro config (quem tinha esses campos no dict).
**Decisão:** Remover. São dead code confirmado. Ninguém usa. Limpar > manter lixo.

### TD-3: Token tracking acumula de ambos os providers
**Trade-off:** Poderia separar `tokens_used_main` e `tokens_used_reranker`.
**Decisão:** Um `tokens_used` total. O dev sabe que o reranker é 1 call — pode estimar a proporção. Separar é over-engineering.

## Applicable Patterns

- **Dependency Injection** — `reranker_llm` segue o mesmo pattern de `llm` (Protocol-based injection).
- **Error Handling** — Fail-safe: se `reranker_llm` falhar, fallback pro ranking original (já existe no reranker).

## Risks

1. **Dev passa provider errado no `reranker_llm`** — Ex: embedding provider em vez de LLM. Mitigação: `LLMProvider` é runtime_checkable Protocol. TypeError em runtime.
