OpenAI 代理服务

兼容 OpenAI Chat Completions API 的代理服务,支持签名校验、流式/非流式响应

v1.0.0 OpenAI 兼容 HMAC-SHA256 签名

# 概述

本服务是一个 OpenAI API 兼容的代理中间层,位于你的 App / 客户端与真正的 LLM 服务(如 OpenAI、OpenRouter 等)之间。

架构说明

┌──────────┐      签名/鉴权       ┌──────────────┐     Bearer Token     ┌──────────────┐
│  客户端   │ ──────────────────→ │  OpenAI 代理  │ ──────────────────→ │   LLM 后端   │
│  (App)   │ ←────────────────── │  (本服务)      │ ←────────────────── │  (OpenAI等)   │
└──────────┘    SSE 流/JSON      └──────────────┘    API Response     └──────────────┘

核心功能

  • ✅ 完整的 OpenAI Chat Completions 兼容
  • ✅ 支持 流式(SSE)和 非流式 两种响应
  • 双重认证:Bearer Token / HMAC-SHA256 签名
  • ✅ 签名 防重放攻击(时间戳校验)
  • ✅ 可配置任意 后端 LLM 服务(通过环境变量)
  • ✅ 完整 Winston 日志(控制台 + 文件轮转)

# 认证方式

所有 API 请求(除了 /health/signature-demo)都需要认证。支持以下两种方式,任选其一即可。

方式一:Bearer Token(简单模式)

适用于服务端调用、快速集成场景。

Authorization: Bearer <PROXY_API_KEY>
✅ 最简单的方式,适合后端服务互相调用

方式二:HMAC-SHA256 签名(推荐)

适用于 App 端、需要防篡改和防重放的场景。需要提供三个 Header。

X-Api-Key: <PROXY_API_KEY>
X-Timestamp: <当前 Unix 时间戳(秒)>
X-Signature: <HMAC-SHA256 签名>
✅ 签名会同时验证时间戳(默认 300 秒容差),防止重放攻击
⚠️ X-Api-KeyAuthorization: Bearer 中的值都来自同一个环境变量 PROXY_API_KEY

快速获取示例

服务提供了一个签名示例端点,可以直接返回带正确签名的 curl 命令和 Header:

# 不需要鉴权即可访问
curl -X GET https://imseon-proxy.zeabur.app/v1/chat/completions/signature-demo

返回示例:

{
  "curl_example": "curl -X POST ...",
  "headers": {
    "X-Api-Key": "sk-xxx",
    "X-Timestamp": "1700000000",
    "X-Signature": "abc123..."
  }
}

# 签名算法详解

