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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
# Gateway authentication key — clients must pass this as x-api-key or Bearer token
GATEWAY_API_KEY=your-gateway-api-key

# PostgreSQL connection string
# Database connection string. Two drivers are supported and the target is fixed
# at deploy time (it cannot be switched later without re-migrating data):
# PostgreSQL: postgresql://user:password@localhost:5432/ai_proxy
# SQLite: sqlite:./data/llm-relay.db (embedded, no external DB needed —
# use an absolute path like sqlite:///data/llm-relay.db in Docker
# and mount a volume so the file persists)
DATABASE_URL=postgresql://user:password@localhost:5432/ai_proxy

# ── Optional ──────────────────────────────────────────────────────────────────
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ dist
*.local.*
data/

# Local SQLite database files
*.db
*.db-shm
*.db-wal
*.sqlite
*.sqlite3

# Personal test scripts (not for public distribution)
scripts/

Expand Down
27 changes: 25 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ LRS is a lightweight LLM relay service built on **Bun + Hono**. It unifies multi
### Prerequisites

- [Bun](https://bun.sh) >= 1.1
- A PostgreSQL database
- A database: PostgreSQL **or** SQLite (pick one)
- **SQLite**: embedded in the process, no separate database to deploy — cheapest option; just set `DATABASE_URL=sqlite:./data/llm-relay.db`
- **PostgreSQL**: better for higher concurrency / multiple instances
- ⚠️ The target database is fixed at deploy time and cannot be switched at runtime (to avoid data ending up split across two stores)

### Install and run

Expand Down Expand Up @@ -169,6 +172,14 @@ docker compose pull && docker compose up -d

> **Tip**: If you already have an external PostgreSQL, just remove the `postgres` service from `docker-compose.yml` and point `DATABASE_URL` at your connection string.

#### SQLite (no PostgreSQL, cheapest)

If you'd rather not run PostgreSQL, use the bundled SQLite compose file — the database is a single file persisted on a named volume:

```bash
GATEWAY_API_KEY=your-key docker compose -f docker-compose.sqlite.yml up -d
```

### Single-container Docker

If you already have your own PostgreSQL, run a single container (it listens on `3300` by default):
Expand All @@ -182,6 +193,18 @@ docker run -d \
ghcr.io/gojam11/llmrelayservice:main
```

With SQLite (mount a volume so the database file persists):

```bash
docker run -d \
--name lrs \
-p 3300:3300 \
-e GATEWAY_API_KEY=your-key \
-e DATABASE_URL=sqlite:///data/llm-relay.db \
-v lrs_sqlite:/data \
ghcr.io/gojam11/llmrelayservice:main
```

Open `http://localhost:3300` to access the console.

### Build from source
Expand Down Expand Up @@ -218,7 +241,7 @@ Open the root path `/` to access the console. Features include:

| Variable | Required | Description |
|----------|----------|-------------|
| `DATABASE_URL` | ✅ | PostgreSQL connection string |
| `DATABASE_URL` | ✅ | Database connection string. PostgreSQL: `postgresql://...`; SQLite: `sqlite:./data/llm-relay.db` (embedded, no separate database). Fixed at deploy time, not switchable at runtime |
| `GATEWAY_API_KEY` | ✅ | The key clients use to access the gateway; also the console login password |
| `PORT` | — | Listening port, default `3300` |
| `UPSTREAM_DEFAULT_FIRST_BYTE_TIMEOUT_MS` | — | Default timeout for normal requests waiting on upstream response headers, default `300000` ms; can be overridden persistently in the console Settings page |
Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ LRS 是一个基于 **Bun + Hono** 的轻量 LLM 中继服务,把多个 AI 服
### 前置条件

- [Bun](https://bun.sh) >= 1.1
- PostgreSQL 数据库
- 数据库:PostgreSQL **或** SQLite(二选一)
- **SQLite**:内嵌于进程,无需额外部署数据库,最省成本;设 `DATABASE_URL=sqlite:./data/llm-relay.db` 即可
- **PostgreSQL**:适合更高并发/多实例场景
- ⚠️ 目标数据库在部署时确定,运行时不支持切换(避免数据分散在两套库中)

### 安装与启动

Expand Down Expand Up @@ -169,6 +172,14 @@ docker compose pull && docker compose up -d

> **提示**:如已有外部 PostgreSQL,只需删除 `docker-compose.yml` 中的 `postgres` 服务,并将 `DATABASE_URL` 改为对应连接字符串。

#### SQLite(无需 PostgreSQL,最省成本)

不想部署 PostgreSQL 时,可使用内置的 SQLite 编排文件,数据库为单个文件并持久化到命名卷:

```bash
GATEWAY_API_KEY=your-key docker compose -f docker-compose.sqlite.yml up -d
```

### 单容器 Docker

如果你已经有自己的 PostgreSQL,可以直接运行单个容器(容器默认监听 `3300`):
Expand All @@ -182,6 +193,18 @@ docker run -d \
ghcr.io/gojam11/llmrelayservice:main
```

使用 SQLite(挂载卷以持久化数据库文件):

```bash
docker run -d \
--name lrs \
-p 3300:3300 \
-e GATEWAY_API_KEY=your-key \
-e DATABASE_URL=sqlite:///data/llm-relay.db \
-v lrs_sqlite:/data \
ghcr.io/gojam11/llmrelayservice:main
```

访问 `http://localhost:3300` 打开控制台。

### 从源码构建
Expand Down Expand Up @@ -218,7 +241,7 @@ Railway / Render 等平台部署时构建命令同上。

| 变量 | 必填 | 说明 |
|------|------|------|
| `DATABASE_URL` | ✅ | PostgreSQL 连接字符串 |
| `DATABASE_URL` | ✅ | 数据库连接字符串。PostgreSQL:`postgresql://...`;SQLite:`sqlite:./data/llm-relay.db`(内嵌,无需额外数据库)。部署时确定,运行时不可切换 |
| `GATEWAY_API_KEY` | ✅ | 客户端访问网关所需的 key,同时用作控制台登录密码 |
| `PORT` | — | 监听端口,默认 `3300` |
| `UPSTREAM_DEFAULT_FIRST_BYTE_TIMEOUT_MS` | — | 普通请求等待上游响应头的默认超时时间,默认 `300000` 毫秒;可在控制台配置页持久化覆盖 |
Expand Down
13 changes: 13 additions & 0 deletions changelog/2026-07-08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 2026-07-08

### 新增

- **SQLite 部署方式**:允许在不部署 PostgreSQL 的情况下运行本项目,降低部署成本。目标数据库由 `DATABASE_URL` 协议自动选择(`postgresql://` → PostgreSQL;`sqlite:` / `file:` → SQLite),部署时确定、运行时不支持切换以避免数据分散。实现方式:
- 新增 `src/db/config.ts` 中的 `getDbDriver()` / `isSqliteUrl()` / `getSqliteFilePath()`,从连接串推断驱动。
- 拆分 schema 为 `schema.pg.ts`(`pg-core`)与 `schema.sqlite.ts`(`sqlite-core`),列名/默认值/索引保持一致;`schema.ts` 按当前驱动运行时导出对应表对象,store 层无需改动。
- `client.ts` 在 SQLite 下使用内置 `bun:sqlite` + `drizzle-orm/bun-sqlite`(WAL、busy_timeout、自动创建父目录),进程内共享单连接。
- `migrate.ts` 按驱动选择迁移目录(PostgreSQL `drizzle/`,SQLite `drizzle/sqlite/`)并用对应 migrator 执行;SQLite 无需 advisory lock 与就绪探针。
- 新增 SQLite 基线迁移 `drizzle/sqlite/0000_*.sql`(drizzle-kit 生成);`drizzle.config.ts` 按驱动切换 dialect / schema / out。
- `console-store.ts` 的 API Key 配额扣费在 SQLite 下改用等价的分步原子写(替代 PostgreSQL 专有的 `UPDATE...FROM ... RETURNING` + `FOR UPDATE` CTE);控制台清库保护对 SQLite 直接放行(本地内嵌文件无远程误删风险)。
- `server.ts` 的数据库重置在 SQLite 下改用 `sqlite_master` 枚举并 DROP 表后重迁移。
- 新增 `docker-compose.sqlite.yml`(单容器 + 命名卷持久化),并在 `.env.example`、`README.md`、`README.en.md`、`src/db/README.md` 补充 SQLite 部署说明。
21 changes: 21 additions & 0 deletions docker-compose.sqlite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SQLite deployment — no external database container required. The database is a
# single embedded file persisted on the `sqlite_data` volume. Start with:
# GATEWAY_API_KEY=your-key docker compose -f docker-compose.sqlite.yml up -d
services:
app:
image: ghcr.io/gojam11/llmrelayservice:main
ports:
- "3001:3000"
environment:
PORT: 3000
# Absolute path inside the container; the parent dir is created on startup.
DATABASE_URL: sqlite:///data/llm-relay.db
GATEWAY_API_KEY: ${GATEWAY_API_KEY:?GATEWAY_API_KEY is required}
PASSWORD: ${PASSWORD:-}
DEBUG_DB_MAX_RECORDS: ${DEBUG_DB_MAX_RECORDS:-50000}
volumes:
- sqlite_data:/data
restart: unless-stopped

volumes:
sqlite_data:
31 changes: 22 additions & 9 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { defineConfig } from 'drizzle-kit';
import { getDatabaseUrl } from './src/db/config';
import { getDatabaseUrl, getDbDriver, getSqliteFilePath } from './src/db/config';

export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: getDatabaseUrl(),
},
});
// The dialect is selected from DATABASE_URL. To generate/apply SQLite migrations:
// DATABASE_URL=sqlite:./data/llm-relay.db bun run db:generate
// and for PostgreSQL (the default):
// DATABASE_URL=postgresql://... bun run db:generate
export default getDbDriver() === 'sqlite'
? defineConfig({
schema: './src/db/schema.sqlite.ts',
out: './drizzle/sqlite',
dialect: 'sqlite',
dbCredentials: {
url: getSqliteFilePath(),
},
})
: defineConfig({
schema: './src/db/schema.pg.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: getDatabaseUrl(),
},
});
137 changes: 137 additions & 0 deletions drizzle/sqlite/0000_many_the_hand.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
CREATE TABLE `console_api_keys` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`key_hash` text NOT NULL,
`key_value` text NOT NULL,
`prefix` text NOT NULL,
`created_at` integer NOT NULL,
`last_used_at` integer,
`revoked` integer DEFAULT 0 NOT NULL,
`allowed_models_json` text DEFAULT '[]' NOT NULL,
`cost_quota_microusd` integer,
`cost_used_microusd` integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE INDEX `idx_console_api_keys_key_hash` ON `console_api_keys` (`key_hash`);--> statement-breakpoint
CREATE INDEX `idx_console_api_keys_created_at` ON `console_api_keys` (`created_at`);--> statement-breakpoint
CREATE TABLE `console_providers` (
`channel_name` text PRIMARY KEY NOT NULL,
`provider_uuid` text DEFAULT '' NOT NULL,
`type` text NOT NULL,
`target_base_url` text NOT NULL,
`system_prompt` text,
`models_json` text DEFAULT '[]' NOT NULL,
`priority` integer DEFAULT 0 NOT NULL,
`auth_header` text,
`auth_value` text,
`extra_fields_json` text DEFAULT '' NOT NULL,
`routing_visibility` text DEFAULT 'direct' NOT NULL,
`enabled` integer DEFAULT 1 NOT NULL,
`auto_sync_models` integer DEFAULT 0 NOT NULL,
`models_synced_at` integer,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE INDEX `idx_console_providers_created_at` ON `console_providers` (`created_at`);--> statement-breakpoint
CREATE INDEX `idx_console_providers_updated_at` ON `console_providers` (`updated_at`);--> statement-breakpoint
CREATE TABLE `console_requests` (
`request_id` text PRIMARY KEY NOT NULL,
`created_at` integer NOT NULL,
`route_prefix` text NOT NULL,
`upstream_type` text DEFAULT 'anthropic' NOT NULL,
`method` text NOT NULL,
`path` text NOT NULL,
`target_url` text NOT NULL,
`request_model` text NOT NULL,
`api_key_id` text,
`api_key_name` text,
`original_payload` text,
`original_payload_truncated` integer DEFAULT 0 NOT NULL,
`original_summary_json` text,
`forwarded_payload` text,
`forwarded_payload_truncated` integer DEFAULT 0 NOT NULL,
`forwarded_summary_json` text,
`original_headers_json` text,
`forward_headers_json` text,
`response_headers_json` text,
`response_status` integer,
`response_status_text` text,
`response_payload` text,
`response_payload_truncated` integer DEFAULT 0 NOT NULL,
`response_payload_truncation_reason` text,
`response_body_bytes` integer DEFAULT 0 NOT NULL,
`first_chunk_at` integer,
`first_token_at` integer,
`completed_at` integer,
`has_streaming_content` integer DEFAULT 0 NOT NULL,
`response_model` text,
`stop_reason` text,
`input_tokens` integer DEFAULT 0 NOT NULL,
`output_tokens` integer DEFAULT 0 NOT NULL,
`total_tokens` integer DEFAULT 0 NOT NULL,
`cache_creation_input_tokens` integer DEFAULT 0 NOT NULL,
`cache_read_input_tokens` integer DEFAULT 0 NOT NULL,
`cached_input_tokens` integer DEFAULT 0 NOT NULL,
`reasoning_output_tokens` integer DEFAULT 0 NOT NULL,
`ephemeral_5m_input_tokens` integer DEFAULT 0 NOT NULL,
`ephemeral_1h_input_tokens` integer DEFAULT 0 NOT NULL,
`quota_charged_microusd` integer DEFAULT 0 NOT NULL,
`cost_pricing_json` text,
`failover_from` text,
`failover_chain_json` text,
`original_route_prefix` text,
`original_request_model` text,
`failover_reason` text,
`retry_attempt` integer DEFAULT 0 NOT NULL,
`source_request_type` text DEFAULT 'unknown' NOT NULL,
`token_usage_estimated` integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE INDEX `idx_console_requests_created_at` ON `console_requests` (`created_at`);--> statement-breakpoint
CREATE INDEX `idx_console_requests_compare` ON `console_requests` (`route_prefix`,`method`,`path`,`request_model`,`created_at`);--> statement-breakpoint
CREATE INDEX `idx_console_requests_route_prefix` ON `console_requests` (`route_prefix`,`created_at`);--> statement-breakpoint
CREATE INDEX `idx_console_requests_response_model` ON `console_requests` (`response_model`,`created_at`);--> statement-breakpoint
CREATE INDEX `idx_console_requests_api_key_id` ON `console_requests` (`api_key_id`);--> statement-breakpoint
CREATE TABLE `gateway_settings` (
`key` text PRIMARY KEY NOT NULL,
`value_json` text DEFAULT '{}' NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `model_aliases` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`alias` text NOT NULL,
`provider` text NOT NULL,
`model` text NOT NULL,
`targets_json` text DEFAULT '' NOT NULL,
`description` text,
`visible` integer DEFAULT 1 NOT NULL,
`enabled` integer DEFAULT 1 NOT NULL,
`return_real_model` integer DEFAULT 0 NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `model_aliases_alias_unique` ON `model_aliases` (`alias`);--> statement-breakpoint
CREATE INDEX `idx_model_aliases_alias` ON `model_aliases` (`alias`);--> statement-breakpoint
CREATE INDEX `idx_model_aliases_created_at` ON `model_aliases` (`created_at`);--> statement-breakpoint
CREATE TABLE `model_catalog_cache` (
`model_id` text PRIMARY KEY NOT NULL,
`context_window` integer,
`pricing_json` text,
`fetched_at` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `model_metadata_overrides` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`channel_name` text NOT NULL,
`model_id` text NOT NULL,
`context_window` integer,
`pricing_json` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `idx_model_metadata_overrides_channel_model` ON `model_metadata_overrides` (`channel_name`,`model_id`);--> statement-breakpoint
CREATE INDEX `idx_model_metadata_overrides_updated_at` ON `model_metadata_overrides` (`updated_at`);
Loading
Loading