# testing-proxy A modern SvelteKit application with Svelte 5 runes, Tailwind CSS, and Flowbite components. Also serves as a multi-provider OpenAI-compatible LLM proxy with OAuth 2.1 authentication. ## Setup Install dependencies: ```sh pnpm install ``` ### Configuration Copy the example configuration to create your local `config.json`: ```sh cp config.json.example config.json ``` This file contains settings for database and server configuration. See [Configuration](#configuration-reference) below for available options. ## Development Start the dev server on http://localhost:3000: ```sh npm run dev # Auto-open in browser npm run dev -- --open ``` Hot module reload enabled by default. ## Build & Deploy Create a production build: ```sh npm run build ``` Preview the production build locally: ```sh npm run preview ``` Production output goes to `/build/` (configured for Node.js via `@sveltejs/adapter-node`). ## Docker The application includes a multi-stage Dockerfile for production deployments using Node.js 24 Alpine. ### Docker Run Build the Docker image: ```sh docker build -t testing-proxy . ``` Run the container with a mounted configuration file: ```sh docker run -d \ --name testing-proxy \ -p 3000:3000 \ --volume ./config.json:/opt/app/server/config.json:ro \ --volume ./data:/opt/app/data \ testing-proxy ``` The flags: - `--volume ./config.json:/opt/app/server/config.json:ro` — Mounts your local `config.json` as read-only inside the container - `--volume ./data:/opt/app/data` — Persists the SQLite database across container restarts Ensure `config.json` exists before running: ```sh cp config.json.example config.json mkdir -p data ``` ### Docker Compose Use Docker Compose for orchestrated deployments with secrets management: ```yaml version: '3.8' services: app: image: testing-proxy:latest container_name: testing-proxy ports: - "3000:3000" secrets: - source: config_secret target: /opt/app/server/config.json uid: "1000" gid: "1000" mode: 0400 volumes: - ./data:/opt/app/data # Persist SQLite database restart: unless-stopped secrets: config_secret: file: ./config.json ``` Save as `docker-compose.yml` and run: ```sh docker-compose up -d ``` The `secrets` section: - **`source`** — Named secret reference (`config_secret`) - **`target`** — Container path where the config is mounted (`/opt/app/server/config.json`) - **`uid` / `gid`** — User/group IDs inside the container (1000 for app user) - **`mode`** — File permissions (`0400` = read-only for owner) ### Configuration Before running Docker containers, create your configuration file: ```sh cp config.json.example config.json ``` Both `docker run` and `docker-compose` methods expect `config.json` to exist on your host machine and mount it into the container. ### Data Persistence For Docker Compose deployments, the `./data` volume persists the SQLite database across container restarts. Ensure the `data/` directory is writable: ```sh mkdir -p data chmod 755 data ``` ## LLM Proxy The application exposes an OpenAI-compatible REST API under `/v1` that proxies chat completion requests to configured LLM providers (AWS Bedrock, Anthropic, or any OpenAI-compatible endpoint). All `/v1` endpoints require OAuth 2.1 Bearer token authentication. ### Provider Setup Providers are configured 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:** ```json { "providers": [ { "id": "bedrock", "type": "bedrock", "enabled": true, "region": "us-east-1", "credentials": { "accessKeyId": "YOUR_ACCESS_KEY", "secretAccessKey": "YOUR_SECRET_KEY" }, "models": { "claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0" } }, { "id": "anthropic", "type": "anthropic", "enabled": true, "apiKey": "YOUR_ANTHROPIC_API_KEY", "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" } } ] } ``` Models are addressed as `provider/localModelId` (e.g., `bedrock/claude-3-sonnet`). The `models` object maps the local key to the provider's native identifier. ### Authentication Every `/v1` request must include an `Authorization` header with a valid Bearer access token: ``` Authorization: Bearer ``` 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 a session cookie) 3. **Authorize** via `GET /oauth/authorize` with PKCE S256 4. **Exchange the code** via `POST /oauth/token` ### Quick Start Use the provided script to automate token acquisition: ```bash # Make the script executable and run it chmod +x scripts/get-token.sh ./scripts/get-token.sh ``` This script runs the full OAuth 2.1 flow and prints an access token. Then make a request: ```bash # List available models curl http://localhost:3000/v1/models \ -H "Authorization: Bearer " # Chat completion (non-streaming) curl -X POST http://localhost:3000/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "bedrock/claude-3-sonnet", "messages": [{"role": "user", "content": "Hello!"}], "temperature": 0.7, "max_tokens": 256 }' # Chat completion (streaming) curl -X POST http://localhost:3000/v1/chat/completions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-3-5-sonnet", "messages": [{"role": "user", "content": "Hello!"}], "stream": true }' ``` ### API Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/v1/chat/completions` | `POST` | Chat with streaming or non-streaming response | | `/v1/models` | `GET` | List all available models across enabled providers | | `/v1/models/{model}` | `GET` | Get details for a single model (URL-encode the slash) | All errors are returned in OpenAI-compatible JSON format. See `docs/V1-API.md` for full API documentation. ## Project Structure - `src/routes/` — SvelteKit filesystem routes - `src/lib/` — Reusable components and utilities (access via `$lib` alias) - `src/app.html` — HTML shell (dark mode enabled via `class="dark"`) ## Tech Stack - **Framework**: SvelteKit 2.63.0 with Svelte 5 (runes mode) - **Styling**: Tailwind CSS 4.3.0 via `@tailwindcss/vite` + Forms/Typography plugins - **Components**: Flowbite Svelte 1.33.1 + Flowbite Icons - **Build Tool**: Vite 8.0.16 - **Adapter**: Node.js (`@sveltejs/adapter-node`) - **LLM Providers**: `@aws-sdk/client-bedrock-runtime`, Anthropic SDK, generic OpenAI-compatible endpoints - **Authentication**: OAuth 2.1 with PKCE S256, Redis-backed token storage ## Important Notes ### Svelte 5 Runes (Mandatory) All `.svelte` files use runes mode by default. Use: - `$state()` for reactive variables - `$derived` for computed values - `$effect()` for side effects - `{#snippet}` for component snippets (e.g., icon slots) See [Svelte 5 runes documentation](https://svelte.dev/docs/svelte/what-are-runes). ### Styling - Dark mode hardcoded in `app.html` (remove `class="dark"` from `` to disable) - Use Tailwind utilities directly; no CSS Modules - Scoped styles available via `