服务端网关
前端组件库通过 ChatAdapter 与后端解耦,但真实项目总需要一个服务端:把浏览器的聊天请求转发给大模型 API,同时让 API Key 只存在在服务端。@toimc/server 就是这个网关——基于 Hono 的通用接收与转发层,配套 @toimc/agents 提供多模型协议适配。
浏览器 → @toimc/vue 组件 → ChatAdapter(fetch SSE)
→ @toimc/server 网关(鉴权/限流/协议归一)
→ @toimc/agents 适配器(OpenAI 兼容 / Anthropic / 自定义 / mock)
→ 上游大模型 API安装
pnpm add @toimc/server @toimc/agents
# Node 运行时再装服务适配器(Bun/Deno/边缘运行时不需要)
pnpm add @hono/node-server最小可用网关
import { createChatGateway } from '@toimc/server'
import { serve } from '@hono/node-server'
const app = createChatGateway({
models: [
{
id: 'deepseek-chat',
provider: 'openai-compat', // OpenAI 兼容协议:OpenAI/DeepSeek/GLM/Kimi/OpenRouter 等
model: 'deepseek-chat',
apiKey: process.env.DEEPSEEK_API_KEY!,
baseURL: 'https://api.deepseek.com/v1',
name: 'DeepSeek Chat',
description: '性价比之选',
},
],
})
serve({ fetch: app.fetch, port: 8787 })三行核心:配置模型 → 创建网关 → 交给任意运行时监听。前端用文档站同款 SSE Adapter(POST /api/chat 消费 StreamChunk 流)即可对接。
多模型注册
provider 决定协议适配器,同一协议换 baseURL + model 即接入不同厂商:
import { ModelRegistry } from '@toimc/agents'
const registry = new ModelRegistry()
registry.register({
id: 'gpt-5-mini',
provider: 'openai-compat',
model: 'gpt-5-mini',
apiKey: process.env.OPENAI_API_KEY!,
})
registry.register({
id: 'claude-sonnet',
provider: 'anthropic', // Anthropic 原生 /v1/messages 协议
model: 'claude-sonnet-4-5',
apiKey: process.env.ANTHROPIC_API_KEY!,
})
const app = createChatGateway({ models: registry })GET /api/models 自动返回注册表视图(id/name/description,永不泄漏 apiKey),前端模型选择器直接消费。
自定义适配器
实现 IModelAdapter(chat + chatStream,流式产出 StreamChunk)即可接入任意后端——本仓库的 mock 演示服务就是这么做的:
import type { ChatRequest, ChatResponse, IModelAdapter } from '@toimc/agents'
import type { StreamChunk } from '@toimc/core'
const myAdapter: IModelAdapter = {
async chat(req: ChatRequest): Promise<ChatResponse> {
/* 一次性返回 */
},
async *chatStream(req: ChatRequest): AsyncGenerator<StreamChunk> {
yield { type: 'text', content: '你好' }
yield { type: 'done', content: '' }
},
}
registry.registerAdapter('my-model', myAdapter, {
name: '自定义模型',
description: '走我自己的后端',
})智能体接入(@toimc/server/mastra)
上文各 provider 都是纯模型转发。需要工具调用、会话记忆这类 Agent 能力时,用可选子路径 @toimc/server/mastra:createMastraGateway 接收 agent 定义列表(MastraAgentDefinition,纯配置对象),逐个构建注册进同一个网关,models 之外的网关选项(auth / rateLimit / onComplete)原样透传:
import { createMastraGateway } from '@toimc/server/mastra'
const { app, registry } = await createMastraGateway({
models: [{ id: 'mock-pro', provider: 'openai-compat', model: 'mock', apiKey: 'unused' }],
agents: [
{
id: 'chat-agent', // 网关模型 id,GET /api/models 可见
name: 'Chat Agent',
model: 'deepseek/deepseek-chat', // 或 { id, url, apiKey? } 自定义端点
instructions: '你是演示 Agent…',
tools: { getTimeTool, getWeatherTool }, // createTool 产物字典
memory: createMemory, // Memory 实例工厂(惰性);不传则无记忆
},
],
})agent 构建失败(@mastra/core 未装、模型配置非法)时整体抛含 agent id 的可读错误——启动即失败优于静默降级。@mastra/core 是该子路径的可选 peer,主入口零新增依赖。完整指南(安装、env、Studio)见智能体接入;不想引入 @toimc/server 时,@toimc/agents/mastra 子路径的 createMastraModel 也把 Mastra Agent 包装成标准 IModelAdapter,注册方式与上文自定义适配器一致。
运行时注册 vs 环境变量注册
MASTRA_MODEL 是启动时经环境变量注册(id 固定 chat-agent);dev-server 还提供运行时注册:浏览器把表单配置 POST /api/providers 上来,服务端即刻组装注册(id 递增 custom-{n};PUT /api/providers/:id 原位更新沿用原 id)。两条路径写进同一个 registry,并存不冲突,GET /api/models 里都可见:
| 环境变量注册 | 运行时注册(API) | |
|---|---|---|
| 触发时机 | 进程启动(readDevServerEnv 探测) | 请求到达(POST 创建 / PUT 原位更新) |
| 模型 id | chat-agent | custom-{n} 递增(注册表落盘 .temp/providers.json,git 忽略;重启经 restore() 恢复且 id 沿用、自增序号抬高防撞) |
| 配置来源 | MASTRA_MODEL / MASTRA_MODEL_URL 等 env | 请求体(ProviderFormPayload:name / provider / baseURL / apiKey / model) |
| 密钥去向 | 环境变量 | 服务端内存 + .temp/providers.json 本地落盘(git 忽略):不进日志、GET /api/providers 只回脱敏视图(不含 key,baseURL 供编辑预填) |
| 注销 | 无(进程级) | DELETE /api/providers/:id |
组装逻辑在 registerRuntimeProvider(packages/dev-server/src/routes/providers.ts),按协议类型走 createMastraModel 的两种 model 形态:
// openai-compat:{ id, url, apiKey } 对象(OpenAI 兼容自定义端点)
model: { id: payload.model, url: payload.baseURL, apiKey: payload.apiKey }
// anthropic:'anthropic/{model}' 路由串,注册前把密钥注入 env(Mastra 官方路由从 env 取)
process.env.ANTHROPIC_API_KEY = payload.apiKey
model: `anthropic/${payload.model}`两条路径注册的模型都自动挂上 get_time / get_weather 两个演示工具与同一个 LibSQL 记忆(file:.temp/dev-server.db),因此运行时注册的模型天然支持工具调用与会话记忆。注册不做上游连通性校验(惰性连接):首次对话才真连上游,密钥错误 / 端点不通等失败按既有路径以 error chunk 呈现,SSE 流仍完整可解析。
四端点契约(POST 校验失败 400、成功 201,PUT / DELETE 目标不存在 404,GET 响应脱敏)见接口文档;前端表单组件见 ProviderSettingsDialog,端到端体验在 Playground。
中间件选项
网关默认只开 CORS;鉴权与限流按需启用,均可单独 import 底层件挂到自己的 Hono 应用:
createChatGateway({
models,
basePath: '/api', // 路由前缀,默认 '/api'
cors: true, // 默认开
logging: true, // hono/logger 请求日志,默认关
auth: { tokens: [process.env.GATEWAY_TOKEN!] }, // Bearer 认证,默认关(/health 不受影响)
rateLimit: { windowMs: 60_000, max: 30 }, // 内存限流,默认关
defaultModel: 'gpt-5-mini', // 请求不带 model 时的缺省
})流收尾钩子 onComplete
流式过程中网关按 chunk 类型累积出完整的助手消息(text 拼接、thinking 归集、tool_call/tool_result 自动配对),流结束(含客户端中止)回调,适合落库或审计:
createChatGateway({
models,
chat: {
onComplete(result) {
// result.body 原始请求体(conversationId 等透传字段在内)
// result.model 本轮使用的模型 id
// result.assistant { id, content, thinking?, toolCalls?, createdAt }
// result.durationMs 耗时;result.aborted 客户端是否中止
await saveMessage(result.body.conversationId!, result.assistant)
},
},
})线协议
POST /api/chat 请求体 { messages, model?, ...rest }——messages/model 之外的任意字段(如 speed、conversationId)会整体透传给适配器的 ChatRequest.passthrough,网关不解释。响应为 SSE:每帧 event: chunk + data: <StreamChunk JSON>,done 收尾,出错时以 error chunk 收尾保持可解析。完整接口说明见接口文档。
部署
网关只导出 fetch 风格的 Hono app,运行时自选:
- Node:
@hono/node-server(上文示例) - Vercel:
api/index.ts里export const POST = handle(app)(本项目线上演示即此形态) - Bun / Deno / Workers:对应 runtime adapter 直接挂
app.fetch
@toimc/agents 零外部依赖(原生 fetch),可脱离网关单独用于脚本、批处理等纯逻辑场景。