Skip to content
Open
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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ ORM entities are in `src/gateway/models/entities.py` (User, APIKey, Budget, Usag
- Docker local build/run: `docker compose up --build`
- CI Docker smoke check is implemented in `scripts/docker_liveness_check.sh`.
## Web Dashboard (`web/`)
- The standalone admin dashboard is a React + HeroUI v3 SPA in `web/` (Vite, Tailwind v4, TanStack Query). It browses the model catalogue, sets model pricing, manages aliases, and toggles runtime settings; it calls the management API (`/v1/models`, `/v1/pricing`, `/v1/aliases`, `/v1/settings`) with the master key, entered on a sign-in screen and kept only in the browser tab's session storage.
- The standalone admin dashboard is a React + HeroUI v3 SPA in `web/` (Vite, Tailwind v4, TanStack Query). It browses the model catalogue, sets model pricing, manages aliases, adds/edits provider API keys, and toggles runtime settings; it calls the management API (`/v1/models`, `/v1/pricing`, `/v1/aliases`, `/v1/provider-credentials`, `/v1/settings`) with the master key, entered on a sign-in screen and kept only in the browser tab's session storage.
- Runtime provider management: the Providers page (`web/src/pages/ProvidersPage.tsx`) manages the `provider_credentials` table via `/v1/provider-credentials` (CRUD + `POST /{instance}/test`). Backend: `services/provider_store_service.py` overlays stored providers onto `config.providers` (per-config baseline on `config._provider_baseline`, refreshed by a TTL refresher wired in the lifespan like the alias refresher, standalone-only); `services/secret_box.py` encrypts keys with `OTARI_SECRET_KEY` (Fernet); `services/master_key_service.py` generates + prints a master key on first run when none is set (hash in `runtime_settings`). Keys are write-only over the API (responses expose only `last4`).
- Build: `npm --prefix web ci && npm --prefix web run build`. Output goes to `src/gateway/static/dashboard/` (set in `web/vite.config.ts`), which is committed so the wheel and Docker image ship the dashboard with no Node build stage. Rebuild and commit after any change under `web/src`.
- Checks: `npm --prefix web run typecheck`, `npm --prefix web test`. CI runs these and fails if the committed bundle is stale (`.github/workflows/otari-dashboard.yml`).
- Serving: standalone mode serves `index.html` at `/` and hashed assets under `/assets` (`src/gateway/main.py`, `src/gateway/dashboard.py`); the get-started tutorial moved to `/welcome`. Hybrid mode has no local management API, so `/` keeps serving the tutorial. Navigation is client-side section switching, so no server catch-all route is needed.
- Serving: standalone mode serves `index.html` at `/` and hashed assets under `/assets` (`src/gateway/main.py`, `src/gateway/dashboard.py`); the get-started tutorial moved to `/welcome`. Hybrid mode has no local management API, so `/` keeps serving the tutorial. Client-side routing uses react-router's `HashRouter` (routes live under `/#/providers`, `/#/models`, `/#/aliases`, `/#/settings`), so hash routes need no server catch-all and the API/asset paths are never shadowed.
## Lint / Typecheck Commands
- Ruff is configured for linting (rules: `E`, `F`, `I`; line length: 120) in `pyproject.toml`.
- Run lint checks with `make lint` (or `uv run ruff check src tests scripts`).
Expand Down
42 changes: 37 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Prefer a typed client? Use one of the [Otari SDKs](#the-otari-ecosystem) for Pyt
The Quickstart touches three different keys. Keeping them straight saves the most common first-run error:

- **Provider key** (`OPENAI_API_KEY`): your real OpenAI secret. It goes in as an environment variable and stays inside Otari. Your apps never see it.
- **Master key** (`OTARI_MASTER_KEY`): manages Otari. Use it to create and revoke API keys, not to make requests.
- **Master key** (`OTARI_MASTER_KEY`): manages Otari. Use it to create and revoke API keys, not to make requests. Leave it unset and Otari generates one on first run and prints it to the logs.
- **API key** (`gw-...`): what clients send to Otari in the `Authorization` header. The bootstrap key is one of these.

To mint a named key yourself instead of using the bootstrap key, call the management endpoint with your master key:
Expand All @@ -145,6 +145,33 @@ curl -X POST http://localhost:8000/v1/keys \

The returned `gw-` key is shown in full only once.

### Set it up in the browser instead

Prefer not to pre-set anything on the command line? Launch Otari bare and finish in the dashboard.

```bash
docker run --rm -p 8000:8000 mzdotai/otari:latest otari serve
```

With no master key set, Otari generates one and prints it to the logs once on startup:

```text
Otari first-run: no master key was set, so one was generated.
Save it now (it is shown only once). Sign in to the dashboard at http://localhost:8000/
Your master key:
otari-mk-...
```

Open `http://localhost:8000/`, sign in with that key, then open **Providers** and add your OpenAI key. Storing a provider key requires `OTARI_SECRET_KEY`, a Fernet key that encrypts credentials at rest; generate one with `otari gen-secret-key` and set it before launch:

```bash
docker run --rm -p 8000:8000 \
-e OTARI_SECRET_KEY="$(docker run --rm mzdotai/otari:latest otari gen-secret-key)" \
mzdotai/otari:latest otari serve
```

Back it up somewhere safe: losing `OTARI_SECRET_KEY` makes every stored provider key undecryptable. Providers you set in `config.yml` keep working and appear in the dashboard marked `config` (read-only there); keys you add in the UI are marked `stored`, and a stored key of the same name takes precedence.

## Run the full stack

The Quickstart runs the gateway alone on SQLite. To get a durable database plus the built-in tools and guardrails, run the full stack with Docker Compose.
Expand Down Expand Up @@ -191,10 +218,15 @@ For hot reload against a local `.env`, use `make dev`.
### Admin dashboard

In standalone mode the gateway serves a web admin dashboard at the root URL
(`http://localhost:8000`). Sign in with your master key (`OTARI_MASTER_KEY`) to
browse the model catalogue, set model pricing, manage aliases, and toggle
runtime settings (model discovery and default pricing). The get-started
tutorial page moved to `/welcome`. The dashboard is a React +
(`http://localhost:8000`). Sign in with your master key (`OTARI_MASTER_KEY`, or
the one printed to the logs on first run) to add provider API keys, browse the
model catalogue, set model pricing, manage aliases, and toggle runtime settings
(model discovery and default pricing). Provider keys added in the **Providers**
page are encrypted at rest (requires `OTARI_SECRET_KEY`; see
[Set it up in the browser instead](#set-it-up-in-the-browser-instead)) and merge
over `config.yml` providers, so you can start with an empty config and add
credentials after launch. The get-started tutorial page moved to `/welcome`. The
dashboard is a React +
HeroUI app that lives in `web/`; its built bundle is committed under
`src/gateway/static/dashboard`, so the published package and Docker image serve
it with no extra build step. See [`web/README.md`](web/README.md) to work on it.
Expand Down
39 changes: 39 additions & 0 deletions alembic/versions/b4d6f8a0c2e4_add_provider_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Add provider_credentials table.

Revision ID: b4d6f8a0c2e4
Revises: a2c4e6f8b0d1
Create Date: 2026-07-17 00:00:00.000000

"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "b4d6f8a0c2e4"
down_revision: str | Sequence[str] | None = "a2c4e6f8b0d1"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
op.create_table(
"provider_credentials",
sa.Column("instance", sa.String(), nullable=False),
sa.Column("provider_type", sa.String(), nullable=True),
sa.Column("api_base", sa.String(), nullable=True),
sa.Column("encrypted_api_key", sa.String(), nullable=True),
sa.Column("last4", sa.String(), nullable=True),
sa.Column("client_args", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("instance"),
)


def downgrade() -> None:
"""Downgrade schema."""
op.drop_table("provider_credentials")
29 changes: 28 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ Note the `require_pricing` interaction: it defaults to `true` (fail-closed), so

| Variable | Description |
|----------|-------------|
| `OTARI_MASTER_KEY` | Master key for management endpoints |
| `OTARI_MASTER_KEY` | Master key for management endpoints. When unset in standalone mode, one is generated on first run and printed to the logs (see [Runtime provider management](#runtime-provider-management)). |
| `OTARI_SECRET_KEY` | Fernet key that encrypts provider credentials added through the dashboard. Required to store a provider key in the UI. Generate one with `otari gen-secret-key`. |
| `OTARI_DATABASE_URL` | Database connection URL |
| `OTARI_HOST` | Server bind host |
| `OTARI_PORT` | Server bind port |
Expand Down Expand Up @@ -185,6 +186,32 @@ These credentials are used for standalone deployments. When connected to otari.a
| `MISTRAL_API_KEY` | Mistral |
| `GEMINI_API_KEY` | Google Gemini |

### Runtime provider management

Instead of setting providers at launch, you can add them from the admin dashboard
after startup (standalone mode only). This lets you launch Otari with almost
nothing set and finish configuration in the browser.

- **First-run master key.** If `OTARI_MASTER_KEY` is unset, Otari generates a
master key on first startup, stores only its hash, and prints the plaintext
once to the logs (look for `Your master key:`). Use it to sign in. An
operator-set `OTARI_MASTER_KEY` always takes precedence and is never generated
over.
- **Encryption at rest.** Provider keys added in the dashboard are stored
encrypted with `OTARI_SECRET_KEY` (a Fernet key). Set it before adding a key;
generate one with `otari gen-secret-key`. Keep it safe and separate from the
database: losing it makes every stored provider key undecryptable, and a
database dump alone cannot decrypt them. Rotate by prepending a new key
(comma- or whitespace-separated); both old and new are tried on decrypt.
- **Precedence.** Dashboard-stored providers merge over `config.yml` providers.
A stored provider with the same instance name as a config one takes precedence
(the shadowing is logged at startup). In the dashboard, config providers are
marked `config` and are read-only; stored providers are marked `stored` and can
be edited, tested, and deleted.
- **Scope.** Providers that authenticate with an API key (OpenAI, Anthropic,
Mistral, Gemini, and OpenAI-compatible backends) are supported. Providers that
use ADC/IAM (Vertex AI, Bedrock) remain config-file only.

### otari.ai variables

These are only relevant when running connected to [otari.ai](https://otari.ai). See [Modes](modes.md) for details.
Expand Down
Loading
Loading