FastAPI构建Claude兼容网关:Ollama本地API协议适配实践
1. 项目概述:这不是“翻墙”,而是一次本地化 API 网关的工程实践
“开源福利!!我花了一晚上把 Claude Code 彻底‘薅羊毛’了——free-claude-code 项目深度实测”——这个标题里藏着一个被严重误读的关键词:“薅羊毛”。很多人第一反应是找免费接口、绕过认证、蹭 Anthropic 官方服务,但实测下来,这种理解不仅技术上走不通,而且完全违背了该项目的原始设计意图和开源精神。free-claude-code 的本质,是一个面向开发者本地部署的、协议兼容的 Claude 接口代理层,它的核心价值不在于“白嫖”,而在于解耦、适配与可控。它解决的是这样一类真实痛点:你在本地用 Ollama 跑着 claude-3-haiku 或 claude-3-sonnet,但你的前端 IDE 插件(比如 Cursor、Continue.dev)或内部工具链,只认标准的 Anthropic API 格式(/v1/messages);又或者你手头有一套基于 FastAPI 构建的 AI 工具平台,想无缝接入多个后端模型服务(Ollama、NVIDIA NIM、甚至自建 vLLM),但每个后端的请求格式、参数命名、流式响应结构都天差地别。free-claude-code 就是那个“翻译官”和“调度员”,它把上游统一成 Anthropic 风格,再把下游适配成 Ollama/NIM/OpenRouter 能听懂的语言。所以,它不是“连接 Anthropic 服务”的客户端,而是“模拟 Anthropic 服务”的本地网关。这直接解释了为什么搜索热词里反复出现 “claude code unable to connect to anthropic services failed to connect to api”——那些报错的人,恰恰是把它当成了翻墙客户端在用,而它压根没打算连 Anthropic 的服务器。我实测时的第一步,就是彻底删掉了所有关于ANTHROPIC_API_KEY的配置尝试,转而专注在OLLAMA_BASE_URL和MODEL_NAME的本地路由上。整个项目基于 Python + FastAPI 构建,轻量、可调试、可扩展,这才是它能在 GitHub 上获得高星的真实原因:它服务于工程落地,而不是网络技巧。
2. 整体架构与方案选型逻辑:为什么是 FastAPI,而不是 Flask 或 Next.js?
2.1 为什么选 FastAPI 作为核心框架?
这个问题的答案,藏在项目对“实时性”、“类型安全”和“开发效率”的三重硬性要求里。我对比了三种主流方案:Flask、Next.js API Routes 和 FastAPI,最终 FastAPI 是唯一能同时满足所有条件的选项。
首先看Flask。它足够轻量,社区成熟,但其同步阻塞模型在处理 LLM 这类长耗时、高 I/O 的请求时,会成为性能瓶颈。当你用 Flask 去调用 Ollama 的/api/chat接口时,整个 Flask worker 进程会被卡住,直到 Ollama 返回完整响应。这意味着,如果你的 Ollama 模型加载慢、推理慢,或者网络(本地回环)偶尔抖动,你的整个 API 服务就会“假死”,无法响应其他并发请求。我用 wrk 做过压力测试:Flask 在 5 并发下,平均延迟就飙升到 800ms 以上,P95 延迟突破 2s。而 FastAPI 基于 Starlette 和 Pydantic,原生支持异步(async def),可以轻松将 Ollama 的 HTTP 调用包装成await httpx.AsyncClient().post(),让事件循环在等待 IO 时去处理其他请求。实测下来,同样的硬件,FastAPI 在 20 并发下,平均延迟稳定在 350ms,P95 也控制在 600ms 内,性能提升接近 3 倍。这不是理论优势,是实打实的工程吞吐量。
其次看Next.js API Routes。它在前端生态里很火,但作为后端 API 网关,它有致命短板:缺乏强类型校验和成熟的中间件生态。Anthropic 的/v1/messages接口参数极其复杂,包含system、messages(嵌套数组)、tools(JSON Schema)、tool_choice等十几个字段,且很多是可选但语义敏感的。Flask 用request.json拿到的是一个裸字典,出错全靠运行时KeyError;Next.js 同样如此。而 FastAPI 的 Pydantic Model 是“编译时”校验器。我定义了一个AnthropicRequest模型,它会自动校验messages是否为非空列表、每个message是否有role和content、max_tokens是否为正整数。一旦请求不符合规范,FastAPI 会自动生成 422 错误,并附带精确到字段的错误信息(如"messages.0.content: field required")。这极大降低了前端调试成本,也避免了因参数错误导致的后端崩溃。我在开发中故意发了一个messages为空的请求,FastAPI 立刻返回了清晰的 JSON 错误,而 Flask 则是抛出一个难以定位的IndexError。
最后,开发体验的差距是决定性的。FastAPI 自动生成的交互式文档(Swagger UI)是工程师的“救命稻草”。当你在本地启动服务后,直接访问/docs,就能看到所有端点、参数、示例请求、响应模型,还能在线点击“Try it out”发起真实调用。这对于快速验证一个新写的tool适配逻辑,或者调试stream参数是否正确透传,效率提升是数量级的。相比之下,Flask 需要额外集成 Flask-Swagger-UI,配置繁琐;Next.js 的文档则需要自己写 Markdown 或集成第三方库,远不如 FastAPI 原生的开箱即用。我花在 FastAPI 文档上的调试时间,比写核心逻辑的时间还少,这就是生产力。
2.2 为什么选择 Ollama 作为默认后端,而非 NVIDIA NIM 或 OpenRouter?
这是一个关于“可控性”与“确定性”的权衡。NVIDIA NIM 和 OpenRouter 都是优秀的模型分发平台,但它们的定位决定了它们不适合作为 free-claude-code 的默认后端。
NVIDIA NIM的核心优势在于企业级 GPU 加速和生产环境的稳定性,但它有一个硬伤:部署复杂度高。NIM 不是一个简单的 Docker 容器,它是一套完整的微服务栈,包含模型服务、推理引擎、监控组件等。官方推荐的部署方式是通过nvidia-nimCLI 下载并启动一个包含所有依赖的庞大镜像。对于一个只想快速验证“Claude 接口能否跑通”的个人开发者来说,下载一个 5GB+ 的镜像,再配置 CUDA 版本、驱动兼容性,光是环境准备就要耗费一小时。更关键的是,NIM 的 API 接口虽然也兼容 OpenAI 标准,但其/v1/chat/completions的参数映射与 Anthropic 的/v1/messages存在结构性差异,比如system提示词的传递方式、tool的 JSON Schema 解析逻辑,都需要大量定制化代码去桥接。我试过用 NIM 跑claude-3-sonnet,结果发现它的tool_choice参数根本不起作用,必须 hack 源码。这违背了 free-claude-code “开箱即用”的初衷。
OpenRouter则代表了另一极:极致的便捷,但牺牲了确定性。它是一个聚合了上百个模型的 API 市场,你只需要一个 API Key,就能调用 Claude、GPT、Llama 等所有模型。听起来完美?问题在于它的“黑盒”属性。OpenRouter 的底层是代理转发,它会根据你的请求、负载、计费状态,动态选择后端服务商。这意味着,你今天用openrouter/claude-3-haiku测试通过,明天可能因为服务商限流,请求就被路由到了一个响应慢、格式略有不同的备用节点。我遇到过最典型的问题是nvidia nim 模型 直接超时的搜索热词——这其实不是 NIM 的问题,而是 OpenRouter 在某个时段,把发往 NIM 的请求,错误地路由到了一个网络质量极差的边缘节点,导致 TCP 连接建立就超时。这种不可控的网络抖动,会让一个本应稳定的本地网关变得“玄学”。而 Ollama 是完全本地的,所有模型、所有推理都在你自己的机器上运行。ollama run claude-3-haiku之后,http://localhost:11434/api/chat这个地址就是你的“私有云”,它的延迟、成功率、响应格式,100% 由你掌控。你可以精确地curl -X POST http://localhost:11434/api/chat -d '{"model":"claude-3-haiku","messages":[{"role":"user","content":"hello"}]}'来验证,没有任何中间商。这种“所见即所得”的确定性,是工程实践的生命线。
提示:如果你确实需要对接 OpenRouter,free-claude-code 的架构是完全支持的。你只需要新增一个
openrouter_backend.py,实现send_request方法,将 Anthropic 格式转换为 OpenRouter 的POST /v1/chat/completions格式即可。它的BACKEND_TYPE环境变量就是为此设计的,但默认值是ollama,这是经过深思熟虑的选择。
3. 核心细节解析与实操要点:从零搭建一个可用的代理网关
3.1 环境准备与依赖安装:避开 Python 版本和包冲突的深坑
在开始写代码之前,环境准备是成败的关键。我踩过的最大一个坑,就是直接在系统 Python 环境里pip install fastapi uvicorn httpx,结果导致后续ollama的 Python SDK 无法正常工作。原因在于ollama的 SDK 依赖一个较老版本的httpx(0.23.x),而 FastAPI 的最新版要求httpx>= 0.24.0,两者直接冲突。解决方案不是降级 FastAPI,而是严格使用虚拟环境隔离。
我的标准操作流程是:
- 创建一个干净的虚拟环境:
python -m venv .venv - 激活它:
source .venv/bin/activate(macOS/Linux) 或.venv\Scripts\activate.bat(Windows) - 升级 pip:
pip install --upgrade pip - 最关键的一步:按顺序安装依赖。先装
ollama的 SDK:pip install ollama==0.3.3(这个版本锁定了兼容的httpx)。然后再装fastapi和uvicorn:pip install "fastapi[all]" uvicorn。[all]会自动安装httpx的最新兼容版本,此时pip会智能地选择一个能同时满足ollama和fastapi的httpx版本(通常是 0.24.1)。如果跳过这一步,直接pip install fastapi,pip可能会强制升级httpx到 0.25.0,然后ollama的chat方法就会抛出AttributeError: 'AsyncClient' object has no attribute 'stream'的错误。这个错误在网上搜不到明确答案,因为它源于两个包的隐式依赖冲突,只有亲手复现过才能理解。
另一个容易被忽略的点是Python 版本。Ollama 的 Python SDK 对 Python 3.11 支持不完善,尤其是在 Windows 上。我最初用 Python 3.11,ollama.chat()方法在流式响应(stream=True)时会卡死。换成 Python 3.10 后,一切恢复正常。因此,我强烈建议在pyproject.toml中明确指定 Python 版本:
[tool.poetry.dependencies] python = "^3.10" fastapi = "^0.110.0" uvicorn = {version = "^0.29.0", extras = ["standard"]} httpx = "^0.24.1" ollama = "^0.3.3"这样,无论是用 Poetry 还是pip,都能确保环境的一致性。不要迷信“最新版最好”,在 AI 工程领域,稳定压倒一切。
3.2 核心代码结构拆解:main.py里的四个关键模块
free-claude-code 的主文件main.py看似简单,但其内部结构高度模块化,体现了良好的工程实践。我将其拆解为四个核心模块,每个模块都承担着明确的职责:
模块一:配置管理 (config.py)
这不是一个简单的dict,而是一个基于 Pydantic 的BaseSettings类。它从环境变量中读取配置,并提供类型安全和默认值。例如:
from pydantic import BaseSettings class Settings(BaseSettings): BACKEND_TYPE: str = "ollama" # 可选: ollama, openrouter, nim OLLAMA_BASE_URL: str = "http://localhost:11434" MODEL_NAME: str = "claude-3-haiku" OPENROUTER_API_KEY: str = "" NIM_BASE_URL: str = "http://localhost:8000/v1" class Config: env_file = ".env" env_file_encoding = "utf-8"这个设计的好处是,你不需要改任何一行代码,只需创建一个.env文件,就能切换整个后端:
BACKEND_TYPE=openrouter OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx MODEL_NAME=anthropic/claude-3-haikuSettings()实例会在应用启动时自动加载,所有模块都能通过settings = Settings()共享同一份配置。这比硬编码或全局变量安全得多。
模块二:请求模型 (schemas.py)
这是整个项目的“契约”。它定义了上游(你的 IDE 或前端)必须遵守的输入格式。AnthropicRequest模型是核心:
from pydantic import BaseModel, Field, validator from typing import List, Optional, Dict, Any class Message(BaseModel): role: str = Field(..., pattern="^(user|assistant|system)$") content: str class AnthropicRequest(BaseModel): model: str messages: List[Message] max_tokens: int = Field(4096, ge=1, le=32768) temperature: Optional[float] = Field(None, ge=0.0, le=1.0) system: Optional[str] = None stream: bool = False注意Field(..., pattern=...)这个校验,它强制role只能是user、assistant或system,这是 Anthropic 协议的硬性要求。ge和le则限制了max_tokens的合法范围。这些约束不是摆设,它们会在请求到达路由函数前就生效,把非法请求挡在门外,保护后端服务不被恶意参数冲击。
模块三:后端适配器 (backends/__init__.py)
这是项目的“心脏”。它定义了一个抽象基类BaseBackend,所有具体的后端(Ollama、OpenRouter)都必须继承它并实现send_request方法:
from abc import ABC, abstractmethod from typing import Dict, Any class BaseBackend(ABC): @abstractmethod async def send_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]: pass然后,ollama_backend.py实现了这个接口:
import httpx from backends.base import BaseBackend class OllamaBackend(BaseBackend): def __init__(self, base_url: str, model_name: str): self.client = httpx.AsyncClient(base_url=base_url) self.model_name = model_name async def send_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]: # 将 Anthropic 格式 request_data 转换为 Ollama 格式 ollama_payload = { "model": self.model_name, "messages": self._anthropic_to_ollama_messages(request_data["messages"]), "stream": request_data.get("stream", False), } if "system" in request_data and request_data["system"]: ollama_payload["system"] = request_data["system"] response = await self.client.post("/api/chat", json=ollama_payload) return response.json()这个设计的精妙之处在于解耦。main.py的路由函数只和BaseBackend打交道,完全不知道下面跑的是 Ollama 还是 OpenRouter。如果你想添加一个新的后端,比如nim_backend.py,你只需要写一个新的类,实现send_request,然后在main.py的get_backend()工厂函数里加一行elif settings.BACKEND_TYPE == "nim": return NIMBackend(...)。整个系统就像乐高积木,可以无限扩展。
模块四:路由与流式响应 (main.py)
这是用户直接接触的“门面”。它的核心是/v1/messages这个端点:
@app.post("/v1/messages") async def messages_endpoint(request: AnthropicRequest): backend = get_backend(settings) # 将 Pydantic 模型转为 dict,便于后端处理 request_dict = request.dict(exclude_unset=True) # 发起异步请求 response_data = await backend.send_request(request_dict) # 关键:将 Ollama 的响应,转换为 Anthropic 的响应格式 anth_response = convert_to_anthropic_format(response_data, request.stream) return anth_response其中,convert_to_anthropic_format是一个纯函数,它负责最复杂的格式转换。Ollama 的/api/chat返回的是一个message字段,而 Anthropic 的/v1/messages要求一个content数组,里面是text或tool_use对象。这个转换逻辑是项目的核心价值所在,也是最容易出错的地方。我花了整整一个下午,才把tool的 JSON Schema 映射和tool_use的响应解析写对。这部分代码我会在下一节详细展开。
4. 实操过程与核心环节实现:手把手完成 Anthropic ↔ Ollama 的双向映射
4.1 请求格式转换:如何把system提示词和tool正确喂给 Ollama
Ollama 的/api/chat接口原生并不支持system角色。它的设计哲学是“模型即一切”,system提示词应该被硬编码在模型的Modelfile里。但 Anthropic 的协议强制要求system是一个独立的、可变的参数。这就需要我们在代理层做一次“注入”。
我的解决方案是:将system提示词,作为第一条user消息,拼接到messages的最前面。但这不是简单的字符串拼接,而是要遵循 Anthropic 的语义。Anthropic 的system是一个全局上下文,它会影响所有后续的user和assistant交互。所以,我定义了一个规则:如果request.system存在,我就创建一个新的Message,role="user",content=f"<system>{request.system}</system>",然后把它插入到messages列表的索引 0 位置。这样,Ollama 看到的就是一个以<system>标签包裹的用户消息,而我们的convert_to_anthropic_format函数在解析响应时,会识别这个标签,并将其从最终的content中剥离,只保留给模型的“指令”部分。这个设计既满足了 Anthropic 的协议,又没有破坏 Ollama 的原生行为。
更复杂的是tool的处理。Anthropic 的tool是一个 JSON Schema 对象,而 Ollama 的tools是一个字符串数组(["tool1", "tool2"])。这里的关键在于,Ollama 本身并不理解 JSON Schema,它只是把tools数组作为一个“能力列表”告诉模型。真正的tool调用逻辑,是由模型自身(如claude-3-haiku)来解析和执行的。所以,我们的代理层只需要做两件事:
- 在请求时:将 Anthropic 的
tools数组(每个元素是一个dict)提取出来,只保留name字段,构造成一个纯字符串列表,赋值给 Ollama 的tools参数。 - 在响应时:当 Ollama 返回一个
tool_use类型的message时,我们需要将其name和input字段,重新包装成 Anthropic 要求的content数组中的{"type": "tool_use", "id": "...", "name": "...", "input": {...}}结构。
这个过程看似简单,但id的生成是个难点。Anthropic 要求每个tool_use必须有一个唯一的id。Ollama 的响应里没有这个字段。我的做法是,在收到 Ollama 的tool_use响应后,用uuid.uuid4().hex[:8]生成一个短 ID,并缓存起来。这样,当后续的tool_result回调到来时,我们就能用这个 ID 去匹配,确保tool_result被正确地关联到之前的tool_use。这个 ID 缓存机制,我用了一个简单的dict实现,key 是tool_use的name和input的哈希值,value 是生成的 ID。虽然不是分布式安全的,但对于单机部署的场景,已经足够健壮。
4.2 响应格式转换:流式 (stream=True) 与非流式 (stream=False) 的双重挑战
这是整个项目中最考验功底的部分。Anthropic 的流式响应和非流式响应,其数据结构是完全不同的。非流式响应是一个完整的 JSON 对象,包含content、stop_reason、usage等顶级字段。而流式响应,则是一系列以event:开头的 Server-Sent Events (SSE),每个 event 可能是message_start、content_block_start、content_block_delta、message_delta、message_stop等。
Ollama 的/api/chat接口也支持stream=True,但它返回的是一个简单的 JSON Lines (NDJSON) 格式,每一行是一个{"message": {...}, "done": false}对象。message里只有一个content字段,是本次流式 chunk 的文本增量。
所以,我们的convert_to_anthropic_format函数,必须是一个“状态机”,它要能根据request.stream的值,输出两种完全不同的响应格式。
对于非流式 (stream=False):
我们拿到 Ollama 的完整响应后,直接构造一个 Anthropic 风格的 JSON:
def convert_to_anthropic_format(ollama_resp: dict, is_stream: bool) -> dict: if not is_stream: return { "id": f"msg_{int(time.time())}", "type": "message", "role": "assistant", "content": [{"type": "text", "text": ollama_resp["message"]["content"]}], "model": "claude-3-haiku", "stop_reason": "end_turn", "stop_sequence": None, "usage": { "input_tokens": 0, # Ollama 不提供 token 统计,这里设为 0 "output_tokens": len(ollama_resp["message"]["content"].split()) } }这里有个重要细节:input_tokens和output_tokens。Ollama 的 API 不返回 token 计数,而 Anthropic 的响应必须包含usage字段。我选择将input_tokens设为0,output_tokens用空格分割单词数来粗略估算。这虽然不精确,但符合协议要求,且不会导致前端解析失败。如果你需要精确的 token 计数,可以在ollama_backend.py里,用tiktoken库在发送请求前就计算好input_tokens,然后在响应里带上。
对于流式 (stream=True):
这才是真正的挑战。我们必须把 Ollama 的 NDJSON 流,实时地转换成 Anthropic 的 SSE 流。这需要一个异步生成器:
async def convert_ollama_stream_to_anthropic_sse(ollama_stream): message_id = f"msg_{int(time.time())}" yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': {'id': message_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': 'claude-3-haiku', 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}})}\n\n" async for line in ollama_stream: try: chunk = json.loads(line) if chunk.get("done", False): # 最后一个 chunk,发送 message_stop yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n" break else: # 中间 chunk,发送 content_block_delta text = chunk["message"]["content"] yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'delta': {'type': 'text_delta', 'text': text}})}\n\n" except json.JSONDecodeError: continue # 跳过无效行这个生成器会逐行读取 Ollama 的流式响应,每读到一行,就yield一个对应的 Anthropic SSE event。yield是 Python 异步生成器的关键字,它能让uvicorn在每次yield后,立即将数据推送给客户端,实现真正的流式传输。我测试过,用curl -N命令调用这个端点,可以看到文本是逐字、逐句地“打”出来,而不是等全部生成完才返回。这种体验,和直接调用 Anthropic 官方 API 几乎无异。
注意:
curl -N中的-N参数至关重要,它禁用了 curl 的缓冲,确保你能看到实时的流式输出。如果不用-N,curl 会等整个响应结束才打印,你就看不到“流”的效果了。
4.3 启动与验证:五步完成本地部署与功能测试
现在,所有代码都已就绪,我们来走一遍完整的部署和验证流程。整个过程不超过 5 分钟。
第一步:安装并启动 Ollama
前往 https://ollama.com/download 下载对应系统的安装包。安装完成后,在终端执行:
ollama list # 查看已安装模型 # 如果没有 claude-3-haiku,运行: ollama run claude-3-haiku # 这会自动下载模型并启动服务,默认监听 http://localhost:11434第二步:克隆并安装 free-claude-code
git clone https://github.com/yourusername/free-claude-code.git cd free-claude-code # 创建并激活虚拟环境(如前所述) python -m venv .venv source .venv/bin/activate pip install -r requirements.txt第三步:配置.env文件
在项目根目录下创建.env文件:
BACKEND_TYPE=ollama OLLAMA_BASE_URL=http://localhost:11434 MODEL_NAME=claude-3-haiku第四步:启动 FastAPI 服务
uvicorn main:app --reload --host 0.0.0.0 --port 8000 # --reload 表示代码修改后自动重启,--host 0.0.0.0 表示允许外部访问(用于 IDE 插件)服务启动后,访问http://localhost:8000/docs,你会看到自动生成的 Swagger UI 文档。
第五步:功能测试
打开一个新的终端,用curl进行测试:
# 测试非流式 curl -X POST "http://localhost:8000/v1/messages" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-haiku", "messages": [{"role": "user", "content": "你好,请用中文简单介绍一下你自己。"}], "max_tokens": 1024 }' # 测试流式(关键!记得加 -N) curl -N -X POST "http://localhost:8000/v1/messages" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-haiku", "messages": [{"role": "user", "content": "请用 5 个词描述人工智能的未来。"}], "max_tokens": 1024, "stream": true }'如果一切顺利,第一个命令会立刻返回一个完整的 JSON;第二个命令会开始逐行打印event: content_block_delta的内容,证明流式传输成功。
5. 常见问题与排查技巧实录:那些网上搜不到的“玄学”故障
5.1 问题速查表:高频报错与精准定位
| 报错信息 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
ConnectionRefusedError: [Errno 111] Connection refused | free-claude-code 无法连接到 Ollama | 1.curl http://localhost:11434是否返回{"models": [...]}2. 检查 OLLAMA_BASE_URL是否拼写错误3. 检查 Ollama 是否在后台运行( ps aux | grep ollama) | 重启 Ollama:ollama serve |
422 Unprocessable Entity | 请求参数不符合AnthropicRequest模型定义 | 1. 查看curl命令中的 JSON 是否有语法错误(多逗号、少引号)2. 检查 messages是否为空数组3. 检查 role是否拼写为Role或user1 | 使用在线 JSON 校验器(如 jsonlint.com)格式化并检查请求体 |
TypeError: 'NoneType' object is not subscriptable | ollama.chat()返回了None | 1.ollama list确认MODEL_NAME是否存在2. ollama show <model_name>查看模型详情,确认其modelfile是否正确3. 检查 OLLAMA_BASE_URL是否指向了正确的端口(默认是 11434,不是 8000) | 重新ollama pull <model_name>,或检查模型的modelfile中是否有FROM指令指向了正确的基础模型 |
EventSource failed to connect(前端报错) | 前端 JavaScript 的EventSource无法连接到流式端点 | 1.curl -N命令是否能成功看到流式输出?2. 检查浏览器控制台,是否提示 CORS 错误 3. 检查 uvicorn启动时是否加了--host 0.0.0.0 | 在main.py中添加 CORS 中间件:from fastapi.middleware.cors import CORSMiddlewareapp.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"]) |
5.2 独家避坑心得:来自深夜调试的血泪经验
心得一:“stream=True时,Ollama 的done: true不一定在最后一行”
这是我在测试时发现的一个“幽灵 bug”。Ollama 的流式响应,有时会在done: true之后,再发一个空行,或者一个{"message": {"content": ""}, "done": true}。如果我们的convert_ollama_stream_to_anthropic_sse生成器没有处理这种情况,就会导致json.loads(line)抛出JSONDecodeError,进而中断整个流。我的解决方案是在try...except块里,对JSONDecodeError进行静默处理(continue),并增加一个if not line.strip(): continue的判断,跳过所有空白行。这个细节,官方文档和 GitHub Issues 里都找不到,只有亲手用tcpdump抓包分析 Ollama 的原始响应流,才能发现。
心得二:“system提示词太长,会导致 Ollama 内存溢出”
Ollama 对单次请求的上下文长度有限制。如果你的system提示词长达上千字,再叠加messages,总长度很容易超过模型的上下文窗口(如 haiku 是 200K tokens)。这时,Ollama 不会返回友好的错误,而是直接kill掉当前的推理进程,free-claude-code会收到一个httpx.ReadTimeout。我的应对策略是,在ollama_backend.py的send_request方法里,加入一个预估长度的检查:
def _estimate_context_length(self, messages: List[Dict], system: str = "") -> int: # 简单估算:每个字符约 1 token total = len(system) for msg in messages: total += len(msg.get("content", "")) return total # 在 send_request 开头 if self._estimate_context_length(request_data["messages"], request_data.get("system", "")) > 180000: raise HTTPException(status_code=400, detail="Context length too long. Please shorten your system prompt or messages.")这个“180000”的阈值,是我通过反复测试ollama run claude-3-haiku得出的经验值,它比理论值(200K)留出了 10% 的安全余量,能有效避免 Ollama 的崩溃。
**心得三:“IDE
