AGENTS.md 14 KB

AGENTS.md — testing-proxy

A SvelteKit 5 + Tailwind frontend with i18n (Paraglide) and dark mode enabled by default.

Project Overview

  • Framework: SvelteKit 2.68.0 with Svelte 5.56.4 (runes mode, mandatory)
  • Build Tool: Vite 8.1.2
  • Styling: Tailwind CSS 4.3.0 (via @tailwindcss/vite, no separate config)
  • Components: Flowbite Svelte 1.33.1 + Flowbite Icons
  • i18n: Paraglide JS (English + Spanish, auto-localized routes)
  • Adapter: @sveltejs/adapter-node (Node.js deployments)
  • Package Manager: pnpm (>=11.9 required)
  • Node: >=24.14 required
  • Type System: JavaScript (JSConfig, no TypeScript enforcement)

Critical Commands

Development & Build

# Start dev server (port 3000, with hot reload)
npm run dev
# or with browser auto-open:
npm run dev -- --open

# Production build (generates .svelte-kit, /build, and optimized JS/CSS)
npm run build

# Preview production build locally (runs on port 3000 by default)
npm run preview

# Sync SvelteKit config (auto-runs on `npm install`)
npm run prepare

# Start production server (requires `npm run build` first)
npm start

No test/lint commands

This is a fresh project with no test suite or linter configured. Do not assume these exist.

Setup Prerequisites

Before first dev/build run, MUST copy configuration:

cp config.json.example config.json

Database is auto-initialized on first run (creates /data/app.db, seeds admin user admin@delete.me / deleteme).

Architecture & Key Files

SvelteKit Project Structure