以下详细说明 HMAC-SHA256 签名的计算步骤,客户端(App/服务端)需按此方式生成 X-Signature

  1. 计算请求体 Body 的 SHA256 哈希

    将整个请求体(JSON 字符串)做 SHA256 哈希,输出 hex(小写十六进制)字符串。

    const bodyHash = crypto
      .createHash('sha256')
      .update(JSON.stringify(body))
      .digest('hex');
    // 输出示例: "e8b3dc2c8b9c0b6c7b8d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5"
    import hashlib, json
    
    body_hash = hashlib.sha256(
        json.dumps(body, separators=(',', ':'))
    ).hexdigest()
    # 输出示例: "e8b3dc2c8b9c0b6c7b8d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5"
    # 假设 body.json 包含请求体
    body_hash=$(cat body.json | sha256sum | cut -d' ' -f1)
    echo "$body_hash"
    ⚠️ 注意:Body JSON 的序列化必须使用紧凑格式(不带多余空格和换行),否则签名会不匹配。
  2. 拼接签名字符串(Payload)

    按以下格式拼接,各部分用冒号 : 分隔:

    "{timestamp}:{method}:{path}:{bodyHash}"

    参数说明:

    参数说明示例
    timestamp 当前 Unix 时间戳(秒),必须和请求头 X-Timestamp 一致 1700000000
    method HTTP 方法,大写 POST
    path 请求路径,包含前导斜杠 /v1/chat/completions
    bodyHash 第 1 步计算出的请求体 SHA256 哈希 e8b3dc2c...

    拼接后的 Payload 示例:

    "1700000000:POST:/v1/chat/completions:e8b3dc2c8b9c0b6c7b8d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5"
  3. 使用 HMAC-SHA256 计算最终签名

    PROXY_API_SECRET 为密钥,对上一步的 Payload 做 HMAC-SHA256 签名,输出 hex。

    const crypto = require('crypto');
    
    function generateSignature(secret, timestamp, method, path, body) {
      const bodyHash = crypto
        .createHash('sha256')
        .update(body)
        .digest('hex');
    
      const payload = `${timestamp}:${method}:${path}:${bodyHash}`;
    
      return crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');
    }
    
    // 使用示例
    const secret = "your-proxy-api-secret";
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const signature = generateSignature(secret, timestamp, 'POST', '/v1/chat/completions', JSON.stringify(body));
    
    console.log(signature);
    // 输出: "a1b2c3d4e5f6..."
    import hashlib
    import hmac
    import json
    import time
    
    def generate_signature(secret, timestamp, method, path, body):
        body_hash = hashlib.sha256(body.encode('utf-8')).hexdigest()
        payload = f"{timestamp}:{method}:{path}:{body_hash}"
        return hmac.new(
            secret.encode('utf-8'),
            payload.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
    
    # 使用示例
    body = {"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}
    timestamp = str(int(time.time()))
    body_str = json.dumps(body, separators=(',', ':'))
    signature = generate_signature("your-proxy-api-secret", timestamp, "POST", "/v1/chat/completions", body_str)
    print(signature)
    # 输出: "a1b2c3d4e5f6..."
    #!/bin/bash
    # 完整的 curl 请求 + 自动签名
    
    PROXY_KEY="your-api-key"
    PROXY_SECRET="your-api-secret"
    TIMESTAMP=$(date +%s)
    BODY='{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}'
    BODY_HASH=$(echo -n "$BODY" | sha256sum | cut -d' ' -f1)
    PAYLOAD="${TIMESTAMP}:POST:/v1/chat/completions:${BODY_HASH}"
    SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$PROXY_SECRET" | cut -d' ' -f2)
    
    curl -X POST https://imseon-proxy.zeabur.app/v1/chat/completions \
      -H "Content-Type: application/json" \
      -H "X-Api-Key: ${PROXY_KEY}" \
      -H "X-Timestamp: ${TIMESTAMP}" \
      -H "X-Signature: ${SIGNATURE}" \
      -d "$BODY"
  4. 在请求中添加 Header 并发起调用

    将生成的三个 Header X-Api-KeyX-TimestampX-Signature 添加到 HTTP 请求中,发送到代理服务即可。

    服务端收到请求后会:

    • 验证 X-Api-Key 是否有效
    • 检查时间戳是否在容差范围内(默认 300 秒 = 5 分钟)
    • 用相同算法重新计算签名,对比是否一致

# 接口列表

GET /health 健康检查

无需认证。用于检查服务是否正常运行。

# 请求
curl https://imseon-proxy.zeabur.app/health

# 响应
{
  "status": "ok",
  "service": "openai-proxy",
  "timestamp": "2025-05-24T10:00:00.000Z"
}
POST /v1/chat/completions 聊天完成(核心接口)

完全兼容 OpenAI Chat Completions API。支持流式和非流式两种模式。

请求头

Header是否必须说明
Content-Type✅ 是application/json
AuthorizationX-Api-Key + X-Timestamp + X-Signature✅ 是选择一种认证方式

请求体

参数类型必需说明
model string 可选 模型名称。如果环境变量 LLM_MODEL 已设置,此值会被覆盖
messages array 必需 消息列表,格式同 OpenAI。每个元素包含 rolecontent
stream boolean 可选 是否启用流式输出(SSE)。默认 false
temperature number 可选 温度,同 OpenAI 参数
max_tokens integer 可选 最大生成 token 数
top_p number 可选 Top-p 采样
其他 OpenAI 支持的标准参数均可传递

示例:非流式请求

curl -X POST https://imseon-proxy.zeabur.app/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROXY_API_KEY>" \
  -d '{
  "model": "gpt-4o",
  "messages": [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Hello!"}
  ],
  "temperature": 0.7
}'
import requests

response = requests.post(
    "https://imseon-proxy.zeabur.app/v1/chat/completions",
    headers={
        "Authorization": f"Bearer <PROXY_API_KEY>",
        "Content-Type": "application/json",
    },
    json={
        "model": "gpt-4o",
        "messages": [
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "Hello!"},
        ],
        "temperature": 0.7,
    }
)

