AI Gateway Lab 是一个独立的功能展示与可执行测试仓库。它不是现有 xapi-frontend / xapi-frontend-v2 的页面,也不依赖它们的路由、登录态或组件。
它解决两个问题:
- 在浏览器中以分类实验室展示 AI Gateway 的全部公开能力。
- 在 Node.js 中直接运行同一套调用代码,用于本地验证、CI 和生产 smoke test。
网页和脚本共同导入 src/lib/gateway-client.js。没有“网页写一套、脚本再写一套”的重复实现。
| 页面 | AI Gateway 端点 | 特性 |
|---|---|---|
| Overview | GET /health, GET /v1/models |
服务状态、实时模型目录、能力矩阵 |
| Text protocols | POST /v1/chat/completions |
JSON / SSE、default / cost、路由与费用 Headers |
| Text protocols | POST /v1/responses |
JSON / SSE、协议翻译 |
| Text protocols | POST /v1/messages |
JSON / SSE、Anthropic headers |
| Embeddings | POST /v1/embeddings |
向量维度与分布预览、完整 Raw JSON |
| Images | POST /v1/images/generations |
X-Provider、202 submit、Proxy task 轮询、图片渲染 |
| Videos | POST /v1/videos |
中立 DTO、X-Provider、202 submit、任务轮询、视频播放 |
| Rerank | POST /v1/rerank |
文档得分和重排对比 |
| WebSocket | WS /v1/realtime |
OpenAI Realtime GA,文本 turn 与原始事件流 |
| WebSocket | WS /v1/asr |
火山流式 ASR,二进制 + gzip,按音频时长计费 |
| WebSocket | WS /v1/tts |
豆包双向流式 TTS,PCM 24k 输出,按字符计费 |
| WebSocket | WS /v1/ast |
豆包同声传译,protobuf v4,按模态 token 计费 |
| WebSocket | WS /v1/podcast |
豆包双人播客生成,二进制 v3,按模态 token 计费 |
| JS scripts | 同上 | 离线全量测试与真实 Gateway smoke |
不把以下接口包装成已实现功能:
POST /v1/messages/count_tokens:当前 AI Gateway 明确返回 404。speed/quality:当前路径可被接受,但排序逻辑仍等同default;页面不把它们展示为可选策略。GET /internal/routing/explain:这是需要共享密钥的内部管理接口,不进入公开实验室。- 豆包 Realtime:虽然 adapter 已注册,但统一域名的
/v1/realtime冲突规则明确选择 OpenAI;因此不伪装成第六个公共路径。 - endpoint UUID 寻址:这是内部/测试兼容入口,公开实验室只展示稳定的 Host + shared path。
要求:Node.js 20+(需要原生 Fetch API)。
npm install
cp .env.example .env.local先运行确定性的本地 Mock Gateway:
npm run mock在第二个终端启动网页:
npm run dev访问 http://127.0.0.1:5175。开发模式默认通过 Vite 同源代理连接 http://127.0.0.1:4010,不需要 API Key,不产生费用,也不会触发浏览器的跨端口限制。
如果 Mock Gateway 不在默认地址,可在 .env.local 中设置 VITE_MOCK_GATEWAY_TARGET。设置 VITE_GATEWAY_BASE_URL 则会绕过开发代理并让浏览器直接请求该 Gateway,适合验证生产 CORS。
以下命令会临时启动 Mock Gateway,依次验证 health、models、三种文本协议、embeddings、rerank、图片异步任务和视频异步任务,然后自动关闭服务器:
npm run test:mockMock Gateway 也可单独启动,供浏览器或其他客户端调用:
MOCK_GATEWAY_PORT=4010 npm run mock默认 smoke 不调用图片和视频,避免无意产生异步生成费用:
AI_GATEWAY_BASE_URL=https://ai.xapi.to \
AI_GATEWAY_API_KEY=sk-xapi-… \
AI_GATEWAY_MODEL=deepseek-v4-flash \
AI_GATEWAY_MESSAGES_MODEL=deepseek-v4-flash \
npm run smoke该命令验证:
/health/v1/models- Chat Completions
- Responses
- Anthropic Messages
- Embeddings
- Rerank
不同环境可以覆盖模型:
AI_GATEWAY_EMBEDDING_MODEL=text-embedding-3-small
AI_GATEWAY_RERANK_MODEL=cohere/rerank-3.5图片和视频必须显式确认:
AI_GATEWAY_ALLOW_BILLABLE=true \
AI_GATEWAY_BASE_URL=https://ai.xapi.to \
AI_GATEWAY_API_KEY=sk-xapi-… \
AI_GATEWAY_TASK_ORIGINS=https://p.xapi.to \
npm run smoke:all未设置 AI_GATEWAY_ALLOW_BILLABLE=true 时,脚本会拒绝执行并退出。
import { GatewayClient } from './src/lib/gateway-client.js';
const client = new GatewayClient({
baseUrl: process.env.AI_GATEWAY_BASE_URL,
apiKey: process.env.AI_GATEWAY_API_KEY,
});
const result = await client.chatCompletions({
model: 'deepseek-v4-flash',
messages: [{ role: 'user', content: 'Hello' }],
});
console.log(result.status);
console.log(result.metadata.provider);
console.log(result.metadata.cost);
console.log(result.data);流式调用通过 onEvent 获取经过解析的 SSE 事件:
await client.messages(
{
model: 'deepseek-v4-flash',
max_tokens: 256,
stream: true,
messages: [{ role: 'user', content: 'Hello' }],
},
{
stream: true,
onEvent(event) {
console.log(event.event, event.data);
},
},
);WebSocket 页面默认连接统一入口 wss://ai.xapi.to,独立于开发期 HTTP Mock 代理。可以用 VITE_WS_GATEWAY_BASE_URL 覆盖:
VITE_WS_GATEWAY_BASE_URL=wss://ai.test.xapi.to npm run dev页面展示并严格限制为当前生产共享路径:
| Path | Adapter | 页面可验证范围 |
|---|---|---|
/v1/realtime |
openai-realtime |
持续双向语音;server VAD 自动分轮;用户转写、助手语音与字幕 |
/v1/asr |
volcengine-asr |
麦克风 PCM16/16k、gzip 二进制帧、实时转写 |
/v1/tts |
doubao-tts |
同一会话连续发送文本片段、PCM16/24k 音频边到边播放 |
/v1/ast |
doubao-ast |
持续麦克风输入、protobuf v4、原文/译文字幕和 Float32/24k 译音 |
/v1/podcast |
doubao-podcast |
话题或对话稿长任务、轮次文本和 PCM16/24k 音频流 |
五项能力分别实现真实的会话生命周期,没有共用“提交请求”模型:
- Realtime:连接后保持双向会话,
server_vad自动提交每个语音轮次并触发回复。 - ASR:录音期间持续发送 gzip 音频帧并持续更新转写;停止时发送协议规定的 final frame。
- TTS:
SessionStarted后可多次发送TaskRequest(text),收到的音频立即排队播放;结束会话时才发送FinishSession。 - AST:麦克风持续发送
TaskRequest(audio),原文、译文和译音同时流式返回;结束口译时发送FinishSession。 - Podcast:不是实时会话。
SessionStarted后立即发送FinishSession触发一次长任务,随后等待轮次、音频、usage 和结束事件。
麦克风规则:
- 只有点击 Start microphone 才会调用
getUserMedia,页面加载和 WebSocket 连接不会自动申请权限。 - 麦克风需要 HTTPS 或
localhost/127.0.0.1。 - Realtime 的 Stop microphone 只停止本地麦克风,不提交 turn、不结束 WebSocket 会话。
- ASR 的 Stop recognition 发送带负序列号的 final frame,等待最终转写后关闭识别流。
- AST 的 End interpretation 发送
FinishSession,等待最终字幕后结束口译会话。 - 停止、断开、切换能力或离开页面都会关闭浏览器音轨和 AudioContext。
浏览器不能在 WebSocket 握手中自行设置 Authorization 或 XAPI-Key header,因此页面使用服务端已支持的子协议认证:
const credential = 'short-lived-ws-token';
const socket = new WebSocket(
'wss://ai.xapi.to/v1/realtime',
[`xapi-key.${credential}`],
);凭据不会进入 URL。实验室允许直接粘贴 XAPI key 以便内部验证;面向终端用户的生产应用应由已登录后端调用 POST /api/keys/ws-token 获取短期凭据,再交给浏览器。当前 token 本质上仍按短期 ApiKey 校验,不应把它描述成已实现一次性消费或 endpoint 强绑定。
常见关闭码:
4401:Key 无效、过期或已撤销。4402:余额或计费预留失败。429通常发生在 Upgrade 阶段:连接频率或并发连接达到限制;浏览器 WebSocket API 不会暴露完整 HTTP response body。
异步图片/视频:
const submission = await client.images(
{ model: 'gpt-image-1', prompt: 'A routing diagram' },
{ provider: 'gpt88' },
);
const completed = await client.waitForTask(submission, {
intervalMs: 2000,
timeoutMs: 180000,
onPoll(result) {
console.log(result.data.status);
},
});网页右上角的 Configure target 可以设置:
- Gateway Base URL
- API Key
- 是否仅在当前会话记住 Key
安全规则:
- API Key 默认仅保存在 React 内存中。
- 用户主动勾选后才写入
sessionStorage。 - 不使用
localStorage保存 API Key。 - API Key 不进入 URL、分享链接、README 或前端日志。
- WebSocket 通过
Sec-WebSocket-Protocol: xapi-key.<token>携带凭据。 - Base URL 可以持久化,因为它不是秘密。
poll_url的 origin 必须在允许列表中,客户端才会向它发送 API Key。
生产的图片/视频 poll_url 通常指向 Proxy,例如 https://p.xapi.to/v1/tasks/:id,而不是 https://ai.xapi.to。官方 ai.xapi.to 配置会自动允许 p.xapi.to;自定义部署通过以下变量添加:
VITE_TASK_ORIGINS=https://tasks.example.com,https://p.xapi.to
AI_GATEWAY_TASK_ORIGINS=https://tasks.example.com,https://p.xapi.to不要把任意第三方 origin 加入列表,因为这会授权客户端向该 origin 发送 API Key。
网页直接请求 Gateway,因此生产 Gateway 至少需要允许:
- Request headers:
Authorization,Content-Type,Accept,X-Provider,Anthropic-Version,X-API-Key,XAPI-Key - Methods:
GET,POST,OPTIONS - Exposed response headers:
X-Routing-ProviderX-Routing-AttemptsX-Routing-FallbackX-Routing-TranslatedX-XAPI-Cost*X-XAPI-Billing*X-AI-Tokens-*Retry-After
当前 AI Gateway 代码只显式 expose 了计费/usage headers。如果没有补充 X-Routing-*,请求仍可以成功,但浏览器页面无法读取 Provider、fallback 和 translation;页面会显示相应提示。Node.js 脚本不受浏览器 CORS 限制。
WebSocket Upgrade 不使用 Fetch CORS 流程,但浏览器仍会发送 Origin;部署层若启用 Origin allowlist,需要允许 Lab 的实际来源。
React HTTP pages ─┐
Node smoke scripts├── src/lib/gateway-client.js ── Fetch API ── AI Gateway
│ │
React WS page ────┴── src/lib/ws-client.js ── WebSocket ─┤
└── Proxy task poll_url
关键目录:
src/
components/ 页面框架、请求/响应 Workbench
lib/
gateway-client.js 浏览器与 Node 共享客户端
ws-client.js WebSocket URL、子协议认证与 Realtime event helper
audio-capture.js 麦克风采集、重采样和 PCM16 分帧
audio-playback.js PCM16/Float32 流式音频无缝排队播放
volc-asr-client.js ASR gzip 二进制协议编解码
ast-protocol.js AST protobuf v4 最小协议编解码
doubao-protocol.js TTS/Podcast v3 会话与二进制帧编解码
capabilities.js 页面能力清单与真实状态标签
gateway-context.jsx 浏览器配置和 Key 生命周期
pages/ 分类实验页
scripts/
mock-gateway.mjs 确定性本地 Gateway
smoke.mjs 默认无图片/视频的真实 smoke
smoke-all.mjs 显式允许计费的全量 smoke
test-mock.mjs 自包含全功能验证
test/
gateway-client.test.js 共享客户端单元测试
ws-client.test.js WebSocket 凭据边界与 event helper 单元测试
npm test
npm run test:mock
npm run buildnpm test 不需要网络;test:mock 也不连接生产。
项目是标准 Vite SPA:
npm run build产物位于 dist/。静态服务器需要把未知路径回退到 index.html,以支持 /text、/images 等深链接。
部署时配置:
VITE_GATEWAY_BASE_URL=https://ai.xapi.to
VITE_WS_GATEWAY_BASE_URL=wss://ai.xapi.to
VITE_TASK_ORIGINS=https://p.xapi.to不要在任何 VITE_* 变量中放 API Key;Vite 会把这些变量编译进公开的浏览器 bundle。
当前 capabilities.js 是经过后端控制器核对的显式清单。后续建议 AI Gateway 提供 /openapi.json 或公开 /v1/capabilities,再增加契约测试校验:
- 页面清单中的端点必须存在于 Gateway contract。
- Gateway 新增公开端点时,CI 要求 Lab 补充展示或显式标记为不展示。
speed/quality真正实现独立排序后,再加入页面的可用策略清单。
本目录是独立 Git 仓库。创建远程 GitHub/GitLab 仓库、添加 remote、push、部署域名都属于外部状态变更,不由本地初始化自动执行。