src/
├── app.html           # HTML shell (dark mode preset)
├── hooks.js           # Paraglide i18n route localization
├── hooks.server.js    # Graceful shutdown, db cleanup, signal handlers
├── routes/
│   ├── +layout.svelte # Root layout component
│   ├── +page.svelte   # Home page (Flowbite Alert, language switcher, dark mode toggle)
│   ├── +error.svelte  # Error page
│   └── layout.css     # Route-level styles
└── lib/
    ├── index.js       # $lib alias exports
    ├── config.js      # Loads config.json (db & password settings)
    ├── crypto.js      # Argon2id hashing & verification (hashPassword, verifyPassword)
    ├── sqlite3/
    │   └── database.js # Database initialization, schema, admin user seeding
    ├── paraglide/     # i18n (auto-generated, DO NOT EDIT)
    │   ├── messages.js          # Locale-aware message functions
    │   ├── runtime.js           # getLocale(), setLocale(), deLocalizeUrl()
    │   └── messages/*.js        # en-us, es-mx translations
    └── assets/        # Static assets

Backend Infrastructure

  • Configuration: config.json (loaded by src/lib/config.js, must exist before runtime)
  • Database: SQLite3 (auto-initialized in /data/app.db on first run)
  • Schema: users table with id, email, password (hashed), created_at, updated_at
  • Auth: Argon2id password hashing via src/lib/crypto.js
  • Admin User: Auto-seeded if users table is empty (admin@delete.me / deleteme)

Build Artifacts (gitignored)

  • .svelte-kit/ — SvelteKit type hints & config cache
  • /build/ — Production output (Node.js adapter)
  • vite.config.js.timestamp-* — Vite cache

Port Configuration

  • Dev Server: port 3000 (configured in vite.config.js)
  • Preview Server: port 3000 (default for npm run preview)

Svelte & SvelteKit Quirks

Runes Mode (Mandatory for this Project)

All code must use Svelte 5 runes by default. The Vite config enforces this for all files except those in node_modules:

runes: ({ filename }) => 
  filename.split(/[/\\]/).includes('node_modules') ? undefined : true

Effect on Component Writing:

  • Use $state() instead of let for reactive variables
  • Use $derived for computed values
  • Use $effect() for side effects (replaces onMount, afterUpdate)
  • Snippets use {#snippet name()} syntax (shown in +page.svelte)

Action: Any new .svelte files must use runes. Violating this breaks dev/build.

Tailwind CSS Integration

  • Configured via @tailwindcss/vite plugin (not postcss)
  • Plugins loaded: @tailwindcss/forms, @tailwindcss/typography
  • Dark mode: hardcoded in app.html (class="dark" on <html>)
  • No separate tailwind.config.js (handled entirely via Vite plugin)

SvelteKit Adapter

  • Using @sveltejs/adapter-node for Node.js deployments
  • Production build outputs to /build/
  • Environment: supports $env module for runtime env vars

Git & Workflow

  • Branch: master
  • Recent commits: 15 commits total (backend infrastructure: SQLite3, Argon2id, config management, graceful shutdown, Docker support)
  • Current state: 3 files modified/untracked (Dockerfile, .dockerignore, README.md updates)

Convention: Clear, atomic commits. Prefix context: "feat:", "fix:", "docs:", "refactor:" when possible.

Known Issues & Observations

  1. No CI/CD: No .github/workflows/ or other CI pipeline configured

  2. No Tests: No test framework or test scripts in package.json

  3. Minimal jsconfig: Inherits TypeScript config from .svelte-kit/tsconfig.json but doesn't enforce type checking (checkJs: false)

  4. Config must exist: Application crashes at startup if config.json is missing. Always ensure it's copied from config.json.example before running dev/build.

  5. pnpm-workspace.yaml native builds: Specifies which native packages can be built (esbuild, Tailwind Oxide, better-sqlite3 enabled; argon2 disabled). If native deps fail to build, check this file first.

Common Agent Tasks & Gotchas

Adding Components

  • Use Flowbite Svelte components from flowbite-svelte
  • Wrap icons in {#snippet} blocks (Svelte 5 pattern)
  • All components must use runes for reactivity

Modifying Styles

  • Tailwind classes work directly in templates
  • No CSS Modules configured (use Svelte <style> blocks for scoped styles)
  • Dark mode available via dark: prefix (already enabled on root)

Creating New Routes

  • Add +page.svelte (and optionally +page.js, +layout.svelte) in src/routes/[path]/
  • SvelteKit filesystem routing is automatic
  • Server-side code goes in +page.server.js (not configured; would need adapter setup)

Environment Variables

  • Store in .env file (gitignored)
  • Reference via import { env } from '$env/dynamic/public' or $env/static/public
  • Prefix public vars with VITE_ for client access

Database & Configuration

Configuration Loading (src/lib/config.js)

The application loads config.json at startup. This must exist before running dev/build:

cp config.json.example config.json

Failure mode: If missing, the app crashes with Failed to load config.json error during npm run dev or npm run build.

Database Initialization (src/lib/sqlite3/database.js)

  • Lazy initialization: Database is created on first getSqlite3() call
  • Schema: users table auto-created with id, email, password, created_at, updated_at
  • Admin seeding: If no users exist, admin@delete.me / deleteme is inserted (hashed with Argon2id)
  • Path: data/app.db (relative to project root, constructed from config.json filename + PROJECT_ROOT/data/, directory auto-created if missing)
  • WAL Mode: Enabled by default in config for better concurrency

Idempotent: Safe to call multiple times; schema and admin user only created once.

Password Hashing (src/lib/crypto.js)

Two exported functions:

  • hashPassword(password) — async, returns Argon2id hash (configured in config.json)
  • verifyPassword(password, hash) — async, returns boolean

Use these for any user auth operations. Config pulls settings from config.password.options.

Graceful Shutdown (src/hooks.server.js)

Database connection is properly closed on process termination (SIGTERM, SIGINT) or runtime errors. Exit codes:

  • 0 — Graceful shutdown (SIGTERM/SIGINT)
  • 1 — Uncaught exception or unhandled rejection
  • 2 — Cleanup error during shutdown

/v1 API — OpenAI-Compatible LLM Proxy

The application exposes an OpenAI-compatible REST API under /v1 that proxies chat completion requests to configured LLM providers. All /v1 endpoints require OAuth 2.1 Bearer token authentication with the llm scope.

Authentication

Every /v1 request must include an Authorization header:

Authorization: Bearer <access_token>

Tokens are issued by the local OAuth 2.1 authorization server. The flow is:

  1. Register a client via POST /oauth/register
  2. Log in via POST /oauth/login (sets session cookie)
  3. Authorize via GET /oauth/authorize with PKCE S256
  4. Exchange the code via POST /oauth/token

See scripts/get-token.sh for an automated script that runs the full flow.

Provider Configuration

Providers are defined in config.json under the providers array. Only providers with enabled: true are exposed.

Supported provider types:

Type Required Config Credentials
bedrock region, credentials AWS access key ID + secret
anthropic apiKey Anthropic API key
openai-compatible baseUrl None (local endpoint)

Example config.json snippet:

{
  "providers": [
    {
      "id": "bedrock",
      "type": "bedrock",
      "enabled": true,
      "region": "us-east-1",
      "credentials": { "accessKeyId": "...", "secretAccessKey": "..." },
      "models": { "claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0" }
    },
    {
      "id": "anthropic",
      "type": "anthropic",
      "enabled": true,
      "apiKey": "...",
      "models": { "claude-3-5-sonnet": "claude-3-5-sonnet-20241022" }
    },
    {
      "id": "local",
      "type": "openai-compatible",
      "enabled": false,
      "baseUrl": "http://localhost:11434/v1",
      "models": { "llama3.1": "llama3.1:latest" }
    }
  ]
}

Model ID Format

Models are addressed as provider/localModelId (e.g., bedrock/claude-3-sonnet). The models object in config maps the local key to the provider's native identifier.

Endpoints

POST /v1/chat/completions

Chat with streaming or non-streaming response.

# Non-streaming
curl -X POST http://localhost:3000/v1/chat/completions \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bedrock/claude-3-sonnet",
    "messages": [{"role": "user", "content": "Hello!"}],
    "temperature": 0.7,
    "max_tokens": 256
  }'

# Streaming
curl -X POST http://localhost:3000/v1/chat/completions \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-3-5-sonnet",
    "messages": [{"role": "user", "content": "Hello!"}],
    "stream": true
  }'

GET /v1/models

List all available models across enabled providers.

curl http://localhost:3000/v1/models \
  -H "Authorization: Bearer <token>"

GET /v1/models/{model}

Get details for a single model. The slash in the model ID must be URL-encoded (%2F).

curl http://localhost:3000/v1/models/bedrock%2Fclaude-3-sonnet \
  -H "Authorization: Bearer <token>"

Error Format

All errors are returned in OpenAI-compatible JSON:

{
  "error": {
    "message": "Human-readable description",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_model"
  }
}

Common error types: invalid_request_error, invalid_token, insufficient_scope, authentication_error, rate_limit_error, api_error.

Common HTTP status codes:

  • 200 — Success
  • 400 — Invalid request (bad JSON, missing params, unknown model)
  • 401 — Missing or invalid Bearer token
  • 403 — Valid token but insufficient scope
  • 404 — Model not found
  • 429 — Rate limited by provider
  • 500 — Internal server error (provider init failure)
  • 502 — Provider service error
  • 503 — Provider overloaded

Key Files

  • src/routes/v1/chat/completions/+server.js — Chat completion handler
  • src/routes/v1/models/+server.js — Model list handler
  • src/routes/v1/models/[model]/+server.js — Model detail handler
  • src/lib/providers/index.js — Provider resolution and model listing
  • src/lib/providers/errors.js — Error normalization to OpenAI format
  • src/lib/providers/bedrock.js — AWS Bedrock provider
  • src/lib/providers/anthropic.js — Anthropic provider
  • src/lib/providers/openai-compatible.js — Generic OpenAI-compatible provider
  • src/lib/oauth/bearer.js — Bearer token validation

Performance Notes

  • Vite dev server is fast; rebuilds are near-instant
  • Production build with Adapter-Node is optimized for server deployments
  • No bundle analyzer configured; use Vite's built-in --profile if needed

External Resources

Docker & Deployment Quirks

Database Path in Docker

When using docker run or docker-compose, the config is mounted at /opt/app/server/config.json. The database path data/app.db is then resolved relative to the working directory inside the container, which is typically /opt/app/. Ensure volume mounts for ./data are in place to persist the database across restarts.

Native Dependencies in Docker

The Dockerfile uses Node.js 24 Alpine. Native packages (better-sqlite3, argon2) must build in the Alpine environment. If builds fail, check:

  • Alpine headers are installed (usually handled in Dockerfile)
  • pnpm-workspace.yaml allowlist is current

Last Updated: Verified against current project state (15 commits, Docker support added, SvelteKit 2.68.0, Vite 8.1.2, pnpm-workspace.yaml quirks documented)