Skip to content

Getting started

Two ways in: the Docker compose stack (the whole platform, PostgreSQL included) or a bare uv environment (the Python services only, tests included). The compose route is the one that gets you a billed completion and a console.

Run the stack

deploy/compose.yaml from this checkout, with a fake upstream so no provider account is needed (the full set of options is in Deployment):

cp deploy/.env.example deploy/.env && chmod 600 deploy/.env
# fill in deploy/.env: TLS_MODE=internal, PUBLIC_ORIGIN=https://dev.example.test:8443,
# SITE_ADDRESS=https://dev.example.test, TLS_DIRECTIVE='tls internal', HTTPS_PORT=8443,
# then mint every secret and, for the bundled Authelia, its digests — the comment above
# each variable in deploy/.env.example names the exact command.
export PYSTINO_SRC="$PWD"    # the fake upstream (deploy/dev/smoke.yml) needs it
cd deploy && docker compose -f compose.yaml -f dev/smoke.yml up -d --wait

dev.example.test must resolve to this machine for a browser (a hosts entry is enough). The gateway itself also listens on 127.0.0.1:8000, loopback only.

Create something to talk to

docker compose exec gateway gateway seed --model my-model --upstream-model gpt-4o-mini

It prints an API key (once) and a ready-made curl. Any OpenAI client works:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="gwk_...")
client.chat.completions.create(
    model="my-model",
    messages=[{"role": "user", "content": "hello"}],
)

The request is redacted, admitted against the caller's quotas, metered and settled into the ledger. Streaming works the same way, with stream=True or stream_options={"include_usage": true} — the gateway forces usage out of the upstream either way.

Sign in to the console

The console is at https://dev.example.test:8443/console. Sign in through the bundled Authelia with AUTHELIA_ADMIN_USER and the password behind AUTHELIA_ADMIN_PASSWORD_DIGEST; because its email (AUTHELIA_ADMIN_EMAIL/PYSTINO_BOOTSTRAP_ADMIN_EMAIL) matches, that first sign-in makes it the administrator. The gateway itself has no password form: even this first sign-in goes through the bundled Authelia's page, and the provider is set in deploy/.env, not in the console; see Identity.

Use it from opencode

opencode is a coding agent that runs in your terminal. To point it at your gateway, mint an API key in the console (API keys on the Overview page, which also shows this command with your address filled in), then run this on the machine where you use opencode, with GATEWAY_BASE_URL set to your gateway's address (its PUBLIC_ORIGIN):

export GATEWAY_BASE_URL=https://pystino.example.org
curl -fsSL "$GATEWAY_BASE_URL/opencode/install.sh" | bash

It asks for the key, adds the gateway as a provider in opencode's global config, keeps the rest of that file, and backs it up first. Then start opencode and pick a model with /models. See Coding agents for the options, how to keep the key out of the file, and how to undo it.

Reach it from another machine

On a server over SSH, forward that one port rather than exposing anything:

ssh -L 8000:localhost:8000 user@your-server

Then open http://localhost:8000/console in your own browser. The local port must match the remote one. The session cookie is scoped to the origin the login happened on, so localhost:8000 on both ends is what keeps it.

Develop without Docker

uv sync
uv run pytest -q                  # about 1,900 tests, SQLite, no services, no network
uv run ruff check .
uv run mypy apps/gateway/src packages/shared-py/src services   # mypy is --strict

Tests run against SQLite and a fake upstream transport, so they need no PostgreSQL, no Valkey and no network. services/redaction is not a workspace member on purpose — spaCy must never enter the gateway's lockfile — but its tests still run from the repo root.

For an end-to-end check over a real socket — real server, real database, real streaming upstream, no Docker:

./scripts/smoke_test.sh

It starts everything on temporary ports, drives the endpoints, prints the resulting ledger and cleans up. It also demonstrates the quota overrun policy: the request that crosses the limit is admitted, the next one gets a 429.

Against a real database, without Docker:

export GATEWAY_DATABASE_URL=postgresql+asyncpg://gateway:gateway@localhost:5432/gateway
uv run alembic -c apps/gateway/alembic.ini upgrade head
uv run gateway seed
uv run gateway serve --reload

Where to next

  • Accounting and quotas — what the ledger now contains, and how a cost is computed.
  • Operations — the live checks, and what they can only find against a running stack.
  • Deployment — TLS, identity, upgrades and backups.