Files
ForgeLM/README.md
T

200 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div align="center">
<h1>ForgeLM</h1>
<p><strong>Inferência local de LLMs <code>.gguf</code> em Rust: API no estilo OpenAI, CLI e app desktop Tauri, tudo na sua máquina.</strong></p>
<a href="#-quickstart">Quickstart</a> ·
<a href="#-arquitetura">Arquitetura</a> ·
<a href="#-documenta%C3%A7%C3%A3o">Documentação</a> ·
<a href="https://git.evandro.dev.br/evandro/ForgeLM/issues">Issues</a>
![Stack](https://img.shields.io/badge/Stack-Rust%20%2B%20Tauri%202-dea584?logo=rust&logoColor=white)
![Status](https://img.shields.io/badge/status-em%20desenvolvimento-yellow)
![Licença](https://img.shields.io/badge/licen%C3%A7a-MIT-blue)
</div>
<hr />
> [!NOTE]
> **Este projeto não está concluído.** O que está descrito abaixo é o que existe hoje no código. Segundo [`.planning/ROADMAP.md`](.planning/ROADMAP.md), as fases 1–4 estão concluídas; a fase 5 (validação de fontes de modelo) segue aberta no roadmap e a fase 6 (redesign do chat) consta como planejada.
## 📌 O problema
Rodar um modelo `.gguf` na própria máquina exige juntar peças soltas: um runtime do llama.cpp compatível com a sua GPU, uma API que clientes no estilo OpenAI consigam chamar e um lugar para escolher modelos e conversar. O ForgeLM reúne essas peças em um workspace Rust local-first, sem depender de infraestrutura hospedada, e documenta o que ainda não funciona.
## ✨ O que faz (o que existe hoje)
- **Serve** `GET /health`, `GET /v1/models` e `POST /v1/chat/completions` em `:8080`, com streaming SSE e `stream_options.include_usage`.
- **Carrega** modelos `.gguf` via llama.cpp (carga dinâmica) com os perfis `Turbo4B` (default), `balanced_9b` e `cpu_fallback`, ajustáveis por variáveis de ambiente.
- **Baixa e valida** o runtime llama.cpp `b8358` (CPU ou CUDA 12.4, com SHA-256) quando ele não é encontrado.
- **Opera** pela CLI `localllm`: `status`, `models`, `auth`, `config`, `bench`, `logs` e `doctor`.
- **Gerencia** o runtime no app desktop (modo `local` ou `remote`; estados `stopped`, `starting`, `ready`, `degraded`, `stopping` e `error`).
- **Escaneia, importa, busca e baixa** modelos do Hugging Face com progresso e cancelamento (token opcional) e troca o modelo carregado com um clique (*hot-swap*).
- **Conversa** com streaming, memória opcional de até 20 mensagens, separação de `<think>…</think>`, contagem de tokens, tokens/s e edição, reenvio e exclusão de mensagens.
- **Protege** a API com chave opcional (`LOCALLLM_API_KEY`), CORS configurável e aliases de modelo.
## 🖼️ Demonstração
Capturas do shell do app com a API local offline e nenhum modelo selecionado: mostram o layout, não uma inferência em andamento.
<p align="center">
<img src="./loaded-shell.png" alt="Shell do app ForgeLM em desktop: Runtime Manager, lista de conversas e área de chat" width="720" />
</p>
<p align="center">
<img src="./loaded-tablet.png" alt="Shell do app ForgeLM em tablet" height="360" />
<img src="./loaded-mobile.png" alt="Shell do app ForgeLM em tela estreita" height="360" />
</p>
## 🚀 Quickstart
> [!WARNING]
> **A API sobe aberta por padrão.** Sem `LOCALLLM_API_KEY` não há autenticação, o bind default é `0.0.0.0:8080` e o CORS libera `*`. Fora de uma máquina confiável, defina a chave e restrinja `LOCALLLM_ADDR` e `LOCALLLM_CORS_ORIGINS`.
| Ferramenta | Versão mínima |
|---|---|
| Rust (cargo) | edition 2021; `rust-version` não declarada |
| Node.js + npm (só para o app) | não declarada (sem `.nvmrc` nem `engines`) |
| Modelo `.gguf` local | obrigatório |
| GPU NVIDIA | opcional (variante CUDA 12.4; existe o perfil `cpu_fallback`) |
**Na primeira execução o servidor baixa o llama.cpp `b8358`** de `github.com/ggml-org/llama.cpp` se não achar o runtime (`LLAMA_DLL_PATH`, binário adjacente ou vendor legado). CUDA 12.4 são dois zips (≈ 227 MB + ≈ 391 MB); CPU, um zip de ≈ 37 MB. O manifesto só lista `windows-x64`.
```bash
# 1. API local (Windows cmd; em bash use `export`)
set LOCALLLM_MODEL=D:\modelos\meu-modelo.gguf
cargo run -p inference-server
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/v1/models
curl -N http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"default","messages":[{"role":"user","content":"Diga ola"}],"stream":true}'
# 2. CLI
cargo run -p localllm-cli -- status
cargo run -p localllm-cli -- models list
# 3. App desktop
cd tauri-app
npm install
npm run tauri dev
```
### Variáveis de ambiente
Extraídas do código e de [`docs/api/configuration.md`](docs/api/configuration.md); não há `.env.example`. O prefixo é `LOCALLLM_` (três L).
| Variável | Obrigatória | Descrição |
|---|---|---|
| `LOCALLLM_MODEL` | Sim | Caminho do `.gguf`; sem ele o engine aborta (`model_path is empty`) |
| `LOCALLLM_ADDR` | Não | Endereço de bind; default `0.0.0.0:8080` |
| `LOCALLLM_MODEL_ALIASES` | Não | Aliases do modelo, separados por vírgula, expostos em `GET /v1/models` |
| `LOCALLLM_PROFILE` | Não | `balanced_9b` ou `cpu_fallback`; ausente ou outro valor = `Turbo4B` |
| `LOCALLLM_GPU_LAYERS` | Não | Camadas na GPU; default do perfil (999 em `Turbo4B`/`balanced_9b`, 0 em `cpu_fallback`) |
| `LOCALLLM_CTX` | Não | Janela de contexto; default do perfil (4096, 2048 e 2048) |
| `LOCALLLM_BATCH` | Não | `n_batch`; sem valor explícito, usa o do perfil limitado a 4096 |
| `LOCALLLM_UBATCH` | Não | `n_ubatch`; default do perfil |
| `LOCALLLM_THREADS` | Não | Threads de geração; default do perfil |
| `LOCALLLM_THREADS_BATCH` | Não | Threads de prefill; default do perfil |
| `LOCALLLM_API_KEY` | Não | Habilita autenticação por chave; vazia ou ausente = API aberta |
| `LOCALLLM_AUTH_HEADER` | Não | Cabeçalho preferencial da chave; default `authorization` (também aceita `x-api-key`) |
| `LOCALLLM_AUTH_SCHEME` | Não | Prefixo removido antes da comparação (ex.: `Bearer`) |
| `LOCALLLM_CORS_ORIGINS` | Não | Allowlist de origens separadas por vírgula; default `*` |
| `LOCALLLM_SYSTEM_PROMPT` | Não | Prompt de sistema usado quando a request não traz `system` nem `developer` |
| `LOCALLLM_ENABLE_TOOL_CALLS` | Não | Liga/desliga tool calling; default ligado |
| `LOCALLLM_LOG_FORMAT` | Não | `json` para logs estruturados; default `pretty` |
| `LLAMA_DLL_PATH` | Não | Caminho completo do `llama.dll`; sobrepõe a resolução automática do runtime |
| `RUST_LOG` | Não | Filtro de logs do `tracing`; default `info` |
A pasta de modelos **não** é variável de ambiente: na CLI é a flag global `--models-dir` (default `./models`); no app é o campo `models_dir` da configuração do runtime. Ao iniciar o servidor local, o app define `LOCALLLM_MODEL`, `LOCALLLM_ADDR`, `LOCALLLM_GPU_LAYERS` e `LOCALLLM_CTX`.
## 🧱 Arquitetura
```mermaid
flowchart LR
UI["App Tauri (React + Rust)"] -->|"HTTP e SSE"| SRV["inference-server (Axum)"]
CLI["localllm-cli"] -->|"HTTP"| SRV
SRV --> CORE["inference-core (engine, sessão, perfis)"]
CORE --> SYS["llama-sys (llama.dll via libloading)"]
SRV --> DEPS["runtime-deps (download + SHA-256)"]
DEPS -->|"llama.cpp b8358"| SYS
UI -->|"busca e download de .gguf"| HF["Hugging Face"]
```
- O app (no modo `local`) inicia e para o `inference-server` e usa `GET /health` para saber se está pronto; no modo `remote` apenas fala com a API.
- A CLI é um cliente HTTP da mesma API.
- O `inference-server` valida a request (auth, CORS, aliases) e delega ao `inference-core`, que mantém o engine, a sessão e o sampler.
- O `inference-core` usa o `llama-sys`, que carrega `llama.dll` dinamicamente; o `runtime-deps` resolve ou baixa esse runtime a partir de [`vendor/llama-runtime/manifest.json`](vendor/llama-runtime/manifest.json).
- O app busca e baixa modelos direto do Hugging Face, sem passar pela API local.
## 🗂️ Estrutura
```text
crates/
api-contracts/ tipos compartilhados (chat, modelos, health, erros)
inference-core/ engine de inferência, sessão, sampler e perfis
inference-server/ API HTTP em Axum (binário localllm)
llama-sys/ carga dinâmica do llama.cpp (libloading)
metrics/ contadores de tokens e throughput
runtime-deps/ resolve e baixa o runtime llama.cpp
localllm-cli/ CLI (binário localllm)
tauri-app/ app desktop: React 18 + Vite + Tailwind, backend Tauri 2
ui/ páginas HTML estáticas de desenvolvimento (index.html, chat.html)
docs/ documentação detalhada
```
## 🧪 Scripts
| Comando | O que faz |
|---|---|
| `cargo run -p inference-server` | Sobe a API em `0.0.0.0:8080` (exige `LOCALLLM_MODEL`) |
| `cargo run -p localllm-cli -- <comando>` | Executa a CLI |
| `cargo test` | Testes dos crates do workspace |
| `cargo test --manifest-path tauri-app/src-tauri/Cargo.toml` | Testes do backend Tauri (fora do workspace) |
| `npm run dev` (em `tauri-app`) | Só o frontend, Vite na porta 1420 |
| `npm run tauri dev` | App desktop completo |
| `npm run build` | `tsc && vite build` |
| `npm run preview` | `vite preview` |
| `npm test` | `vitest run` |
| `npm run tauri` | Repassa argumentos ao `tauri` CLI |
| `scripts/build-dev.sh` (há `.ps1`) | Compila `vendor/llama.cpp` em Debug com CUDA |
| `scripts/build-prod.sh` (há `.ps1`) | Compila `vendor/llama.cpp` em Release com LTO e CUDA (arch 86) |
## 📚 Documentação
| Documento | Conteúdo |
|---|---|
| [`docs/README.md`](docs/README.md) | Índice da documentação |
| [`docs/api/overview.md`](docs/api/overview.md) · [`endpoints.md`](docs/api/endpoints.md) · [`configuration.md`](docs/api/configuration.md) | API: visão geral, endpoints e configuração |
| [`docs/cli/overview.md`](docs/cli/overview.md) · [`commands.md`](docs/cli/commands.md) | CLI: visão geral e comandos |
| [`docs/app/overview.md`](docs/app/overview.md) · [`user-guide.md`](docs/app/user-guide.md) · [`backend-commands.md`](docs/app/backend-commands.md) | App: visão geral, guia de uso e comandos Tauri |
| [`docs/developer/architecture.md`](docs/developer/architecture.md) | Mapa do workspace e fluxos entre módulos |
| [`.planning/ROADMAP.md`](.planning/ROADMAP.md) | Fases e estado do desenvolvimento |
## 🛣️ Limitações e próximos passos
- A API expõe só três endpoints; a compatibilidade com a OpenAI é parcial: `n` precisa ser `1`, `parallel_tool_calls` não é recurso suportado e `content` em partes vira texto consolidado.
- O raciocínio não é um campo separado na API; quem separa `<think>…</think>` é o frontend do app.
- Na CLI, a execução ao vivo está mais consolidada em `status`, `models list` e `bench throughput`; o resto da superfície está mais avançado no parser do que nas capacidades HTTP públicas.
- O chat usa `sql.js` carregado por CDN e persiste o banco exportado em `localStorage`; não é um banco nativo do sistema.
- No modo local o app precisa encontrar o binário `localllm`, e o runtime manager faz polling do snapshot (sem eventos push).
- O manifesto do runtime só lista variantes `windows-x64`; em outros alvos o servidor só sobe com um runtime já presente (binário adjacente ou `LLAMA_DLL_PATH`).
- `scripts/build-dev.sh` e `scripts/build-prod.sh` exigem o submódulo `vendor/llama.cpp`, que não está declarado no repositório (não há `.gitmodules`).
- `inference-server` e `localllm-cli` declaram ambos um binário chamado `localllm`; use `cargo run -p <crate>` para escolher.
- Próximos passos registrados no roadmap: concluir a fase 5 (validação de fontes de modelo, com seus três planos marcados como feitos mas a fase ainda aberta) e a fase 6 (redesign do chat e da página de Configurações, 0/3 planos).
## 🤝 Contribuindo
Abra uma issue ou PR descrevendo a mudança. Antes de enviar, rode os testes que cobrem o que você alterou:
```bash
cargo test && (cd tauri-app && npm test)
```
## 📄 Licença
MIT, declarada em [`Cargo.toml`](Cargo.toml); o repositório não inclui arquivo `LICENSE`. Os binários de inferência vêm do [llama.cpp](https://github.com/ggml-org/llama.cpp) e os modelos do [Hugging Face](https://huggingface.co) seguem a licença de cada autor.