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
17 changes: 16 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
OPENROUTER_API_KEY=your-key-here
# Model provider API keys — set at least one. All providers are equal;
# keys can also be added later in Admin → Model Providers.
OPENROUTER_API_KEY=
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
GOOGLE_API_KEY=
DEEPSEEK_API_KEY=
GROQ_API_KEY=
CEREBRAS_API_KEY=
MISTRAL_API_KEY=
XAI_API_KEY=
ZAI_API_KEY=
FIREWORKS_API_KEY=
MINIMAX_API_KEY=
HUGGINGFACE_API_KEY=

BRAVE_SEARCH_API_KEY=your-key-here
# 32+ char secrets. Generate each with: openssl rand -hex 32
CREDENTIALS_KEY=
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Self-hosted. Multi-user. Runs unattended.
git clone https://github.com/0-AI-UG/zero-agent.git
cd zero-agent && bun install

cp .env.example .env # add OPENROUTER_API_KEY, generate JWT_SECRET + CREDENTIALS_KEY
cp .env.example .env # add a model provider API key, generate JWT_SECRET + CREDENTIALS_KEY
bun run dev
```

Expand Down Expand Up @@ -105,12 +105,12 @@ The same image backs `docker-compose.yml`, so `docker compose pull` fetches it i

| Variable | Description |
|---|---|
| `OPENROUTER_API_KEY` | Required. [OpenRouter](https://openrouter.ai) key. |
| `OPENROUTER_API_KEY` · `ANTHROPIC_API_KEY` · `OPENAI_API_KEY` · … | At least one model provider key is required; all providers are configured the same way (see `.env.example` for the full list). |
| `JWT_SECRET` · `CREDENTIALS_KEY` | Required. ≥32 chars each (`openssl rand -hex 32`). |
| `BRAVE_SEARCH_API_KEY` | Optional. Web search. |
| `APP_URL` · `RP_ID` · `CORS_ORIGIN` · `TRUST_PROXY` | Set in production. |

Models, image providers, and per-user limits are configured at runtime via the admin panel.
Models, provider keys, capability routing (embeddings / image generation / captioning), and per-user limits are configured at runtime via the admin panel.

## Tech Stack

Expand Down
33 changes: 27 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"trustedDependencies": ["sharp", "bcrypt", "better-sqlite3", "esbuild"],
"dependencies": {
"@0-ai/s3lite": "^0.6.0",
"@ai-sdk/anthropic": "^3",
"@ai-sdk/google": "^3",
"@ai-sdk/openai": "^3",
"@ai-sdk/openai-compatible": "^2",
"@anthropic-ai/sandbox-runtime": "^0.0.50",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
Expand All @@ -30,14 +34,13 @@
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@hono/node-server": "^1.19.12",
"@earendil-works/pi-agent-core": "^0.78.0",
"@earendil-works/pi-ai": "^0.78.0",
"@earendil-works/pi-coding-agent": "^0.78.0",
"@earendil-works/pi-tui": "^0.78.0",
"@hono/node-server": "^1.19.12",
"@mozilla/readability": "^0.6.0",
"@openrouter/ai-sdk-provider": "^2.8.0",
"@openrouter/sdk": "^0.12.3",
"@simplewebauthn/server": "^13.3.0",
"@types/ws": "^8.18.1",
"ai": "^6.0.164",
Expand Down
5 changes: 2 additions & 3 deletions server/cli-handlers/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
* embeddings without needing direct API key access.
*/
import type { z } from "zod";
import { embed } from "@/lib/openrouter/embed.ts";
import { getEmbeddingModelId } from "@/lib/providers/index.ts";
import { embed } from "@/lib/inference/embed.ts";
import { isEmbeddingConfigured } from "@/lib/search/vectors.ts";
import type { CliContext } from "./context.ts";
import { ok, fail } from "./response.ts";
Expand All @@ -19,7 +18,7 @@ export async function handleEmbed(
return fail("not_configured", "Embedding model is not configured", 503);
}

const embeddings = await embed(input.texts, { model: getEmbeddingModelId() });
const embeddings = await embed(input.texts);

return ok({
embeddings,
Expand Down
6 changes: 3 additions & 3 deletions server/cli-handlers/image.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/**
* Image generation handler — wraps generateImageViaOpenRouter, writes the
* Image generation handler — wraps generateImageViaProvider, writes the
* bytes into the project directory so the agent can read them back, and
* inserts a `files` row. The inotify watcher converges FTS / vectors
* after the file lands.
*/
import type { z } from "zod";
import { generateImageViaOpenRouter } from "@/lib/media/image.ts";
import { generateImageViaProvider } from "@/lib/media/image.ts";
import { insertFile } from "@/db/queries/files.ts";
import { createFolder as createFolderRecord, getFolderByPath } from "@/db/queries/folders.ts";
import { writeProjectFile, workspacePathFor } from "@/lib/projects/fs-ops.ts";
Expand All @@ -31,7 +31,7 @@ export async function handleImageGenerate(
ctx: CliContext,
input: z.infer<typeof ImageGenerateInput>,
): Promise<Response> {
const image = await generateImageViaOpenRouter(input.prompt);
const image = await generateImageViaProvider(input.prompt);
const timestamp = Date.now();
const rawPath = input.path ?? `images/${timestamp}.png`;
const filePath = sanitizePath(rawPath);
Expand Down
9 changes: 6 additions & 3 deletions server/cli-handlers/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* without needing direct API key access.
*/
import type { z } from "zod";
import { generateText } from "@/lib/openrouter/text.ts";
import { generateText } from "@/lib/inference/text.ts";
import { getScriptsModelId } from "@/lib/providers/index.ts";
import { resolveModelForPi } from "@/lib/pi/model.ts";
import { insertUsageLog } from "@/db/queries/usage-logs.ts";
import type { CliContext } from "./context.ts";
import { ok } from "./response.ts";
Expand All @@ -16,10 +17,12 @@ export async function handleLlmGenerate(
input: z.infer<typeof LlmGenerateInput>,
): Promise<Response> {
const model = getScriptsModelId(ctx.projectId);
const resolved = resolveModelForPi(model);

const result = await generateText({
model,
messages: input.prompt,
provider: resolved.provider,
model: resolved.modelId,
prompt: input.prompt,
system: input.system,
maxOutputTokens: input.maxTokens ?? 4096,
});
Expand Down
4 changes: 2 additions & 2 deletions server/db/queries/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface ModelInput {
enabled?: boolean;
sortOrder?: number;
thinkingLevel?: ThinkingLevel | null;
piProvider?: string;
piProvider: string;
piModelId?: string | null;
}

Expand All @@ -64,7 +64,7 @@ export function insertModel(data: ModelInput): ModelRow {
data.enabled !== false ? 1 : 0,
data.sortOrder ?? 0,
data.thinkingLevel ?? null,
data.piProvider ?? "openrouter",
data.piProvider,
data.piModelId ?? null,
);
return getById.get(data.id) as ModelRow;
Expand Down
4 changes: 4 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ import {
handleAcceptUserInvitation,
} from "@/routes/user-invitations.ts";
import { handleSetupStatus, handleSetupComplete } from "@/routes/setup.ts";
import { handleListProviders } from "@/routes/providers.ts";
import { handleGetSettings, handleUpdateSettings, handleListImageModels } from "@/routes/settings.ts";
import { handleEmailFeatureStatus, handleEmailFeatureToggle } from "@/routes/admin-email.ts";
import {
Expand Down Expand Up @@ -428,6 +429,9 @@ app.post("/api/pending-responses/:id/respond", h(handleRespondPendingResponse));
app.get("/api/setup/status", h(handleSetupStatus));
app.post("/api/setup/complete", h(handleSetupComplete));

// Model providers (no auth required — static, no secrets)
app.get("/api/providers", h(handleListProviders));

// Admin
app.get("/api/admin/users", h(handleListUsers));
app.post("/api/admin/users", h(handleCreateUser));
Expand Down
27 changes: 0 additions & 27 deletions server/lib/ai/provider.ts

This file was deleted.

4 changes: 2 additions & 2 deletions server/lib/chat-providers/email/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { generateId, db } from "@/db/index.ts";
import type { ChatRow, ProjectRow } from "@/db/types.ts";
import { runTurn } from "@/lib/pi/run-turn.ts";
import { resolveModelForPi } from "@/lib/pi/model.ts";
import { getActiveProvider } from "@/lib/providers/index.ts";
import { getDefaultChatModelId } from "@/lib/providers/index.ts";
import { beginChatStream, endChatStream, publishPiEvent } from "@/lib/http/ws.ts";
import { events as eventBus } from "@/lib/tasks/events.ts";

Expand Down Expand Up @@ -229,7 +229,7 @@ async function runEmailAgentTurn(
const isFirstTurn = !existsSync(join(sessionsDirFor(project.id), `${chatId}.jsonl`));
const priorOutbound = isFirstTurn ? outboundForChat(project.id, chatId) : [];
const userText = composeUserMessage(parsed, attachments, priorOutbound, inboundEmailId);
const chatModelId = getActiveProvider().getDefaultChatModelId();
const chatModelId = getDefaultChatModelId();
const resolved = resolveModelForPi(chatModelId);

beginChatStream(chatId, "");
Expand Down
Loading
Loading