Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 72 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,77 +1,85 @@
<p align="right"><a href="README_EN.md">English</a></p>

# ModelHub

<div align="center">

![ModelHub](https://img.shields.io/badge/ModelHub-AI%20Gateway-blue?style=for-the-badge)

**Gateway unificado para múltiplos provedores de IA com API compatível com OpenAI**

[![CI](https://github.com/actus7/modelhub/actions/workflows/ci.yml/badge.svg)](https://github.com/actus7/modelhub/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Node.js](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen)](https://nodejs.org)
[![Next.js](https://img.shields.io/badge/Next.js-16.2-black)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/actus7/modelhub)

[Sobre](#sobre) • [Funcionalidades](#funcionalidades) • [Setup](#setup-local) • [API](#api) • [Arquitetura](#arquitetura) • [CI/CD](#cicd)

<img src="public/logo.png" alt="ModelHub" width="120" height="120" />

<h1>ModelHub</h1>

<p><strong>Gateway unificado para múltiplos provedores de IA com API compatível com OpenAI.</strong></p>

<p>
<a href="https://github.com/actus7/modelhub/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/actus7/modelhub/actions/workflows/ci.yml/badge.svg" /></a>
<a href="LICENSE"><img alt="Licença MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
<a href="https://nodejs.org"><img alt="Node.js >= 22" src="https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen" /></a>
<a href="https://nextjs.org/"><img alt="Next.js 16.2" src="https://img.shields.io/badge/Next.js-16.2-black" /></a>
<a href="https://www.typescriptlang.org/"><img alt="TypeScript 5" src="https://img.shields.io/badge/TypeScript-5.x-blue" /></a>
</p>

<p>
<a href="#visao-geral">Visão geral</a> ·
<a href="#funcionalidades">Funcionalidades</a> ·
<a href="#quickstart">Quickstart</a> ·
<a href="#api">API</a> ·
<a href="#arquitetura">Arquitetura</a> ·
<a href="#deploy">Deploy</a>
</p>

<p>
<a href="https://vercel.com/new/clone?repository-url=https://github.com/actus7/modelhub"><img alt="Deploy with Vercel" src="https://vercel.com/button" /></a>
</p>
</div>

---

## Sobre
## Visão geral

ModelHub é uma plataforma open-source para centralizar o acesso a vários provedores de IA em uma única interface. Ele expõe uma API compatível com OpenAI, uma interface de chat, gerenciamento de credenciais por usuário e dashboard de uso.
ModelHub centraliza OpenAI, Google, Groq, Mistral, OpenRouter e outros provedores em uma única plataforma open-source. Ele entrega uma API compatível com OpenAI, chat web autenticado, gerenciamento seguro de credenciais e dashboard de uso.

A ideia é simples: em vez de cada aplicação integrar separadamente OpenAI, Google, Groq, Mistral, OpenRouter e outros, o ModelHub fica no meio e padroniza autenticação, roteamento, logs, custos e fallbacks.
Em vez de cada aplicação integrar vários provedores separadamente, o ModelHub padroniza autenticação, roteamento, logs, custos, catálogo de modelos e fallbacks.

## Funcionalidades

- **API Gateway OpenAI-compatible**: endpoints `/v1/chat/completions` e `/v1/models` para uso programático.
- **Chat web autenticado**: interface pronta para conversar com modelos configurados.
- **Gerenciamento de API keys**: crie chaves ModelHub para clientes, scripts e integrações.
- **Credenciais por provider**: salve chaves de provedores com criptografia no banco.
- **Dashboard de uso**: acompanhe requests, custos estimados, status codes, tokens e logs recentes.
- **Roteamento inteligente**: tiers por complexidade, overrides por tarefa e fallbacks quando modelos falham.
- **Suporte a anexos**: imagens, PDFs e documentos no fluxo de chat.
- **Catálogo dinâmico de modelos**: lista modelos locais e busca modelos remotos quando o provider suporta.
- **Rate limit e cooldown**: proteção básica contra abuso e provedores instáveis.
- **Deploy Vercel-ready**: build e preview integrados ao fluxo de PR.
<table>
<tr>
<td><strong>API OpenAI-compatible</strong><br />Use <code>/v1/chat/completions</code> e <code>/v1/models</code> com clientes existentes.</td>
<td><strong>Chat web</strong><br />Interface autenticada para conversar com modelos configurados.</td>
</tr>
<tr>
<td><strong>Credenciais seguras</strong><br />API keys ModelHub e chaves de provedores criptografadas por usuário.</td>
<td><strong>Dashboard de uso</strong><br />Requests, custos estimados, status codes, tokens e logs recentes.</td>
</tr>
<tr>
<td><strong>Roteamento inteligente</strong><br />Tiers por complexidade, overrides por tarefa e fallbacks automáticos.</td>
<td><strong>Anexos no chat</strong><br />Suporte a imagens, PDFs e documentos.</td>
</tr>
<tr>
<td><strong>Catálogo dinâmico</strong><br />Modelos locais e busca remota quando o provider suporta.</td>
<td><strong>Pronto para produção</strong><br />Rate limit, cooldown, headers de segurança, CI e deploy na Vercel.</td>
</tr>
</table>

## Provedores

O catálogo fica em `server/lib/catalog.ts` e cada adapter vive em `server/providers/`.

Provedores suportados incluem:

- OpenAI
- Google AI Studio
- Groq
- Mistral, incluindo Codestral como modelo Mistral
- OpenRouter
- HuggingFace
- DeepSeek
- Perplexity
- Together AI
- Fireworks AI
- Cohere
- Cloudflare Workers AI
- Ollama e Ollama Cloud
- GitHub Models
- GitHub Copilot
- Qwen e Qwen Token Plan
- Z.ai e Z.ai Coding Plan
- Moonshot/Kimi
- NVIDIA NIM
- Pollinations
- Puter
| Suportados | |
|---|---|
| OpenAI | Google AI Studio |
| Groq | Mistral / Codestral |
| OpenRouter | HuggingFace |
| DeepSeek | Perplexity |
| Together AI | Fireworks AI |
| Cohere | Cloudflare Workers AI |
| Ollama / Ollama Cloud | GitHub Models |
| GitHub Copilot | Qwen / Qwen Token Plan |
| Z.ai / Z.ai Coding Plan | Moonshot / Kimi |
| NVIDIA NIM | Pollinations / Puter |

Providers quebrados ou duplicados devem ser removidos do catálogo e do registry para não aparecerem na tela de integrações.

## Requisitos
## Quickstart

### Requisitos

- Node.js >= 22
- pnpm >= 10
Expand All @@ -80,7 +88,7 @@ Providers quebrados ou duplicados devem ser removidos do catálogo e do registry
- `ENCRYPTION_KEY` de 64 caracteres hexadecimais
- Chaves dos provedores que você pretende usar

## Setup local
### Instalação local

```bash
git clone https://github.com/actus7/modelhub.git
Expand Down Expand Up @@ -185,11 +193,13 @@ O campo `model` segue o formato `provider/model`, por exemplo:

## Interface web

- `/chat`: conversa com provedores configurados.
- `/setup`: tela de integrações e credenciais por provider.
- `/dashboard`: API keys, uso, custos, logs e routing.
- `/account`: informações da conta.
- `/playground`: comparação/teste de providers.
| Rota | Descrição |
|---|---|
| `/chat` | Conversa com provedores configurados |
| `/setup` | Integrações e credenciais por provider |
| `/dashboard` | API keys, uso, custos, logs e routing |
| `/account` | Informações da conta |
| `/playground` | Comparação e teste de providers |

Rotas autenticadas são protegidas por `proxy.ts`.

Expand Down Expand Up @@ -226,17 +236,7 @@ A aplicação usa duas camadas:

O banco é PostgreSQL via Neon, acessado com Prisma 7 e `@prisma/adapter-neon`.

Modelos importantes:

- `User`
- `ApiKey`
- `ProviderCredential`
- `Conversation`
- `Message`
- `ConversationAttachment`
- `UsageLog`
- `UserMemory`
- `UserSettings`
Modelos importantes: `User`, `ApiKey`, `ProviderCredential`, `Conversation`, `Message`, `ConversationAttachment`, `UsageLog`, `UserMemory` e `UserSettings`.

Para mudanças de schema:

Expand Down Expand Up @@ -293,8 +293,6 @@ docker run --env-file .env -p 3000:3000 modelhub

Veja `CONTRIBUTING.md`.

Fluxo recomendado:

```bash
git checkout -b fix/minha-mudanca
pnpm test
Expand Down Expand Up @@ -322,10 +320,4 @@ MIT. Veja `LICENSE`.

## Agradecimentos

- Next.js
- Hono
- Prisma
- Neon
- shadcn/ui
- Vitest
- Comunidade open-source
Next.js · Hono · Prisma · Neon · shadcn/ui · Vitest · Comunidade open-source
95 changes: 72 additions & 23 deletions components/chat/chat-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Image from "next/image";
import { useSearchParams } from "next/navigation";
import {
type CloudDeploymentSummary,
type ProviderModel,
type UiProvider,
} from "@/lib/contracts";
import {
Expand Down Expand Up @@ -137,6 +138,13 @@ import {
type PersistedConversationMessage,
} from "@/lib/chat-utils";

const AUTO_PROVIDER_ID = "modelhub-auto";
const AUTO_MODEL: ProviderModel = {
capabilities: { documents: true, images: true, reasoning: true },
id: "auto",
name: "Auto · Smart Routing",
};

export function ChatPage() {
const { credentials, providers, refreshCredentials } = useAppState();
const [selectedProviderId, setSelectedProviderId] = useState<string>("");
Expand Down Expand Up @@ -218,6 +226,22 @@ export function ChatPage() {
}
}, [searchParams, openclawDeployments]);

const autoProvider = useMemo<UiProvider>(() => ({
base: "/v1",
category: "gateway",
hasModels: true,
id: AUTO_PROVIDER_ID,
label: "Auto · Smart Routing",
localModels: [AUTO_MODEL],
runtime: {
authMode: "none",
externalApi: false,
kind: "server",
openAiCompatible: true,
transport: "openai-compatible",
},
}), []);

const openclawProviders = useMemo<UiProvider[]>(
() => {
const providerLabels = new Map(providers.map((provider) => [provider.id, provider.label]));
Expand Down Expand Up @@ -253,9 +277,9 @@ export function ChatPage() {

const selectedProvider = useMemo(
() =>
[...providers, ...openclawProviders].find((provider) => provider.id === selectedProviderId) ??
[autoProvider, ...providers, ...openclawProviders].find((provider) => provider.id === selectedProviderId) ??
null,
[providers, openclawProviders, selectedProviderId],
[autoProvider, providers, openclawProviders, selectedProviderId],
);
const browserProviderAdapter = useMemo(
() => getBrowserChatProviderAdapter(selectedProviderId),
Expand Down Expand Up @@ -345,7 +369,7 @@ export function ChatPage() {

const preferred =
(globalThis.window?.localStorage.getItem("selected-provider") ?? null) ??
(providers.find((provider) => provider.id === "gateway")?.id ?? providers[0]?.id ?? "");
AUTO_PROVIDER_ID;
setSelectedProviderId(preferred);
}, [providers, selectedProviderId]);

Expand Down Expand Up @@ -824,14 +848,21 @@ export function ChatPage() {
setBrowserProviderAuthState("signed-in");
} else {
let parsedStream: Awaited<ReturnType<typeof parseChatStream>> | null = null;
const response = await apiFetch(`${selectedProvider.base}/api/chat`, {
body: JSON.stringify(requestPayload),
headers: {
"Content-Type": "application/json",
const response = await apiFetch(
selectedProviderId === AUTO_PROVIDER_ID ? "/v1/chat/completions" : `${selectedProvider.base}/api/chat`,
{
body: JSON.stringify(
selectedProviderId === AUTO_PROVIDER_ID
? { messages: nextConversation, model: selectedModelId }
: requestPayload,
),
headers: {
"Content-Type": "application/json",
},
method: "POST",
signal: controller.signal,
},
method: "POST",
signal: controller.signal,
});
);

if (!response.ok) {
const errorMessage = await parseApiErrorResponse(response);
Expand Down Expand Up @@ -981,19 +1012,27 @@ export function ChatPage() {
const titleConvId = convId;
void (async () => {
try {
const titleResponse = await apiFetch(`${selectedProvider.base}/api/chat`, {
body: JSON.stringify({
messages: [
{
role: "user",
parts: [{ type: "text", text: buildTitleGenerationPrompt(text, fullText) }],
},
],
modelId: selectedProvider.hasModels ? selectedModelId : undefined,
}),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const titleMessages = [
{
role: "user",
parts: [{ type: "text", text: buildTitleGenerationPrompt(text, fullText) }],
},
];
const titleResponse = await apiFetch(
selectedProviderId === AUTO_PROVIDER_ID ? "/v1/chat/completions" : `${selectedProvider.base}/api/chat`,
{
body: JSON.stringify(
selectedProviderId === AUTO_PROVIDER_ID
? { messages: titleMessages, model: selectedModelId }
: {
messages: titleMessages,
modelId: selectedProvider.hasModels ? selectedModelId : undefined,
},
),
headers: { "Content-Type": "application/json" },
method: "POST",
},
);
if (titleResponse.ok) {
const titleResult = await parseChatStream(titleResponse, {});
const cleanTitle = titleResult.text.trim().replaceAll(/^["']|["']$/g, "").slice(0, 100);
Expand Down Expand Up @@ -1181,6 +1220,16 @@ export function ChatPage() {
<SelectValue placeholder="Provider" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Roteamento</SelectLabel>
<SelectItem value={AUTO_PROVIDER_ID}>
<span className="flex min-w-0 items-center gap-1.5">
<span className="truncate">Auto · Smart Routing</span>
<SparklesIcon className="size-3 shrink-0 text-primary" aria-label="Smart Routing" />
</span>
</SelectItem>
</SelectGroup>
<SelectSeparator />
{openclawProviders.length > 0 && (
<SelectGroup>
<SelectLabel>Ambientes OpenClaw</SelectLabel>
Expand Down
Loading
Loading