版本:v0.1(草案) · 语言:Rust · 最后更新:2026-06-29
构建一个高性能的大语言模型(LLM)API 网关。核心能力:
- 对外:同时暴露两套业界标准 API
- OpenAI 兼容 API(
/v1/chat/completions、/v1/embeddings等) - Anthropic 兼容 API(
/v1/messages等)
- OpenAI 兼容 API(
- 对内:可接入任意上游供应商(OpenAI、Anthropic、Azure OpenAI、Google Gemini、DeepSeek、通义千问、智谱、本地 vLLM/Ollama 等)。
- 协议互转:客户端用 OpenAI SDK 发的请求,可路由到 Claude 后端;反之亦然。网关负责双向协议翻译。
- 统一治理:鉴权、配额、限流、计费、可观测、缓存、故障转移。
- C 端自助门户:面向终端用户(开发者)的网站,支持手机号 / 微信 / 支付宝登录;登录后系统自动为其分配 API Key,可查看用量、账单、充值。
- 管理后台:面向运营/管理员,管理供应商、路由、用户、密钥、价格、用量报表、系统配置。
- 前端:客户端门户与管理后台均用 React + TypeScript 实现,分为两个独立前端应用。
- 不做模型训练/微调。
- 不做面向最终用户的聊天对话 UI(门户只做账号/密钥/用量/充值;聊天 Playground 可作为后续增强)。
- 不内置向量数据库(embeddings 仅做转发)。
- 协议中立的内部表示(UIR, Unified Internal Representation):所有入站请求先翻译成统一中间模型,再翻译到目标供应商;避免 N×M 适配爆炸,变成 N+M。
- 流式优先:SSE 流式是主路径,非流式是流式的特例(聚合)。
- 零拷贝/低延迟:网关本身不应成为瓶颈,转发开销目标 < 5ms(不含上游)。
- 配置即代码 + 热加载:供应商、路由、密钥可动态变更,不重启。
- 单机 + 零外部依赖:不依赖 Redis,数据库用 SQLite;热路径全部走内存,数据库只做冷启动加载与异步落盘。目标:请求路径上零(或极少)同步 DB 查询,以支撑高并发。
| 关注点 | 选型 | 理由 |
|---|---|---|
| 语言 | Rust (2021/2024 edition) | 高并发、低延迟、内存安全 |
| 异步运行时 | tokio |
事实标准 |
| HTTP 服务 | axum |
基于 tower/hyper,流式与中间件生态好 |
| HTTP 客户端 | reqwest(启用 stream) |
上游调用,支持流式 body |
| 序列化 | serde / serde_json |
|
| 流式 SSE | axum::response::sse + 自定义解析 |
入站出站均需处理 SSE |
| 数据库 | SQLite(WAL 模式)+ sqlx(编译期校验 SQL) |
单机、零外部依赖;密钥、用户、用量 |
| 内存缓存 | dashmap(并发 Map)+ arc-swap(配置热替换) |
API Key/用户态全量驻留内存,校验不查库 |
| 限流/并发 | governor(进程内令牌桶)+ AtomicU32/I64 计数 |
无 Redis,纯内存 |
| 配置 | figment(env + TOML/YAML 合并) |
多源配置 |
| 可观测 | tracing + tracing-subscriber + opentelemetry + metrics(Prometheus exporter) |
日志/追踪/指标 |
| 密钥哈希 | argon2 / sha2 |
虚拟密钥存储 |
| 错误处理 | thiserror(库)+ anyhow(应用边界) |
|
| 测试 | cargo test + wiremock(mock 上游) + criterion(基准) |
runapi/
├── Cargo.toml # workspace
├── crates/
│ ├── gateway/ # 二进制:HTTP 服务入口、路由装配
│ ├── core/ # UIR 类型、trait 定义(Provider, Router...)
│ ├── protocols/ # 入站/出站协议适配
│ │ ├── openai/ # OpenAI <-> UIR
│ │ └── anthropic/ # Anthropic <-> UIR
│ ├── providers/ # 上游连接器(openai, anthropic, gemini, azure...)
│ ├── routing/ # 路由引擎、负载均衡、故障转移
│ ├── governance/ # 鉴权、限流、配额、计费
│ ├── storage/ # sqlx(SQLite)仓储层 + 内存态/落盘任务
│ ├── observability/ # tracing/metrics 初始化、用量记录
│ └── admin/ # Admin API
├── migrations/ # sqlx 迁移
├── config/ # 默认配置文件
└── DESIGN.md
┌─────────────────────────────────────────────┐
Client SDK ───► │ Ingress Layer (axum) │
(OpenAI / │ - TLS / HTTP │
Anthropic) │ - 路由分发: /v1/chat/completions /v1/messages │
└───────────────┬─────────────────────────────┘
│
┌───────────────▼─────────────────────────────┐
│ Middleware (tower layers) │
│ 认证 → 限流 → 配额 → 请求日志 → 追踪 │
└───────────────┬─────────────────────────────┘
│
┌───────────────▼─────────────────────────────┐
│ Inbound Protocol Adapter │
│ OpenAI Req ─┐ │
│ Anthropic Req─┴─► UIR (统一中间表示) │
└───────────────┬─────────────────────────────┘
│
┌───────────────▼─────────────────────────────┐
│ Routing Engine │
│ model 名 → 目标供应商 + 上游模型 │
│ 负载均衡 / 故障转移 / 权重 / 灰度 │
└───────────────┬─────────────────────────────┘
│
┌───────────────▼─────────────────────────────┐
│ Outbound Provider Adapter │
│ UIR → OpenAI / Anthropic / Gemini / ... │
└───────────────┬─────────────────────────────┘
│
┌───────────────▼─────────────────────────────┐
│ Provider Connector (reqwest, 流式) │
└───────────────┬─────────────────────────────┘
│ 上游响应(可能是另一种协议)
┌───────────────▼─────────────────────────────┐
│ Response Adapter: 上游 → UIR → 客户端协议 │
│ (流式逐 chunk 翻译) │
└───────────────┬─────────────────────────────┘
│
┌───────────────▼─────────────────────────────┐
│ Post-processing: 用量统计 / 计费 / 缓存写入 │
└───────────────────────────────────────────────┘
- 客户端用 OpenAI SDK 调
POST /v1/chat/completions,model: "claude-opus-4-8"。 - 中间件:校验虚拟密钥 → 限流 → 配额检查。
- Inbound adapter:OpenAI ChatCompletion 请求 → UIR。
- Routing:查路由表,
claude-opus-4-8→ provideranthropic-prod,上游模型claude-opus-4-8。 - Outbound adapter:UIR → Anthropic Messages 请求(system 提取、role 映射、tools schema 转换)。
- Connector:带 Anthropic key 发起流式请求。
- 上游返回 Anthropic SSE 事件流。
- Response adapter:Anthropic
message_start/content_block_delta/...→ UIR delta → OpenAIchat.completion.chunkSSE。 - 边转发边累计 token,结束后写用量、扣配额、记审计、(可选)写缓存。
系统存在三种相互独立的身份与鉴权,切勿混淆:
| 平面 | 使用者 | 凭证 | 入口 | 说明 |
|---|---|---|---|---|
| 数据面 Data Plane | 调用方程序 / SDK | 虚拟 API Key(rk-...) |
/v1/* 网关端点 |
真正调模型用的,登录后自动发放 |
| 门户面 Portal Plane | C 端终端用户(开发者) | 会话 JWT(手机号/微信/支付宝登录换取) | /portal/* + React 客户端 |
管理自己的 Key、看用量、充值 |
| 管理面 Admin Plane | 运营 / 管理员 | 管理员账号 + RBAC(JWT) | /admin/* + React 管理端 |
管供应商、路由、用户、价格 |
区别要点:登录(门户面)≠ 调用(数据面)。用户用手机号登录拿到的是会话 JWT,只能操作门户;真正调用大模型用的是登录后系统发给他的虚拟 API Key。两套凭证生命周期、撤销、限流维度都不同。
┌────────────────────┐ ┌────────────────────┐
│ Client Portal │ │ Admin Console │
│ (React, C 端) │ │ (React, 管理端) │
└─────────┬──────────┘ └─────────┬──────────┘
│ /portal/* (JWT) │ /admin/* (JWT+RBAC)
▼ ▼
SDK ──/v1/*(API Key)──►┌──────────────────────────────────────┐
│ RunAPI Gateway (单进程, Rust) │
│ ┌────────────────────────────────┐ │
│ │ 内存态(热路径): │ │
│ │ DashMap<key→KeyEntry>(校验) │ │
│ │ DashMap<user→UserState> │ │
│ │ token余额(Atomic)/并发/限流 │ │
│ │ ArcSwap<配置>(路由/供应商) │ │
│ └────────────────────────────────┘ │
│ data plane + portal API + admin API │
└──────┬───────────────────┬─────────────┘
冷启动加载/异步落盘 │ │ 上游调用
┌──────▼─────┐ ┌────▼─────────┐
│ SQLite │ │ 上游供应商 │
│ (WAL,单文件)│ └──────────────┘
└────────────┘
▲
┌──────────────┴───────────────┐
│ 外部:短信(阿里/腾讯)、 │
│ 微信开放平台、支付宝开放平台 │
└──────────────────────────────┘
UIR 是协议无关的核心数据模型,位于 core crate。所有翻译都经过它。
pub struct ChatRequest {
pub model: String, // 客户端请求的逻辑模型名
pub messages: Vec<Message>,
pub system: Option<String>, // 抽出的 system prompt(Anthropic 独立字段)
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub stop: Vec<String>,
pub stream: bool,
pub tools: Vec<ToolDef>,
pub tool_choice: ToolChoice,
pub response_format: Option<ResponseFormat>, // json_schema / json_object
pub metadata: RequestMetadata, // user_id、request_id 等
pub extra: serde_json::Map<String, Value>, // 透传供应商专有参数
}
pub struct Message {
pub role: Role, // System | User | Assistant | Tool
pub content: Vec<ContentBlock>, // 多模态:文本/图片/工具调用/工具结果
}
pub enum ContentBlock {
Text(String),
Image { source: ImageSource }, // url / base64
ToolUse { id: String, name: String, input: Value },
ToolResult { tool_use_id: String, content: Vec<ContentBlock>, is_error: bool },
}
pub struct ToolDef {
pub name: String,
pub description: Option<String>,
pub input_schema: Value, // JSON Schema
}pub enum StreamEvent {
Start { id: String, model: String },
TextDelta { text: String },
ToolUseStart { id: String, name: String },
ToolUseDelta { partial_json: String },
ToolUseStop,
Usage(Usage),
Stop { reason: StopReason }, // EndTurn | MaxTokens | StopSequence | ToolUse
Error(ProviderError),
}
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
pub cache_read_tokens: u32,
pub cache_write_tokens: u32,
}设计要点:UIR 的内容块模型刻意贴近 Anthropic 的「content blocks」结构,因为它比 OpenAI 的扁平
content + tool_calls表达力更强,OpenAI→UIR 是无损升维,UIR→OpenAI 是降维(可控)。
每种对外协议实现两个方向;每个上游供应商实现两个方向。统一 trait:
/// 入站:客户端协议 <-> UIR
pub trait InboundProtocol {
fn parse_request(&self, raw: Bytes) -> Result<ChatRequest>;
fn render_response(&self, uir: &ChatResponse) -> Result<Bytes>; // 非流式
fn render_stream_event(&self, ev: &StreamEvent, st: &mut RenderState) -> Option<SseEvent>; // 流式
}
/// 出站:UIR <-> 上游供应商协议
pub trait OutboundProvider {
fn build_request(&self, uir: &ChatRequest, route: &Route) -> Result<reqwest::Request>;
fn parse_stream_chunk(&self, raw: &[u8], st: &mut ParseState) -> Vec<StreamEvent>;
fn parse_response(&self, raw: Bytes) -> Result<ChatResponse>; // 非流式
}| 难点 | OpenAI | Anthropic | 对策 |
|---|---|---|---|
| system prompt | messages 中 role=system | 顶层 system 字段 |
UIR 单独存 system,翻译时按需提取/下沉 |
| 工具调用 | tool_calls(数组,放在 assistant message) |
tool_use content block |
统一为 ContentBlock::ToolUse |
| 工具结果 | role=tool 的独立 message | tool_result content block(在 user message 内) |
统一为 ContentBlock::ToolResult,翻译时重组 message 边界 |
| 流式分帧 | chat.completion.chunk,delta |
content_block_delta 等多事件类型 |
用 RenderState 跟踪状态机映射 |
| 停止原因 | finish_reason: stop/length/tool_calls |
stop_reason: end_turn/max_tokens/tool_use |
UIR 的 StopReason 双向枚举映射 |
| 多模态图片 | image_url(url 或 data URI) |
source.{type,media_type,data} |
UIR 的 ImageSource 归一 |
| token 用量 | usage.{prompt,completion}_tokens |
usage.{input,output}_tokens + cache |
UIR Usage 超集 |
| 参数差异 | frequency_penalty 等 |
无对应 | 不支持的参数:丢弃 + 警告日志(可配置为报错) |
Anthropic 事件 UIR OpenAI chunk
message_start → Start → {choices:[{delta:{role:"assistant"}}]}
content_block_start(text) → (记录 index) → (不输出)
content_block_delta(text) → TextDelta → {choices:[{delta:{content:"..."}}]}
content_block_start(tool) → ToolUseStart → {choices:[{delta:{tool_calls:[{id,function:{name}}]}}]}
content_block_delta(json) → ToolUseDelta → {choices:[{delta:{tool_calls:[{function:{arguments}}]}}]}
message_delta(stop_reason) → Stop → {choices:[{finish_reason:"stop"}]}
message_stop → (end) → data: [DONE]
- 逻辑模型名 → 一个或多个上游目标(Route)。
- 多目标时:负载均衡(加权轮询 / 最少连接 / 一致性哈希)。
- 故障转移(failover):上游 5xx/超时/限流 时按优先级切换。
- 灰度/金丝雀:按百分比分流到不同上游或不同模型。
- 模型别名:把
gpt-4o映射成内部某个 deployment。
路由键是请求里的 model 名,不是用户。 用户身份只决定鉴权/白名单/扣费;去哪个上游由 model 名经 model_routes 决定。三者解耦:
客户端 model 名(逻辑名) ──model_routes映射──► [(provider, upstream_model, weight)...]
│ 按权重选 / 失败 failover
▼
provider(自带协议 kind)
│ 入站协议≠provider协议 → 互转
▼
用 upstream_model 调上游
示例:用户用 OpenAI Key 调,但 model 是 Claude
POST /v1/chat/completions
Authorization: Bearer rk_live_xxx
{ "model": "claude-opus-4-8", "messages": [...] }
- 鉴权:
rk_live_xxx→ 内存DashMap→user_id、interface_kind=openai。 - 白名单:
claude-opus-4-8是否在该用户allowed_models(空=全部)。 - 查表:内存配置
model_routes["claude-opus-4-8"]→[{anthropic-prod,80},{bedrock,20}]。 - 负载均衡:按权重选中
anthropic-prod。 - 协议互转:入站 OpenAI、provider
kind=anthropic→ OpenAI→UIR→Anthropic。 - 调上游:provider base_url + 真实 key,模型名用
upstream_model。 - failover:超时/5xx 换
bedrock重试。 - 回译:Anthropic 响应 → UIR → OpenAI 格式返回。
要点
- 逻辑名 ≠ 上游真名:逻辑名可抽象(如
fast-cheap),后端随时可换,客户端无感。 - 入站接口与上游供应商无关:OpenAI 接口进来可路由到 Anthropic 上游(靠互转),反之亦然。
- 可调模型清单:
GET /v1/models返回逻辑模型(按用户白名单过滤),用户从中选model。 - (可选)按用户路由:给路由规则加用户分组/标签条件,实现"同一 model 名、不同用户走不同通道"(如 VIP 专属通道)。默认不启用。
models:
- name: "claude-opus-4-8" # 客户端看到的模型名
targets:
- provider: "anthropic-prod"
upstream_model: "claude-opus-4-8"
weight: 80
- provider: "bedrock-anthropic" # 同模型不同通道做容灾
upstream_model: "anthropic.claude-opus-4-8-v1"
weight: 20
failover: [ "anthropic-prod", "bedrock-anthropic" ]
- name: "gpt-4o"
targets:
- provider: "azure-openai-eastus"
upstream_model: "gpt-4o-2024-deploy"
- name: "fast-cheap" # 抽象模型名,屏蔽底层
targets:
- provider: "deepseek"
upstream_model: "deepseek-chat"
providers:
anthropic-prod:
kind: anthropic
base_url: "https://api.anthropic.com"
api_key_ref: "secret://ANTHROPIC_KEY"
timeout_ms: 60000
max_retries: 2
azure-openai-eastus:
kind: azure_openai
base_url: "https://xxx.openai.azure.com"
api_version: "2024-10-01"
api_key_ref: "secret://AZURE_KEY"
deepseek:
kind: openai # OpenAI 兼容协议,直接复用 openai 连接器
base_url: "https://api.deepseek.com"
api_key_ref: "secret://DEEPSEEK_KEY"- 可重试错误:连接超时、429、500/502/503/504。
- 不可重试:400(请求本身错)、401/403(鉴权)、模型不存在。
- 流式中途失败:若已输出 token,默认不重试(避免重复内容),记录并向客户端发 error 事件;可配置「未发首 token 前可重试」。
- 退避:指数退避 + 抖动,受
max_retries限制。
- 虚拟密钥(Virtual Key):网关发给客户端的 key(如
rk-...),与真实上游 key 解耦。 - 存储:仅存哈希(
argon2或sha256,带前缀便于检索),原文只在创建时返回一次。 - 一个虚拟密钥可绑定:允许的模型集合、配额、限流、过期时间、所属团队/项目。
- 头部兼容:OpenAI 用
Authorization: Bearer,Anthropic 用x-api-key—— 两者都接受。
- 主维度:每用户(
user_id)。token 余额、并发、RPM/TPM 都以用户为计量主体(一个用户的两把 Key 合并计算)。 - 指标:并发数(in-flight 请求)、RPM(请求/分钟)、TPM(token/分钟)。
- 全部走进程内存,无 Redis:
- 并发:
UserState内AtomicU32,请求进入fetch_add、结束fetch_sub(用 RAII guard 保证断流/panic 也释放),超concurrency_limit立即拒绝;流式连接占用到流结束。 - RPM/TPM:
governor(进程内令牌桶)按用户一个限流器,存于DashMap<user_id, RateLimiter>。
- 并发:
- 限额取值:用户级设置优先,未设则取全局默认(管理后台配,存内存)。
- 超限返回 429 +
Retry-After(按客户端协议格式)。
关于「并发按用户还是按 Key」的取舍见 §7.2.1。
- 本系统里 Key 不是独立租户,只是同一用户的两个协议入口(OpenAI / Anthropic),且共用同一 token 余额。计量主体天然是用户。
- 若按 Key 限并发,用户用两把 Key 就能突破限制,失去公平性与成本保护意义;管理后台设的也是「用户并发」。
- 因此:并发、RPM/TPM、配额都以用户为准(跨该用户所有 Key 累加)。
- 预留扩展:
api_keys可保留可选的concurrency_limit作为用户额度内的子上限(默认不启用),将来若演进出「团队/多项目」场景再开启 Key 级隔离。
本系统以 token 为计费与配额单位(非美元)。
- 每个用户有一个全局 token 余额(
users.token_balance),所有接口、所有 Key 的调用都从这同一个池子里扣减。 - 新用户注册赠送 1000 万 token(10,000,000)。
- 计量口径:每次调用结束后,按
input_tokens + output_tokens(可对不同模型设权重系数,见下)从余额扣减。 - 模型权重系数:不同模型成本差异大,每个模型可设
token_multiplier(如贵模型 1 token 记 3),实际扣减 = 原始 token × 系数。默认 1。该系数在管理后台可视化配置(model_prices.token_multiplier),热更新到内存。 - 内存计数 + 异步落盘(关键):余额是
UserState.token_balance(AtomicI64),扣费在内存原子操作完成,不同步写库。后台任务周期性(如每 5s 或累计变更达阈值)批量回写 SQLite;进程退出时强制 flush。- 余额校验在请求入口读内存,零 DB 查询。
- 崩溃容忍:极端情况下最多丢失最近一个 flush 周期内的扣减(用户少量"白嫖"),单机场景可接受;flush 周期越短窗口越小。
- 余额 ≤ 0 时,数据面调用拒绝(按客户端协议返回 402/429);门户提示充值。
- 软阈值(如剩余 10%)触发提醒。
- 用量明细落库(
usage_logs),记录每次调用的 input/output token、接口类型、模型、上游。 - 门户:用户看自己的「剩余额度卡片 + 用量趋势」。
- 管理后台:按用户/模型/供应商聚合,看消耗与剩余额度。
- 价格表
model_prices仍保留(用于内部成本核算/对账),但对 C 端展示以 token 为准。
类型说明:以下 DDL 用通用 SQL 表达概念;在 SQLite 中
UUID→TEXT、BIGINT/INT→INTEGER、NUMERIC→REAL或TEXT(高精度金额用 TEXT 存)、TIMESTAMPTZ→TEXT(ISO8601)或INTEGER(unix 秒)、JSONB→TEXT(JSON 字符串)、TEXT[]→TEXT(JSON 数组)。开启 WAL 模式(PRAGMA journal_mode=WAL; synchronous=NORMAL)以支持多读单写并发。
-- C 端终端用户
CREATE TABLE users (
id UUID PRIMARY KEY,
phone TEXT UNIQUE, -- 手机号(可空,纯第三方登录时)
nickname TEXT,
avatar_url TEXT,
status TEXT NOT NULL DEFAULT 'active', -- active|disabled|banned
token_balance BIGINT NOT NULL DEFAULT 10000000, -- 剩余 token 余额;新用户默认赠 1000 万
token_granted_total BIGINT NOT NULL DEFAULT 10000000, -- 累计授予(注册赠送+充值)
token_used_total BIGINT NOT NULL DEFAULT 0, -- 累计消耗
concurrency_limit INT, -- 并发上限(NULL=用全局默认),管理后台可设
rpm_limit INT, -- 每分钟请求数(NULL=全局默认)
tpm_limit INT, -- 每分钟 token 数(NULL=全局默认)
allowed_models TEXT[] DEFAULT '{}', -- 模型白名单(空=全部)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ
);
-- 注:token_balance 实时扣减走内存 AtomicI64,周期性批量回写本表(见 §8.1)。
-- 第三方身份绑定(微信/支付宝),一个用户可绑多个
CREATE TABLE user_identities (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
provider TEXT NOT NULL, -- wechat | alipay
-- 微信:优先用 unionid 跨应用统一身份;openid 按 app 维度
external_id TEXT NOT NULL, -- unionid / alipay user_id
union_id TEXT, -- 微信 unionid(若有)
raw_profile JSONB, -- 第三方返回的原始资料
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(provider, external_id)
);
-- 短信验证码(放内存 Map 带 TTL 即可;此表用于审计/风控,可选)
CREATE TABLE verification_codes (
id UUID PRIMARY KEY,
phone TEXT NOT NULL,
code_hash TEXT NOT NULL, -- 不存明文
purpose TEXT NOT NULL, -- login | bind | ...
attempts INT DEFAULT 0,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 管理员
CREATE TABLE admins (
id UUID PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL, -- argon2
role TEXT NOT NULL DEFAULT 'operator', -- superadmin|operator|viewer
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 团队/项目(B 端可选;C 端自助场景下用户即默认归属)
CREATE TABLE teams (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 虚拟密钥
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id), -- C 端密钥归属用户
interface_kind TEXT NOT NULL, -- openai | anthropic(对应门户两张接口卡片)
team_id UUID REFERENCES teams(id),
key_hash TEXT NOT NULL UNIQUE, -- argon2/sha256
key_prefix TEXT NOT NULL, -- 'rk-abc...' 前几位,便于展示/检索
name TEXT,
allowed_models TEXT[] DEFAULT '{}', -- 空=全部
rpm_limit INT,
tpm_limit INT,
budget_usd NUMERIC(12,4),
spent_usd NUMERIC(12,4) DEFAULT 0,
expires_at TIMESTAMPTZ,
disabled BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 上游供应商(也可纯配置文件,DB 用于动态管理)
CREATE TABLE providers (
id UUID PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
kind TEXT NOT NULL, -- openai|anthropic|azure_openai|gemini...
base_url TEXT NOT NULL,
config JSONB NOT NULL, -- 协议/版本/超时等
secret_ref TEXT NOT NULL, -- 指向密钥管理(env/vault)
enabled BOOLEAN DEFAULT true
);
-- 模型路由
CREATE TABLE model_routes (
id UUID PRIMARY KEY,
model_name TEXT NOT NULL,
targets JSONB NOT NULL, -- [{provider, upstream_model, weight}]
failover JSONB,
enabled BOOLEAN DEFAULT true
);
-- 用量日志(高写入,建议分区表 by day)
CREATE TABLE usage_logs (
id BIGSERIAL PRIMARY KEY,
request_id UUID NOT NULL,
api_key_id UUID,
team_id UUID,
client_protocol TEXT, -- openai|anthropic
model_name TEXT, -- 逻辑模型
provider TEXT, -- 实际上游
upstream_model TEXT,
input_tokens INT,
output_tokens INT,
cache_read_tokens INT,
cache_write_tokens INT,
cost_usd NUMERIC(12,6),
status INT, -- HTTP 状态
latency_ms INT,
ttft_ms INT, -- 首 token 时间
stream BOOLEAN,
error_code TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 模型价格表
CREATE TABLE model_prices (
model_name TEXT PRIMARY KEY,
input_per_mtok NUMERIC(12,6), -- 内部成本核算用(美元)
output_per_mtok NUMERIC(12,6),
cache_read_per_mtok NUMERIC(12,6),
cache_write_per_mtok NUMERIC(12,6),
token_multiplier NUMERIC(8,3) NOT NULL DEFAULT 1.0 -- C 端扣费倍率,管理后台可改
);用量写入用「异步批量管道」:请求结束发到内存 channel,后台任务批量 INSERT,避免阻塞主路径。
设计目标:请求热路径上零同步 DB 查询、零同步 DB 写;SQLite 仅用于冷启动加载与异步落盘。
内存态(AppState,Arc 共享)
pub struct AppState {
// key 明文哈希 → Key 信息(校验入口,O(1),不查库)
keys: DashMap<String, KeyEntry>, // key_hash -> {user_id, interface_kind, revoked}
// 用户运行态
users: DashMap<Uuid, Arc<UserState>>,
// 配置:路由/供应商/全局默认限额/模型倍率,热替换
config: ArcSwap<RuntimeConfig>,
// 异步落盘通道
usage_tx: mpsc::Sender<UsageEvent>,
}
pub struct UserState {
token_balance: AtomicI64, // 实时扣减
in_flight: AtomicU32, // 并发计数
rate: governor::RateLimiter<...>, // RPM/TPM
status: AtomicU8, // active/disabled
dirty: AtomicBool, // 余额是否需回写
// 限额快照(用户级覆盖)
concurrency_limit: AtomicU32,
}生命周期
- 启动:从 SQLite 全量加载
users、未吊销的api_keys、配置到内存(几万~几十万行,秒级)。 - API Key 校验:取请求里的 key → 哈希 →
keys.get(hash)→ 拿到user_id→users.get。全程内存,无 DB。 - 扣费/限流/并发:全在
UserState的原子量上操作。 - 写回(后台任务):
- 余额:定时(如每 5s)扫描
dirty=true的用户,批量UPDATE回 SQLite;退出时强制 flush。 - 用量明细:请求结束发
UsageEvent到 mpsc channel,后台批量事务 INSERT(如每 200 条或每 1s 一批),避开 SQLite 单写瓶颈。
- 余额:定时(如每 5s)扫描
- 变更(创建用户/建 Key/改限额/改倍率):这类操作低频,采用「先写 SQLite,成功后更新内存」,保证持久与一致;不在热路径。
SQLite 并发要点
- WAL 模式:多读不阻塞写,写串行化 → 所以所有写集中到少数后台任务,批量提交。
- 连接池:读用多连接,写用单连接串行(或一个专职写任务),避免
SQLITE_BUSY。 - 热路径几乎不碰库,SQLite 的单写限制就不再是瓶颈。
取舍:崩溃会丢失最近一个 flush 周期的余额扣减与未落盘用量。单机产品可接受;若要更强持久,可缩短 flush 周期或对充值/扣大额走同步写。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /v1/chat/completions |
聊天补全(流式/非流式) |
| POST | /v1/embeddings |
向量嵌入(转发) |
| POST | /v1/completions |
旧版文本补全(可选) |
| GET | /v1/models |
列出网关暴露的逻辑模型 |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /v1/messages |
Messages API(流式/非流式) |
| POST | /v1/messages/count_tokens |
token 计数(可选) |
| GET | /v1/models |
与上面共享 |
注意:
/v1/models在两套规范里响应结构略不同,按请求路径/头部判断返回格式。也可用不同前缀区分,如/openai/v1/...与/anthropic/v1/...,推荐同时支持:既支持标准路径(方便 SDK base_url 直接替换),也支持带前缀路径(消除歧义)。
- 透传上游错误时,翻译成客户端协议对应的错误结构(OpenAI 的
{error:{type,message,code}}vs Anthropic 的{type:"error",error:{type,message}})。 - 网关自身错误(限流/配额/路由失败)也按客户端协议格式返回,保证 SDK 能正确解析。
GET /admin/users 所有用户列表(状态、剩余/已用 token)
POST /admin/users 创建用户(手机号/初始额度)
GET /admin/users/{id} 用户详情
PATCH /admin/users/{id} 设并发/RPM/TPM/模型白名单/状态、增减额度
GET /admin/users/{id}/usage 该用户 token 消耗明细与趋势
DELETE /admin/users/{id}/keys 重置/吊销该用户全部 Key
POST /admin/keys 创建虚拟密钥
GET /admin/keys 列表(可按 user 过滤)
DELETE /admin/keys/{id} 吊销
POST /admin/providers 增删改上游
POST /admin/routes 配置模型路由
PATCH /admin/models/{name}/multiplier 设模型扣费倍率(热更新)
GET /admin/usage 全局用量/成本(按 user/model/provider/时间)
POST /admin/reload 热加载配置
GET /healthz /readyz /metrics 健康检查与 Prometheus 指标
- 日志:
tracing结构化日志,每请求一个 span,带request_id、model、provider。默认不记录 prompt/response 正文(隐私),可按密钥/团队开启采样记录。 - 指标(Prometheus):
requests_total{protocol,model,provider,status}request_duration_seconds(直方图)ttft_seconds(首 token 延迟)tokens_total{type=input|output}upstream_errors_total{provider,code}ratelimit_rejections_total
- 追踪:OpenTelemetry,贯穿入站→路由→上游调用。
- 告警:错误率、p99 延迟、预算耗尽、上游不可用。
- 精确缓存:对完全相同的(model + messages + 参数)请求,key 为规范化哈希,命中直接返回。需谨慎处理流式回放与
temperature>0场景(默认仅缓存确定性请求)。 - 存储:进程内 LRU(如
moka/lru),带 TTL 与容量上限;单机无需 Redis。 - 命中时仍记用量(标记
cached=true,成本计 0 或折扣)。 - 语义缓存(向量相似)列为后续增强,默认不开。
- 虚拟密钥与真实上游密钥隔离;上游密钥由 secret 管理(env / Vault / KMS),不入库明文。
- 全链路 TLS;Admin API 独立强鉴权 + IP allowlist。
- 输入大小限制、超时、最大并发,防资源耗尽。
- 可选:内容审核钩子(请求/响应过审核接口)、PII 脱敏。
- 审计日志:谁在何时改了路由/密钥/供应商。
- 依赖审计:
cargo audit接入 CI。
- 形态:单进程单二进制(static musl)+ 一个 SQLite 文件;Docker 镜像或裸机直接跑,零外部中间件。
- 依赖:仅 SQLite(随二进制,无需独立部署);无 Redis、无 PostgreSQL。
- 状态:有状态(内存态 + SQLite 文件),单实例运行;不做水平扩展(单机定位)。需要更大规模时再演进为「内存态外置 + 共享 DB」。
- 配置:环境变量 + 配置文件(
figment合并),敏感项(上游 key、微信/支付宝 secret)走 env/secret 文件。 - 持久化备份:定期备份 SQLite 文件(WAL checkpoint 后复制,或
VACUUM INTO)。 - 优雅退出:收到 SIGTERM 时 flush 内存余额与用量队列再退出,避免丢数据。
- 进程守护:systemd / supervisor 拉起;崩溃自动重启后从 SQLite 重建内存态。
| 指标 | 目标 |
|---|---|
| 网关转发额外延迟(p99,不含上游) | < 5 ms |
| 单实例并发流式连接 | > 10k(取决于上游与内存) |
| 吞吐 | 主要受上游限制;网关不应是瓶颈 |
| 内存 | 空闲 < 100MB,随连接数线性增长 |
用户在门户选择登录方式
├─ 手机号:输入手机号 → 收短信验证码 → 校验 → 找到/创建 user
├─ 微信:扫码授权 → code → 换 access_token + openid/unionid → 找到/创建 user
└─ 支付宝:授权 → auth_code → 换 access_token + user_id → 找到/创建 user
│
▼
颁发会话 JWT(门户面凭证,含 user_id)
│
▼
首次登录:创建 user,赠送 1000 万 token 余额(不自动建 Key)
│
▼
门户首页:顶部「剩余额度卡片」+ 下方两张接口卡片(OpenAI / Anthropic)
每张卡片有「创建」按钮,Key 由用户手动创建
同一个人可能既用手机号又用微信登录,需要避免产生重复账号:
- 微信:以
unionid(同主体下跨公众号/小程序/网站应用唯一)为主键关联;无 unionid 时退化用openid。 - 支付宝:以
alipay user_id关联。 - 绑定策略:登录成功后,若当前会话已是某 user,则把第三方身份绑定到该 user;若该第三方身份已存在则直接登录对应 user。
- 允许用户在门户「账号设置」里绑定/解绑手机号、微信、支付宝,实现多方式登录同一账号。
- 手机号是软主键:若第三方资料里能拿到手机号(支付宝授权可申请),可提示用户合并。
- 发码端点
POST /portal/auth/sms/send:参数phone。- 风控:同手机号 60s 内限发 1 次、单日上限;同 IP 限频;图形/滑块验证码前置(防刷)。
- 生成 6 位数字码,存内存 Map(
DashMap<phone, {hash, expire}>,TTL 5 分钟);只存哈希。 - 调短信服务商(阿里云 SMS / 腾讯云 SMS)发送。
- 校验登录
POST /portal/auth/sms/verify:参数phone、code。- 校验码、次数限制(≤5 次,超限失效);成功后:
users中按 phone upsert;颁发 JWT;首登触发建 Key。
- 校验码、次数限制(≤5 次,超限失效);成功后:
采用方案:H5(公众号网页授权,snsapi_userinfo) —— 在微信内置浏览器中打开门户 H5 完成授权。
前置条件:已认证的微信公众号(服务号),配置网页授权域名(redirect_uri 的域名需在公众号后台白名单)。
服务端流程:
- 前端引导跳转授权页:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=ENC&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect - 微信回调带
code到后端GET /portal/auth/wechat/callback。 - 后端用
code+appid+secret换网页授权access_token+openid。 - 用
access_token+openid拉用户资料(昵称/头像),并取unionid(公众号需绑定开放平台才有 unionid;有则用 unionid 做账号合一,无则退化用 openid)。 - 按
unionid/openid在user_identities找/建用户 → 颁发 JWT,前端跳回门户。
仅做 H5,不做 PC 扫码 / App。
appid/secret走 secret 管理,不入库明文;回调state用一次性随机值并校验防 CSRF。 注意非微信浏览器(如 PC 端)打开门户时,微信登录入口应隐藏或提示「请在微信中打开」,手机号/支付宝登录仍可用。
- 前端跳转支付宝授权页(
alipay.user.info.share或第三方登录 scopeauth_user)。 - 回调带
auth_code→ 后端GET /portal/auth/alipay/callback。 - 用
auth_code调alipay.system.oauth.token换access_token+user_id。 - (可选)
alipay.user.info.share拉资料。 - 按
user_id在user_identities找/建用户 → 颁发 JWT。
需要支付宝开放平台应用 + RSA2 密钥对(应用私钥签名、支付宝公钥验签)。SDK 侧用社区 crate 或自实现签名。
- JWT:短期 access token(如 2h)+ 刷新 token(如 30d,可旋转);需即时吊销时用内存会话黑名单/白名单(
DashMap,随进程,单机够用)。 - Cookie(HttpOnly + Secure + SameSite)或 Authorization 头,二选一并防 CSRF。
- 风控:登录异常告警、设备/IP 记录、验证码防刷、第三方回调
state校验。 - 门户面 JWT 不能直接调
/v1/*(数据面),反之亦然。
额度与 Key 解耦:token 余额是用户全局的(注册即送 1000 万);Key 只是调用凭证,本身不带额度,所有 Key 共用同一余额。
门户布局:
- 顶部一张额度卡片:显示剩余 token 总额(如
剩余 98,234,100 / 100,000,000)。 - 下方两张接口卡片:
OpenAI与Anthropic,各自独立一把 Key(interface_kind区分),各带 base_url 和调用示例。
Key 生命周期(每张接口卡片各自独立):
- 创建:卡片初始为空,显示「创建 Key」按钮。点击后:
- 后端生成
rk_live_+ 高熵随机串(如 32 字节 base62)。 - 仅存哈希(
key_hash)+ 前缀(key_prefix,如rk_live_a1b2…)入库;明文不落库。 - 接口仅在本次响应中返回一次明文,前端弹窗展示,提示用户「请立即复制,关闭后无法再次查看」。
- 后端生成
- 展示:之后页面只显示掩码密文(如
rk_live_a1b2****************wxyz),无法再看到完整明文。 - 刷新(轮换):卡片上有「刷新」按钮。点击后:
- 生成一把新 Key,旧 Key 立即失效(撤销),新 Key 哈希入库。
- 与创建一致:弹窗显示新明文一次。
- 风险提示:刷新会使正在使用旧 Key 的应用立刻失效,需二次确认。
接口设计:
POST /portal/keys(body:interface_kind)→ 创建,响应含明文(唯一一次)。POST /portal/keys/{id}/rotate或POST /portal/keys/rotate(body:interface_kind)→ 轮换,响应含新明文。GET /portal/keys→ 列表,仅返回掩码key_prefix+ 状态,绝不返回明文。
约束:UNIQUE(user_id, interface_kind) WHERE revoked = false —— 每用户每接口同时只有一把有效 Key。
余额耗尽时数据面调用返回 402/429(按客户端协议格式)。
POST /portal/auth/sms/send 发送短信验证码
POST /portal/auth/sms/verify 验证码登录
GET /portal/auth/{wechat|alipay}/url 获取第三方授权跳转地址
GET /portal/auth/{wechat|alipay}/callback 第三方回调
POST /portal/auth/refresh 刷新会话
POST /portal/auth/logout 登出
GET /portal/me 当前用户资料
POST /portal/identities/bind 绑定第三方/手机号
GET /portal/keys 我的 Key 列表(仅掩码,不含明文)
POST /portal/keys 创建 Key(body: interface_kind;响应含明文一次)
POST /portal/keys/rotate 刷新/轮换 Key(body: interface_kind;响应含新明文一次)
DELETE /portal/keys/{id} 吊销 Key
GET /portal/balance 我的剩余 token 额度(顶部卡片数据)
GET /portal/usage 我的用量趋势/明细
POST /portal/recharge 充值 token(对接支付,后续)
| 应用 | 受众 | 路径/域名(示例) | 重点 |
|---|---|---|---|
| Client Portal | C 端开发者 | portal.example.com |
登录、API Key 管理、用量/账单、文档、充值 |
| Admin Console | 运营/管理员 | admin.example.com |
供应商、路由、用户、密钥、价格、报表、配置 |
两者共享 UI 基础库,但独立构建、独立部署、独立鉴权域。
| 关注点 | 选型 |
|---|---|
| 框架 | React 18 + TypeScript |
| 构建 | Vite |
| 路由 | React Router |
| 数据请求/缓存 | TanStack Query(React Query) |
| 全局状态 | Zustand(轻量) |
| UI 组件库 | Ant Design(中后台场景成熟,中文友好) |
| 表单/校验 | React Hook Form + Zod |
| 图表 | ECharts / Recharts(用量趋势、成本) |
| HTTP | axios / fetch 封装,统一注入 JWT、错误处理 |
| 国际化 | i18next(中/英) |
- 登录页:手机号(发码/验证)、微信扫码、支付宝按钮三种入口。
- 仪表盘(首页核心布局):
- 顶部额度卡片:剩余 token / 总授予 token(进度条),今日消耗。
- 下方两张接口卡片:
OpenAI与Anthropic,每张含:- base_url、Key(掩码显示)、状态。
- 「创建」按钮(无 Key 时)/「刷新」按钮(有 Key 时,二次确认)。
- 创建/刷新成功 → 明文弹窗(一次性,带「复制」按钮 + 不可再看提示)。
- 调用示例代码片段(对应 SDK 的 base_url 替换写法)。
- 用量明细:按模型/时间维度图表与列表。
- 充值:token 套餐充值(对接支付,后续)。
- 账号设置:绑定/解绑手机号、微信、支付宝。
- 文档/快速开始。
- 概览:全局 QPS、错误率、各供应商健康、成本。
- 供应商管理:增删改、密钥引用、健康探测。
- 路由管理:模型名 → 目标(权重/failover/灰度)可视化编辑。
- 用户管理(核心):
- 查看所有用户列表:注册方式、注册时间、状态、剩余/已用 token。
- 查看单用户token 消耗明细(按时间/模型/接口)与趋势。
- 手动创建用户(预置手机号/初始额度),用于内部/对公开通。
- 设置用户并发上限、RPM/TPM、模型白名单。
- 调整额度(增/减赠送 token)、禁用/封禁、重置/吊销其 Key。
- 密钥管理:全局视角、查询某 Key 归属用户、批量吊销。
- 价格/倍率管理:模型
token_multiplier(扣费倍率)、内部成本单价维护,热更新。 - 用量报表:按用户/模型/供应商聚合。
- 系统配置:限流默认值、缓存开关、热加载。
- 管理员登录:用户名 +
argon2密码 → 颁发管理员 JWT(管理面,独立于门户/数据面)。 - 角色:
superadmin:全部权限(含供应商上游密钥、删用户)。operator:日常运营(改限额、调额度、看用量),不能查看/修改供应商上游密钥。viewer:只读。
- 敏感操作(吊销 Key、删用户、改供应商、改倍率)需二次确认并写审计日志(谁、何时、改了什么)。
- 建议加 IP allowlist;管理端不暴露公网或仅内网可达。
管理操作都是低频写,统一走「先写 SQLite,成功后更新内存态」,改完即时生效、无需重启:
| 管理操作 | 持久化 | 内存生效方式 |
|---|---|---|
| 改路由 / 供应商 / 模型倍率 / 全局默认 | 写 SQLite | 原子替换 ArcSwap<RuntimeConfig>,下一个请求即用新配置 |
| 改用户限额 / 额度 / 状态 | 写 SQLite | 更新该用户 UserState 的原子量(concurrency_limit/token_balance/status) |
| 创建用户 | 写 SQLite | 插入 DashMap<user_id, UserState> |
| 创建 / 吊销 Key | 写 SQLite | 增删 DashMap<key_hash, KeyEntry> |
因此管理端完全不碰请求热路径,与数据面的高并发互不影响。
- 鉴权:Portal 用门户 JWT;Admin 用管理员 JWT(RBAC),互不通用。
- 第三方登录回调:后端处理
code/auth_code换取身份后,通过前端回调页带短期票据交换 JWT,避免 token 出现在 URL。 - API 契约:后端用
utoipa等生成 OpenAPI,前端据此生成 TS 类型(openapi-typescript),保证类型一致。 - 跨域:同源部署或配置 CORS;敏感操作二次确认。
M1 — 最小可用(MVP)
- axum 服务骨架、配置加载
- OpenAI 入站 + OpenAI/兼容上游(含 DeepSeek 等)直通,流式打通
- 虚拟密钥鉴权(配置文件级)、基础日志
M2 — 双协议 + 互转
- Anthropic 入站、Anthropic 上游
- OpenAI ⇄ Anthropic 双向协议翻译(含工具调用、多模态、流式状态机)
- 路由引擎(权重 + failover)
M3 — 治理 + 用户体系后端
- SQLite 接入 + 内存态(DashMap/Atomic)+ 异步落盘任务:用户/身份/密钥/用量/价格/路由
- 内存限流(governor)、并发计数、token 余额扣减
- C 端登录后端:手机号短信、微信、支付宝 OAuth;会话 JWT;登录自动发 Key
- Portal API + Admin API + 热加载
M4 — 前端(React)
- Client Portal:三种登录、仪表盘、API Key 管理、用量/账单、账号设置
- Admin Console:供应商/路由/用户/密钥/价格/报表
- OpenAPI → TS 类型契约
M5 — 可观测与生产化
- Prometheus 指标、OTel 追踪
- 缓存模块、内容审核钩子
- 更多上游:Gemini、Azure、Bedrock、本地 vLLM
- 充值/支付对接
- 压测与调优、Helm chart
- ✅ 计费单位:按 token,非美元。门户顶部显示全局剩余 token 余额(所有接口/Key 共用)。
- ✅ 新用户赠送:1000 万 token(10,000,000)。
- ✅ Key 模型:每用户每接口(OpenAI / Anthropic)各一把 Key,共用同一 token 余额。
- ✅ Key 创建:不自动建,用户手动点「创建」;明文仅创建时弹窗显示一次,之后只显示掩码。
- ✅ Key 刷新:「刷新」按钮轮换 Key,旧 Key 立即失效,新明文同样弹窗显示一次。
- ✅ 账号体系:纯 C 端个人账号;管理后台可查看所有用户。
- ✅ 模型扣费倍率:每模型
token_multiplier,在管理后台配置、可热更新。 - ✅ 微信登录:仅 H5(公众号网页授权
snsapi_userinfo),不做 PC 扫码/App。 - ✅ 并发限制维度:按用户(非按 Key);管理后台可设。
- ✅ 部署形态:单机单进程,无 Redis;数据库用 SQLite(WAL);热路径全走内存,DB 仅冷启动加载 + 异步落盘。
- 首批必须支持的上游供应商有哪些?(决定 M1/M2 连接器优先级)
- 资质准备:微信服务号(且绑定开放平台以拿 unionid)、支付宝开放平台应用、短信服务商账号——是否已就绪?
- 充值 / 付费模式:除赠送额度外,后续充值是「买 token 套餐」还是「充值金额按用量折算 token」?
- 额度耗尽行为:直接拒绝,还是允许少量透支 / 自动提醒充值?
- 部署目标:K8s / 裸机 / Serverless?是否区分国内合规区域?
- 是否记录 prompt 正文(调试/审计)及其合规边界?
- embeddings / 图像 / 语音等非 chat 端点是否在 v1 范围内?