print(response.json())
const response = await fetch("https://imseon-proxy.zeabur.app/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <PROXY_API_KEY>",
  },
  body: JSON.stringify({
    model: "gpt-4o",
    messages: [
      { role: "system", content: "You are a helpful assistant." },
      { role: "user", content: "Hello!" },
    ],
    temperature: 0.7,
  }),
});

const data = await response.json();
console.log(data);

响应(非流式)

{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1700000000,
  "model": "gpt-4o",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Hello! How can I help you today?" },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 10,
    "total_tokens": 35
  }
}

示例:流式请求

# 设置 stream: true 即可开启流式 SSE 响应
curl -X POST https://imseon-proxy.zeabur.app/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <PROXY_API_KEY>" \
  -d '{
  "model": "gpt-4o",
  "messages": [{"role": "user", "content": "Count from 1 to 5."}],
  "stream": true
}'
import requests
from sse_starlette.sse import EventSourceResponse

with requests.post(
    "https://imseon-proxy.zeabur.app/v1/chat/completions",
    headers={"Authorization": "Bearer <PROXY_API_KEY>"},
    json={"model": "gpt-4o", "messages": [{"role": "user", "content": "Count from 1 to 5."}], "stream": True},
    stream=True,
) as r:
    for line in r.iter_lines():
        if line:
            print(line.decode('utf-8'))
const response = await fetch("https://imseon-proxy.zeabur.app/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <PROXY_API_KEY>",
  },
  body: JSON.stringify({
    model: "gpt-4o",
    messages: [{ role: "user", content: "Count from 1 to 5." }],
    stream: true,
  }),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value);
  console.log(chunk);
  // 每次收到 data: {...}\n\n 格式的 SSE 数据块
}

流式响应格式

# SSE (Server-Sent Events) 格式
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1700000000,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]
GET /v1/chat/completions/signature-demo 签名示例

无需认证。返回当前有效的带签名字段和可直接运行的 curl 命令。

curl https://imseon-proxy.zeabur.app/v1/chat/completions/signature-demo

# 环境变量配置

以下环境变量用于配置代理服务的行为。在 Zeabur 控制台设置。

变量名 必需 默认值 说明
PORT 可选 3000 服务监听端口(Zeabur 推荐 8080)
LLM_BASE_URL 可选 https://api.openai.com/v1 后端 LLM 服务的基础地址
LLM_API_KEY 必需 后端 LLM 服务的 API Key
LLM_MODEL 可选 固定模型名称。设置后,客户端传入的 model 会被覆盖。
不设置则客户端可以自由指定模型
PROXY_API_KEY 必需 客户端访问代理时使用的 API Key(用于 Bearer Token 和签名认证)
PROXY_API_SECRET 可选 PROXY_API_KEY HMAC-SHA256 签名的密钥。如不设置则默认为 PROXY_API_KEY
SIGNATURE_TOLERANCE 可选 300 签名时间戳容差(秒)。默认 5 分钟,防止重放攻击

# 错误码

HTTP 状态码 错误类型 说明
401 未认证 缺少或无效的认证信息。检查 Authorization 或签名 Header 是否正确
401 签名过期 X-Timestamp 超出容差范围(默认 ±5 分钟)。请校准客户端时钟
401 签名不匹配 HMAC-SHA256 校验失败。检查 PROXY_API_SECRET 和签名算法实现
502 后端错误 代理无法连接到 LLM 后端,或后端返回了错误。检查 LLM_BASE_URLLLM_API_KEY
502 流式错误 流式请求过程中后端连接中断
413 请求体过大 请求体超过 10MB 限制

错误响应格式

{
  "error": "proxy_error",
  "message": "Signature verification failed: Timestamp expired",
  "status": 401
}