OpenAI API 直连 vs 中转:实测对比与接入教程
摘要
把模型调用直接写进业务代码,最初很快,但随着模型增加,地址、密钥、模型判断和错误处理会散落在多个文件里。本文用 FastAPI 封装一个最小的统一调用服务:从环境变量读取配置,通过模型白名单限制可调用模型,设置请求超时,并把常见错误转换成统一 JSON。示例使用 OpenAI 兼容协议,实际服务地址和模型名以所选平台文档为准。
一、最终要构建什么
这个小服务提供一个 POST /chat 接口。调用方只提交模型名和消息,服务端负责创建客户端、检查模型、发送请求并返回结果。项目结构如下:
model-gateway/├── app.py├── requirements.txt├── .env.example└── .gitignore
调用方不需要知道具体 SDK 客户端如何创建,也不应该把服务端密钥放进浏览器或移动端代码。后续增加模型时,只修改服务端允许列表和配置,不改变调用方协议。
二、准备环境
创建目录并安装依赖:
mkdir model-gatewaycd model-gatewaypython -m venv .venv
Windows 激活虚拟环境:
.venv\\Scripts\\activate
Linux 或 macOS 激活虚拟环境:
source .venv/bin/activate
requirements.txt:
fastapiuvicorn[standard]OpenAIpython-dotenv
安装:
pip install -r requirements.txt
三、用环境变量保存配置
以下示例需要一个支持 OpenAI 兼容协议的服务地址,例如 jiekou.vip;实际地址、密钥和模型名以平台文档为准。
.env.example:
MODEL_API_KEY=sk_replace_meMODEL_BASE_URL=https://example.invalid/v1MODEL_TIMEOUT_SECONDS=30
不要把真实密钥提交到 Git。 .gitignore 至少加入:
.env.venv/__pycache__/
四、实现模型白名单和请求接口
app.py:
import osfrom typing import Literalfrom dotenv import load_dotenvfrom fastapi import FastAPI, HTTPExceptionfrom openai import APIConnectionError, APIStatusError, APITimeoutError, AsyncOpenAIfrom pydantic import BaseModelload_dotenv()MODEL_API_KEY = os.environ["MODEL_API_KEY"]MODEL_BASE_URL = os.environ["MODEL_BASE_URL"]MODEL_TIMEOUT = float(os.getenv("MODEL_TIMEOUT_SECONDS", "30"))ALLOWED_MODELS = { "model-a", "model-b",}client = AsyncOpenAI( api_key=MODEL_API_KEY, base_url=MODEL_BASE_URL, timeout=MODEL_TIMEOUT,)app = FastAPI(title="Model Gateway")class ChatRequest(BaseModel): model: Literal["model-a", "model-b"] messages: list[dict[str, str]]@app.post("/chat")async def chat(request: ChatRequest): if request.model not in ALLOWED_MODELS: raise HTTPException(status_code=400, detail="model is not allowed") try: response = await client.chat.completions.create( model=request.model, messages=request.messages, ) except APITimeoutError: raise HTTPException(status_code=504, detail="upstream timeout") except APIConnectionError: raise HTTPException(status_code=502, detail="upstream connection failed") except APIStatusError as exc: status = exc.status_code if 400 <= exc.status_code < 600 else 502 raise HTTPException(status_code=status, detail="upstream request failed") return { "model": request.model, "text": response.choices[0].message.content or "", "usage": response.usage.model_dump() if response.usage else None, }
这里有三个重要约束。第一,Literal 和 ALLOWED_MODELS 共同限制模型范围,调用方不能任意传入服务端支持的其他模型。第二,客户端使用异步接口,避免同步等待占满 Web 服务线程。第三,异常响应只返回通用错误描述,不把密钥、完整上游响应或内部堆栈直接返回给调用方。
五、启动并测试
启动服务:
uvicorn app:app --reload --port 8000
用 curl 发起请求:
curl -X POST http://127.0.0.1:8000/chat \ -H "Content-Type: application/json" \ -d '{"model":"model-a","messages":[{"role":"user","content":"只返回 OK"}]}'
成功响应的结构类似:
{ "model": "model-a", "text": "OK", "usage": { "prompt_tokens": 12, "completion_tokens": 1, "total_tokens": 13 }}
如果使用 Windows PowerShell,可以写成单行命令,避免不同 Shell 对换行符和引号的处理差异:
curl.exe -X POST http://127.0.0.1:8000/chat -H "Content-Type: application/json" -d "{\"model\":\"model-a\",\"messages\":[{\"role\":\"user\",\"content\":\"只返回 OK\"}]}"
六、常见错误怎么处理
400 | 请求参数或模型不在白名单 | JSON 结构、模型名称和允许列表 |
401 | 鉴权失败 | 环境变量中的密钥和服务端权限 |
404 | 地址或模型路径不正确 | MODEL_BASE_URL 是否重复拼接版本路径 |
429 | 上游限流 | 请求频率、并发数和重试策略 |
502 | 上游连接失败 | 服务地址、网络和 DNS |
504 | 上游超时 | 超时设置、请求复杂度和上游状态 |
不要把所有错误都简单重试。400 和 401 通常需要修改配置;429 可以按服务端约定退避;502 和 504 是否重试,要结合请求是否幂等以及业务能否接受重复执行来决定。
七、为生产环境补齐的检查项
密钥管理。 使用部署平台的 Secret 或专用密钥服务,不把 .env 和真实密钥提交到仓库;不同应用使用不同密钥,便于撤销和定位。
请求边界。 限制消息长度、请求体大小和单次超时,避免一条请求长期占用资源。
模型策略。 模型白名单和默认模型放在配置中,模型切换要有版本记录,不要允许用户直接传入任意上游地址。
日志脱敏。 记录请求 ID、模型、耗时、状态码和用量摘要,不记录完整密钥,也不要默认记录用户原始提示词。
重试与降级。 只对明确的瞬时错误重试,并设置最大次数和退避时间。需要备用模型时,应记录降级原因,避免错误被静默吞掉。
监控和测试。 为成功、鉴权失败、模型不存在、限流、超时和上游连接失败分别写测试;上线前用一个低风险模型验证完整链路。
八、可以怎样继续扩展
这个版本只处理普通非流式响应。下一步可以增加流式输出、请求 ID、结构化日志和备用模型,但每项扩展都要先明确返回协议和失败语义。若接入工具调用或多模态输入,还要为消息类型和响应字段增加独立的数据模型,不能继续假设所有请求都是简单文本。
小结
把模型调用封装成一个小型 FastAPI 服务,重点不在于增加一层代码,而在于把配置、模型范围、超时和错误处理集中起来。先用环境变量和白名单建立边界,再用统一 JSON 和 curl 验证成功、失败和超时路径,最后补齐密钥、日志、重试和监控,才能从“能调用”走向“可维护”。