公司动态
构建API适配层:解决本地大模型与工具调用框架的协议兼容性问题
1. 项目缘起当本地大模型遇上“不听话”的API最近在折腾一个挺有意思的项目我想让本地跑的大模型比如用Ollama部署的Llama 3或者Qwen能够无缝接入Codex这个工具调用框架。Codex本身设计得挺好能帮大模型规划和执行一系列工具比如搜索、计算、文件操作但问题来了——它的API设计和我本地模型服务比如Ollama的API完全是两套“语言”。这感觉就像你请了个精通中文的管家本地模型想让他去操作一套全英文的控制面板Codex。管家能力很强但看不懂面板上的指令直接对接肯定乱套。最常见的报错就是各种“API Error: 400”内容五花八门比如‘type’ must be in [“enabled”, “disabled”, “auto”]或者是This model‘s maximum context length is...再狠一点直接给你来个Connection closed mid-response。这些错误信息本质上就是两边API的请求格式、响应结构、参数命名对不上号互相“听不懂”对方在说什么。我的目标很明确在这两者之间做一个“翻译官”。这个翻译官需要准确理解Codex发出的“指令”API请求将其转换成我的本地模型服务能听懂的“方言”再把本地模型的“回答”API响应翻译回Codex能理解的格式。整个过程要稳定、高效并且最好能处理各种边界情况和错误。这不仅仅是简单的参数映射还涉及到流式响应处理、错误码转换、上下文长度适配等一系列细节。下面我就把自己趟出来的路以及路上踩过的坑完整地分享出来。2. 核心矛盾拆解Codex API 与 Ollama API 的“语言”差异要实现翻译首先得搞清楚两边到底在说什么以及为什么直接对话会失败。我以最典型的Ollama本地服务作为“本地模型”的代表与Codex的预期API进行对比。2.1 请求体Request Body的结构性冲突这是最根本的差异。Codex在调用一个模型时它发出的请求格式通常遵循OpenAI API的兼容格式因为这是目前事实上的标准。而Ollama的API虽然也尽力向OpenAI靠拢但在细节上存在不少出入。Codex期望的OpenAI格式示例{ model: gpt-3.5-turbo, messages: [ {role: system, content: You are a helpful assistant.}, {role: user, content: Hello!} ], stream: false, temperature: 0.7, max_tokens: 500 }关键字段model,messages(一个包含role和content的对象数组),stream,temperature,max_tokens。Ollama实际接收的格式示例{ model: llama3:8b, prompt: Hello!, stream: false, options: { temperature: 0.7, num_predict: 500 } }或者对于聊天模式{ model: qwen2:7b, messages: [ {role: user, content: Hello!} ], stream: false, options: { temperature: 0.7, num_predict: 500 } }关键差异点promptvsmessagesOllama的/api/generate端点主要使用prompt字段接收单个字符串提示。虽然较新版本也支持/api/chat端点和messages格式但Codex默认可能调用的是/v1/chat/completions这样的兼容端点而Ollama原生并不提供完全一致的路径。参数位置像temperature、max_tokens这样的参数在OpenAI格式中是顶级字段而在Ollama中它们被嵌套在options对象里并且max_tokens对应的是num_predict。模型名称modelCodex传递的可能是gpt-3.5-turbo这样的抽象名而Ollama需要的是具体的模型标签如llama3:8b、qwen2:7b。直接对接时Ollama服务收到Codex格式的请求会因为找不到预期的字段比如options或无法理解字段值比如max_tokens而返回400错误提示类似‘type’ must be in [“enabled”, “disabled”, “auto”]这种让人摸不着头脑的信息这可能是Ollama内部校验某个未知字段时产生的泛化错误。2.2 响应体Response Body的格式错位即使请求转换对了回来的响应也可能对不上。Codex期望的响应格式是OpenAI式的。Codex期望的响应格式OpenAI ChatCompletion{ id: chatcmpl-abc123, object: chat.completion, created: 1677858242, model: gpt-3.5-turbo, choices: [{ index: 0, message: { role: assistant, content: Hello there! How can I assist you today? }, finish_reason: stop }], usage: { prompt_tokens: 9, completion_tokens: 12, total_tokens: 21 } }Ollama实际返回的响应格式/api/chat{ model: qwen2:7b, created_at: 2024-01-01T00:00:00.000Z, message: { role: assistant, content: Hello there! How can I assist you today? }, done: true }差异点结构嵌套OpenAI格式的结果放在choices数组里而Ollama直接返回message。字段名createdvscreated_at,finish_reasonvsdone虽然语义不同。必选字段Codex可能严格要求id,object,usage等字段存在而Ollama没有这些。如果Codex收到Ollama的原生响应它无法正确解析出choices[0].message.content就会认为调用失败或者得到空结果。2.3 流式响应Streaming的处理分歧对于需要实时显示生成过程的场景流式响应stream: true至关重要。两者的流式格式也大相径庭。OpenAI流式响应格式每行是一个独立的JSON对象以data:前缀开头最后以data: [DONE]结束。data: {id:...,object:chat.completion.chunk,choices:[{delta:{content:Hello}}]} data: {id:...,object:chat.completion.chunk,choices:[{delta:{content: there}}]} data: [DONE]Ollama流式响应格式/api/chat每行是一个完整的JSON对象没有data:前缀通过done: false表示进行中done: true表示结束。{model:qwen2:7b,created_at:...,message:{role:assistant,content:Hello},done:false} {model:qwen2:7b,created_at:...,message:{role:assistant,content: there},done:false} {model:qwen2:7b,created_at:...,message:{role:assistant,content:!},done:true}如果不做转换Codex的流式解析器会完全无法识别Ollama的数据格式导致连接中断或显示异常。2.4 错误处理与上下文长度错误信息格式不匹配是另一个头疼的问题。Ollama返回的错误可能结构简单而Codex期望的是OpenAI格式的错误对象。此外上下文长度限制是高频报错点。Codex传递的max_tokens参数和模型自身的上下文窗口可能产生冲突。例如Ollama模型llama3:8b的上下文长度是8192如果Codex请求中max_tokens设置得过大或者历史消息累计token数超限Ollama可能返回类似maximum context length is 8192 tokens的错误但这个错误信息需要被“翻译”成Codex能理解的格式并传递回去而不是直接导致整个代理崩溃。3. 构建API翻译层从设计到实现理解了问题解决方案就清晰了我们需要一个中间层代理服务器它接收Codex的请求进行翻译和转发再将Ollama的响应翻译回去。我选择用Python的FastAPI来搭建这个代理因为它轻量、异步支持好适合处理HTTP代理任务。3.1 项目结构与核心依赖首先初始化项目环境。# 创建项目目录 mkdir codex_ollama_proxy cd codex_ollama_proxy python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows # 安装核心依赖 pip install fastapi uvicorn httpx pydantichttpx是一个现代化的HTTP客户端库支持异步我们将用它来转发请求到后端的Ollama服务。项目核心文件结构如下codex_ollama_proxy/ ├── main.py # FastAPI应用主入口 ├── config.py # 配置文件模型映射、Ollama地址等 ├── translator.py # 核心的请求/响应翻译逻辑 ├── models.py # Pydantic数据模型定义 └── requirements.txt3.2 定义数据模型models.py使用Pydantic严格定义输入输出格式这能帮我们做好数据验证和自动文档生成。from pydantic import BaseModel, Field from typing import List, Optional, Union, Literal # OpenAI / Codex 兼容的请求格式 class OpenAIChatMessage(BaseModel): role: Literal[system, user, assistant, function] content: str name: Optional[str] None class OpenAIChatCompletionRequest(BaseModel): model: str messages: List[OpenAIChatMessage] stream: Optional[bool] False temperature: Optional[float] Field(default0.7, ge0, le2) max_tokens: Optional[int] Field(defaultNone, gt0) # 其他可能字段如 top_p, frequency_penalty 等可以按需添加 # OpenAI / Codex 兼容的响应格式非流式 class OpenAIChatCompletionChoice(BaseModel): index: int message: OpenAIChatMessage finish_reason: Optional[str] None class OpenAIUsage(BaseModel): prompt_tokens: int completion_tokens: int total_tokens: int class OpenAIChatCompletionResponse(BaseModel): id: str object: str chat.completion created: int model: str choices: List[OpenAIChatCompletionChoice] usage: OpenAIUsage # Ollama 的请求/响应格式简化 class OllamaMessage(BaseModel): role: str content: str class OllamaChatRequest(BaseModel): model: str messages: List[OllamaMessage] stream: Optional[bool] False options: Optional[dict] {} # 存放 temperature, num_predict 等 class OllamaChatResponse(BaseModel): model: str created_at: str message: OllamaMessage done: bool # 流式响应中done为false时message.content是增量内容定义这些模型看似繁琐但至关重要。它能确保进入我们代理的数据是“干净”的也让我们在代码里能有清晰的类型提示。3.3 配置与映射config.py我们需要一个地方来管理配置比如Ollama服务的地址以及最重要的——模型名称映射。import os from typing import Dict class Config: # Ollama服务地址默认本地11434端口 OLLAMA_BASE_URL os.getenv(OLLAMA_BASE_URL, http://localhost:11434) # 模型映射表Codex请求中的model名 - Ollama实际的model tag # 这是翻译的关键之一Codex发来“gpt-3.5-turbo”我们将其转为“llama3:8b” MODEL_MAPPING: Dict[str, str] { gpt-3.5-turbo: llama3:8b, gpt-4: qwen2:7b, claude-3-haiku: mistral:7b, # 可以添加更多映射 } # 默认模型如果请求的模型不在映射表中则使用此默认 DEFAULT_OLLAMA_MODEL llama3:8b # 代理服务器自身的主机和端口 PROXY_HOST 0.0.0.0 PROXY_PORT 8000 config Config()这个映射表是解决model字段不匹配的核心。你可以根据自己本地部署的模型灵活配置。3.4 核心翻译器translator.py这是整个项目的“大脑”负责格式转换的所有细节逻辑。import json import time import uuid from typing import AsyncGenerator, Dict, Any import httpx from .models import ( OpenAIChatCompletionRequest, OpenAIChatCompletionResponse, OpenAIChatCompletionChoice, OpenAIChatMessage, OpenAIUsage, OllamaChatRequest, OllamaMessage, ) from .config import config class APITranslator: def __init__(self): self.client httpx.AsyncClient(base_urlconfig.OLLAMA_BASE_URL, timeout30.0) async def translate_request(self, openai_req: OpenAIChatCompletionRequest) - OllamaChatRequest: 将OpenAI格式请求翻译为Ollama格式请求 # 1. 模型名称映射 requested_model openai_req.model ollama_model config.MODEL_MAPPING.get(requested_model, config.DEFAULT_OLLAMA_MODEL) # 2. 消息格式转换角色类型可能需微调Ollama通常支持system/user/assistant ollama_messages [] for msg in openai_req.messages: # Ollama的role一般是字符串直接使用。确保没有不支持的role。 if msg.role not in [system, user, assistant]: # 如果不支持可以降级处理比如function-assistant role assistant else: role msg.role ollama_messages.append(OllamaMessage(rolerole, contentmsg.content)) # 3. 参数转换将顶级参数放入options字典 options {} if openai_req.temperature is not None: options[temperature] openai_req.temperature if openai_req.max_tokens is not None: # OpenAI的max_tokens对应Ollama的num_predict options[num_predict] openai_req.max_tokens # 4. 构建Ollama请求 ollama_req OllamaChatRequest( modelollama_model, messagesollama_messages, streamopenai_req.stream, optionsoptions if options else None # Ollama允许options为null ) return ollama_req async def translate_response(self, ollama_resp_dict: Dict[str, Any], openai_req_model: str) - OpenAIChatCompletionResponse: 将Ollama的非流式响应翻译为OpenAI格式响应 # 注意ollama_resp_dict是Ollama API返回的原始字典 message_content ollama_resp_dict.get(message, {}).get(content, ) # 构建OpenAI格式的响应 # 生成一个唯一的ID response_id fchatcmpl-{uuid.uuid4().hex[:16]} # 使用当前时间戳 created int(time.time()) # 构造choices数组 choice OpenAIChatCompletionChoice( index0, messageOpenAIChatMessage(roleassistant, contentmessage_content), finish_reasonstop if ollama_resp_dict.get(done, True) else None ) # 估算token使用情况这是一个简化版生产环境应用更准确的tokenizer # 这里假设一个粗略的估算1个token约等于4个英文字符或2个中文字符 prompt_text .join([msg.content for msg in self._last_openai_request.messages]) if hasattr(self, _last_openai_request) else completion_text message_content prompt_tokens_est len(prompt_text) // 4 completion_tokens_est len(completion_text) // 4 usage OpenAIUsage( prompt_tokensprompt_tokens_est, completion_tokenscompletion_tokens_est, total_tokensprompt_tokens_est completion_tokens_est ) openai_resp OpenAIChatCompletionResponse( idresponse_id, createdcreated, modelopenai_req_model, # 返回Codex请求的原始模型名保持一致性 choices[choice], usageusage ) return openai_resp async def translate_stream_response(self, ollama_stream_lines: AsyncGenerator[bytes, None]) - AsyncGenerator[str, None]: 将Ollama的流式响应翻译为OpenAI流式格式 response_id fchatcmpl-{uuid.uuid4().hex[:16]} created int(time.time()) model_name gpt-3.5-turbo # 可以从前文获取这里简化 async for line in ollama_stream_lines: if not line: continue try: # Ollama流式响应每行是一个JSON line_str line.decode(utf-8).strip() if not line_str: continue ollama_chunk json.loads(line_str) chunk_content ollama_chunk.get(message, {}).get(content, ) done ollama_chunk.get(done, False) # 构建OpenAI格式的流式chunk openai_chunk { id: response_id, object: chat.completion.chunk, created: created, model: model_name, choices: [ { index: 0, delta: {content: chunk_content} if chunk_content else {}, finish_reason: stop if done else None } ] } # 以SSE (Server-Sent Events) 格式输出 yield fdata: {json.dumps(openai_chunk)}\n\n if done: yield data: [DONE]\n\n break except json.JSONDecodeError: # 忽略非JSON行如可能的错误信息 continue except Exception as e: # 发生错误时发送一个错误chunkOpenAI风格并结束 error_chunk { error: { message: fStream decoding error: {str(e)}, type: internal_error } } yield fdata: {json.dumps(error_chunk)}\n\n yield data: [DONE]\n\n break async def close(self): await self.client.aclose()这个APITranslator类完成了最繁重的工作translate_request: 处理模型映射、消息转换、参数搬家从顶级移到options。translate_response: 将Ollama的单次响应包装成OpenAI格式包括生成虚拟的id、usage等字段。translate_stream_response: 这是一个异步生成器实时转换流式数据。它逐行读取Ollama的流实时包装成OpenAI的SSE格式并yield出去这是实现流畅体验的关键。注意这里的usagetoken计数是估算的并不精确。对于严格要求token计费的场景你需要集成一个tokenizer如tiktoken用于OpenAI模型或transformers库用于本地模型来进行准确计算。不过对于让Codex能工作起来的基本需求估算值通常足够。3.5 代理服务器主入口main.py最后我们用FastAPI把这一切粘合起来创建一个代理端点。from fastapi import FastAPI, Request, HTTPException from fastapi.responses import StreamingResponse import json from .translator import APITranslator from .models import OpenAIChatCompletionRequest from .config import config app FastAPI(titleCodex-Ollama API Translator) translator APITranslator() app.post(/v1/chat/completions) async def chat_completions(request: Request): 核心代理端点。接收OpenAI格式请求转发给Ollama并返回OpenAI格式响应。 try: # 1. 解析请求 body await request.json() openai_req OpenAIChatCompletionRequest(**body) # 存储请求用于可能的后续处理如估算usage translator._last_openai_request openai_req # 2. 翻译请求格式 ollama_req await translator.translate_request(openai_req) # 3. 确定Ollama的端点聊天或生成 # 根据消息中是否有system角色或最新Ollama版本特性决定使用 /api/chat 还是 /api/generate # 这里假设使用 /api/chat因为它更接近OpenAI的messages格式 ollama_endpoint /api/chat ollama_payload ollama_req.dict(exclude_noneTrue) # 4. 发起请求到Ollama async with httpx.AsyncClient() as client: if openai_req.stream: # 流式响应 async with client.stream( POST, f{config.OLLAMA_BASE_URL}{ollama_endpoint}, jsonollama_payload, timeout30.0 ) as ollama_response: if ollama_response.status_code ! 200: error_text await ollama_response.aread() raise HTTPException(status_codeollama_response.status_code, detailerror_text.decode()) # 创建流式响应转换 async def generate(): async for chunk in translator.translate_stream_response(ollama_response.aiter_lines()): yield chunk return StreamingResponse( generate(), media_typetext/event-stream, headers{ Cache-Control: no-cache, Connection: keep-alive, } ) else: # 非流式响应 ollama_response await client.post( f{config.OLLAMA_BASE_URL}{ollama_endpoint}, jsonollama_payload, timeout30.0 ) ollama_response.raise_for_status() ollama_data ollama_response.json() # 5. 翻译响应格式 openai_resp await translator.translate_response(ollama_data, openai_req.model) return openai_resp.dict() except json.JSONDecodeError: raise HTTPException(status_code400, detailInvalid JSON in request body) except httpx.HTTPStatusError as e: # 将Ollama的错误信息“翻译”并返回 error_detail e.response.text if e.response else str(e) # 尝试解析Ollama的错误封装成OpenAI错误格式 try: error_json json.loads(error_detail) message error_json.get(error, error_detail) except: message error_detail raise HTTPException(status_codee.response.status_code, detail{ error: { message: message, type: invalid_request_error, param: None, code: None } }) except Exception as e: raise HTTPException(status_code500, detailstr(e)) app.on_event(shutdown) async def shutdown_event(): await translator.close() if __name__ __main__: import uvicorn uvicorn.run(app, hostconfig.PROXY_HOST, portconfig.PROXY_PORT)这个FastAPI应用创建了一个/v1/chat/completions端点它完美模仿了OpenAI的聊天补全API。Codex会向这个地址发送请求而代理会完成所有的翻译和转发工作。4. 部署、配置与实战踩坑记录代码写完了但让它跑起来并稳定工作才是真正的挑战。下面是我在部署和调试过程中总结的关键步骤和遇到的坑。4.1 环境准备与Ollama侧配置首先确保Ollama服务已经正确安装并在运行。# 拉取并运行一个模型例如Llama 3 8B ollama pull llama3:8b ollama run llama3:8b # 在另一个终端测试Ollama API是否正常 curl http://localhost:11434/api/tags你应该能看到一个包含llama3:8b的JSON响应。坑点一Ollama版本与API端点兼容性早期版本的Ollama可能只提供/api/generate端点它主要接收prompt字符串。而我们的代理默认使用了更现代的/api/chat端点需要Ollama版本0.1.15左右。如果你遇到404错误请先检查你的Ollama版本并确认/api/chat端点是否存在。如果不存在你需要在translator.py和main.py中将端点改为/api/generate并重写请求转换逻辑将messages数组拼接成一个单独的prompt字符串。这会复杂很多因为你需要处理system、user、assistant消息的拼接格式例如使用[INST]、SYS等模型特定的模板。因此强烈建议升级Ollama到最新稳定版。4.2 启动代理并配置Codex启动我们的API翻译代理cd codex_ollama_proxy source venv/bin/activate python main.py服务将在http://localhost:8000启动。现在我们需要告诉Codex它的“OpenAI API”地址在这里。如何配置Codex的API端点取决于Codex的具体实现。通常Codex会有一个配置文件或环境变量来设置OpenAI API的Base URL。例如你可能需要设置export OPENAI_API_BASEhttp://localhost:8000/v1 export OPENAI_API_KEYdummy-key # 由于是本地代理API Key可以任意填写但代理层可以忽略或验证然后启动Codex。Codex的所有对https://api.openai.com/v1/chat/completions的请求都会被重定向到你的本地代理http://localhost:8000/v1/chat/completions。坑点二SSL证书与HTTP/HTTPS问题如果你的Codex强制要求HTTPS比如某些Web前端而你的代理是HTTP连接会失败。有两种解决方案使用反向代理在代理前面套一层Nginx或Caddy配置SSL证书将HTTPS流量代理到本地的HTTP服务。这是生产环境的标准做法。修改Codex配置如果Codex允许将其配置为使用HTTP而非HTTPS仅限本地开发环境。对于本地开发我通常用ngrok或localhost.run快速创建一个临时的HTTPS隧道但这会引入网络延迟。更简单的方法是直接修改Codex客户端的配置允许不安全的HTTP连接如果它有这个选项的话。4.3 流式响应与超时处理流式模式stream: true下连接会保持打开直到生成结束。这里有两个关键点超时设置确保你的代理httpx和Ollama服务都有足够长的超时时间。大模型生成几百个token可能需要几十秒。我在代码中设置了30秒超时对于长文本可能不够你可以根据需要调整。连接保持代理服务器需要正确设置响应头如Cache-Control: no-cache并确保在流式传输过程中不提前关闭连接。我们的StreamingResponse配合异步生成器通常能很好地处理这一点。坑点三网络抖动与连接中断在流式传输过程中如果网络不稳定或者Ollama服务本身崩溃会导致连接意外中断Codex前端可能会收到不完整的响应或直接报错Connection closed mid-response。我们的代理需要在translate_stream_response方法中做好异常捕获并尝试发送一个格式正确的错误chunk给Codex让前端能优雅地显示错误而不是直接崩溃。代码中已经包含了一个简单的错误处理块。4.4 上下文长度与Token估算的陷阱这是错误maximum context length is ... tokens的来源。我们的代理目前只是被动转发max_tokens参数。但更健壮的做法是进行主动校验。改进方案在translator.py的translate_request方法中加入上下文长度校验逻辑。# 在APITranslator类中添加一个模型上下文长度映射 MODEL_CONTEXT_WINDOWS { llama3:8b: 8192, qwen2:7b: 32768, mistral:7b: 8192, } async def translate_request(self, openai_req): # ... 之前的映射代码 ... ollama_model config.MODEL_MAPPING.get(requested_model, config.DEFAULT_OLLAMA_MODEL) # 上下文长度校验简化版需要准确tokenizer max_context self.MODEL_CONTEXT_WINDOWS.get(ollama_model, 4096) # 默认值 if openai_req.max_tokens and openai_req.max_tokens max_context: # 可以调整max_tokens或者直接抛出错误 openai_req.max_tokens min(openai_req.max_tokens, max_context) # 更佳实践计算消息历史的大致token数与max_tokens相加后与max_context比较 # ... 后续转换代码 ...最准确的做法是集成一个tokenizer在代理层计算整个messages的token数量如果超过模型上限则提前返回一个清晰的错误给Codex而不是让Ollama返回一个可能格式混乱的错误。4.5 处理其他API端点的代理Codex可能不止调用/chat/completions还可能调用/models来列出可用模型或者调用/embeddings。为了让代理更完整我们可以添加这些端点的模拟。app.get(/v1/models) async def list_models(): 返回一个模拟的模型列表让Codex知道有哪些模型可用 # 这里返回我们在MODEL_MAPPING中定义的“虚拟”模型名 models_list { object: list, data: [ { id: model_name, object: model, created: 1686935000, owned_by: local-ollama } for model_name in config.MODEL_MAPPING.keys() ] } return models_list这样当Codex查询可用模型时它会看到gpt-3.5-turbo、gpt-4等而实际上背后对应的是你本地的模型。5. 进阶优化与扩展思路一个基础可用的翻译层已经搭建完成。但要投入生产环境或追求更好体验还有不少优化点。5.1 性能优化连接池与请求合并频繁创建销毁HTTP连接开销很大。我们已经在APITranslator的__init__中为每个工作进程创建了一个httpx.AsyncClient实例作为连接池。在FastAPI的生产部署中例如使用uvicorn多worker每个worker进程都会有自己的连接池这能有效提升性能。对于高并发场景可以考虑引入请求队列或批处理但这会显著增加复杂性。对于本地工具调用场景通常并发不高当前的连接池模式已足够。5.2 增强错误处理与重试机制网络是不稳定的。我们应该为转发到Ollama的请求增加重试逻辑特别是针对网络超时、连接拒绝等临时性错误。import asyncio from httpx import TimeoutException, ConnectError async def make_request_with_retry(client, method, url, json_data, max_retries3): for attempt in range(max_retries): try: response await client.request(method, url, jsonjson_data, timeout30.0) response.raise_for_status() return response except (TimeoutException, ConnectError) as e: if attempt max_retries - 1: raise wait_time 2 ** attempt # 指数退避 print(fRequest failed ({e}), retrying in {wait_time}s...) await asyncio.sleep(wait_time)在main.py的请求转发部分可以调用这个带重试的函数。5.3 支持多模型与动态加载目前的模型映射是硬编码在配置里的。你可以将其扩展为从数据库或配置文件动态加载。甚至可以实现一个“模型路由”功能根据请求的某些特征如内容长度、语言自动选择最合适的本地模型来响应。5.4 添加监控与日志在生产环境中你需要知道代理的运行状态。集成像Prometheus和Grafana这样的监控工具来收集请求量、延迟、错误率等指标。同时结构化日志使用structlog或json-logging对于排查问题至关重要。记录下转换前后的请求/响应摘要注意不要记录包含敏感信息的完整消息以及任何错误信息。5.5 安全性考虑目前我们的代理对API Key是“放行”的使用dummy key。如果你需要将代理暴露给不可信的网络必须添加认证层。可以在FastAPI应用前加一个反向代理如Nginx进行基础认证或者在FastAPI中实现一个简单的API Key验证中间件。from fastapi import Security, Depends from fastapi.security import APIKeyHeader API_KEY_NAME Authorization api_key_header APIKeyHeader(nameAPI_KEY_NAME, auto_errorFalse) async def verify_api_key(api_key: str Security(api_key_header)): if not api_key: raise HTTPException(status_code403, detailAPI key missing) # 这里可以验证api_key是否在你的合法密钥列表中 if api_key ! your-secure-token: raise HTTPException(status_code403, detailInvalid API key) return api_key app.post(/v1/chat/completions) async def chat_completions(request: Request, _ Depends(verify_api_key)): # ... 原有逻辑 ...这样Codex在发送请求时就需要在Header中携带正确的Authorization密钥。经过以上步骤一个能够“翻译”Codex与Ollama之间API语言的代理就构建完成了。它不仅仅是一个简单的转发器而是一个处理了协议差异、错误转换、流式兼容的适配层。这个模式是通用的你可以用同样的思路去适配其他任何与OpenAI API不兼容的本地或远程模型服务比如通义千问、文心一言的API等让它们都能无缝接入Codex这样的工具调用框架极大地扩展了本地AI应用的可能性。