ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

构建API适配层:解决本地大模型与工具调用框架的协议兼容性问题

构建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(一个包含rolecontent的对象数组),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 } }

关键差异点:

  1. promptvsmessages:Ollama的/api/generate端点主要使用prompt字段接收单个字符串提示。虽然较新版本也支持/api/chat端点和messages格式,但Codex默认可能调用的是/v1/chat/completions这样的兼容端点,而Ollama原生并不提供完全一致的路径。
  2. 参数位置:像temperaturemax_tokens这样的参数,在OpenAI格式中是顶级字段,而在Ollama中,它们被嵌套在options对象里,并且max_tokens对应的是num_predict
  3. 模型名称(model):Codex传递的可能是gpt-3.5-turbo这样的抽象名,而Ollama需要的是具体的模型标签,如llama3:8bqwen2: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 }

差异点:

  1. 结构嵌套:OpenAI格式的结果放在choices数组里,而Ollama直接返回message
  2. 字段名createdvscreated_at,finish_reasonvsdone(虽然语义不同)。
  3. 必选字段: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 pydantic

httpx是一个现代化的HTTP客户端库,支持异步,我们将用它来转发请求到后端的Ollama服务。

项目核心文件结构如下:

codex_ollama_proxy/ ├── main.py # FastAPI应用主入口 ├── config.py # 配置文件(模型映射、Ollama地址等) ├── translator.py # 核心的请求/响应翻译逻辑 ├── models.py # Pydantic数据模型定义 └── requirements.txt

3.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(default=0.7, ge=0, le=2) max_tokens: Optional[int] = Field(default=None, gt=0) # 其他可能字段如 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_url=config.OLLAMA_BASE_URL, timeout=30.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(role=role, content=msg.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( model=ollama_model, messages=ollama_messages, stream=openai_req.stream, options=options 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 = f"chatcmpl-{uuid.uuid4().hex[:16]}" # 使用当前时间戳 created = int(time.time()) # 构造choices数组 choice = OpenAIChatCompletionChoice( index=0, message=OpenAIChatMessage(role="assistant", content=message_content), finish_reason="stop" 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_tokens=prompt_tokens_est, completion_tokens=completion_tokens_est, total_tokens=prompt_tokens_est + completion_tokens_est ) openai_resp = OpenAIChatCompletionResponse( id=response_id, created=created, model=openai_req_model, # 返回Codex请求的原始模型名,保持一致性 choices=[choice], usage=usage ) return openai_resp async def translate_stream_response(self, ollama_stream_lines: AsyncGenerator[bytes, None]) -> AsyncGenerator[str, None]: """将Ollama的流式响应翻译为OpenAI流式格式""" response_id = f"chatcmpl-{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 f"data: {json.dumps(openai_chunk)}\n\n" if done: yield "data: [DONE]\n\n" break except json.JSONDecodeError: # 忽略非JSON行(如可能的错误信息) continue except Exception as e: # 发生错误时,发送一个错误chunk(OpenAI风格)并结束 error_chunk = { "error": { "message": f"Stream decoding error: {str(e)}", "type": "internal_error" } } yield f"data: {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格式,包括生成虚拟的idusage等字段。
  • translate_stream_response: 这是一个异步生成器,实时转换流式数据。它逐行读取Ollama的流,实时包装成OpenAI的SSE格式并yield出去,这是实现流畅体验的关键。

注意:这里的usage(token计数)是估算的,并不精确。对于严格要求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(title="Codex-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_none=True) # 4. 发起请求到Ollama async with httpx.AsyncClient() as client: if openai_req.stream: # 流式响应 async with client.stream( "POST", f"{config.OLLAMA_BASE_URL}{ollama_endpoint}", json=ollama_payload, timeout=30.0 ) as ollama_response: if ollama_response.status_code != 200: error_text = await ollama_response.aread() raise HTTPException(status_code=ollama_response.status_code, detail=error_text.decode()) # 创建流式响应转换 async def generate(): async for chunk in translator.translate_stream_response(ollama_response.aiter_lines()): yield chunk return StreamingResponse( generate(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", } ) else: # 非流式响应 ollama_response = await client.post( f"{config.OLLAMA_BASE_URL}{ollama_endpoint}", json=ollama_payload, timeout=30.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_code=400, detail="Invalid 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_code=e.response.status_code, detail={ "error": { "message": message, "type": "invalid_request_error", "param": None, "code": None } }) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.on_event("shutdown") async def shutdown_event(): await translator.close() if __name__ == "__main__": import uvicorn uvicorn.run(app, host=config.PROXY_HOST, port=config.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.pymain.py中将端点改为/api/generate,并重写请求转换逻辑,将messages数组拼接成一个单独的prompt字符串。这会复杂很多,因为你需要处理systemuserassistant消息的拼接格式(例如,使用[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_BASE=http://localhost:8000/v1 export OPENAI_API_KEY=dummy-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,连接会失败。有两种解决方案:

  1. 使用反向代理:在代理前面套一层Nginx或Caddy,配置SSL证书,将HTTPS流量代理到本地的HTTP服务。这是生产环境的标准做法。
  2. 修改Codex配置:如果Codex允许,将其配置为使用HTTP而非HTTPS(仅限本地开发环境)。

对于本地开发,我通常用ngroklocalhost.run快速创建一个临时的HTTPS隧道,但这会引入网络延迟。更简单的方法是直接修改Codex客户端的配置,允许不安全的HTTP连接(如果它有这个选项的话)。

4.3 流式响应与超时处理

流式模式(stream: true)下,连接会保持打开直到生成结束。这里有两个关键点:

  1. 超时设置:确保你的代理(httpx)和Ollama服务都有足够长的超时时间。大模型生成几百个token可能需要几十秒。我在代码中设置了30秒超时,对于长文本可能不够,你可以根据需要调整。
  2. 连接保持:代理服务器需要正确设置响应头(如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.pytranslate_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-turbogpt-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_retries=3): for attempt in range(max_retries): try: response = await client.request(method, url, json=json_data, timeout=30.0) response.raise_for_status() return response except (TimeoutException, ConnectError) as e: if attempt == max_retries - 1: raise wait_time = 2 ** attempt # 指数退避 print(f"Request failed ({e}), retrying in {wait_time}s...") await asyncio.sleep(wait_time)

main.py的请求转发部分,可以调用这个带重试的函数。

5.3 支持多模型与动态加载

目前的模型映射是硬编码在配置里的。你可以将其扩展为从数据库或配置文件动态加载。甚至可以实现一个“模型路由”功能:根据请求的某些特征(如内容长度、语言),自动选择最合适的本地模型来响应。

5.4 添加监控与日志

在生产环境中,你需要知道代理的运行状态。集成像PrometheusGrafana这样的监控工具来收集请求量、延迟、错误率等指标。同时,结构化日志(使用structlogjson-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(name=API_KEY_NAME, auto_error=False) async def verify_api_key(api_key: str = Security(api_key_header)): if not api_key: raise HTTPException(status_code=403, detail="API key missing") # 这里可以验证api_key是否在你的合法密钥列表中 if api_key != "your-secure-token": raise HTTPException(status_code=403, detail="Invalid 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应用的可能性。

返回列表