1. OpenHands 本地部署里,模型请求到底卡在哪一层
OpenHands 是一个开源的 AI 软件工程代理,能读代码、跑命令、改文件,适合想在自己机器上跑一个「会动手的编程助手」的开发者。它默认通过 LiteLLM 统一管理模型调用,而 LiteLLM 的 endpoint、API Key、模型名这三样东西,决定了代理的每一次思考请求最终发到哪里。很多人本地部署完 OpenHands,界面能打开、会话能创建,但一发消息就报litellm.AuthenticationError或者APIConnectionError,问题基本都出在这一层:请求从 OpenHands 的 API 网关出去之后,没有正确落到你想要的模型服务上。
这篇笔记聚焦的就是这条链路:OpenHands 的 API 网关怎么路由请求、鉴权信息怎么传递、以及怎么把模型 endpoint 统一改到 TaoToken,让所有对话和工具调用都走同一个出口。我会给出可复制的配置文件、请求转发示例,以及用 curl 验证链路是否生效的具体动作。如果你正在做 OpenHands 本地部署,或者想把它的模型出口收敛到一个统一网关,这篇可以直接跟着操作。
先说清楚 OpenHands 的请求处理结构。它启动后主容器监听 3000 端口,Web 界面和 REST API 都从这里进。用户在前端发一条消息,请求先到 OpenHands 自己的 API 层,这一层做会话管理、请求校验、工具调度,然后把「需要模型生成」的部分交给 LiteLLM。LiteLLM 再根据配置里的model字段决定用哪个 provider、发到哪个 base_url。所以「改 endpoint」这件事,改的不是 OpenHands 的 3000 端口,而是 LiteLLM 指向的上游地址。
这里有个容易混淆的点:OpenHands 的 API 网关和模型网关是两层。前者管的是「用户请求怎么进 OpenHands」,后者管的是「OpenHands 怎么调模型」。我们要动的是后者。理解这一点,后面配置就不会改错地方。
我试过在本地用 Docker 跑 OpenHands,最初把LLM_BASE_URL写成了 OpenHands 自己的地址,结果请求在容器里打转,日志里全是连接超时。后来才理清:LLM_BASE_URL必须是模型服务的地址,跟 OpenHands 的 3000 端口没关系。这个坑先记下,第五节还会展开。
TaoToken 在这里的角色,就是那个统一的模型出口。它提供 OpenAI 兼容的接口,base_url 是https://taotoken.net/api,模型 ID 用标准的 provider 前缀格式。OpenHands 通过 LiteLLM 调用时,只要把 base_url、api_key、model 三件套配对,请求就能正常转发。下面进入具体配置。
2. TaoToken 前置准备:拿到 Base URL、Key 和 Model ID 三件套
在改 OpenHands 配置之前,先把 TaoToken 这边的三样东西准备好。这三样是后面所有配置的基础,缺一个请求都发不出去。
第一样是 Base URL。TaoToken 的 API 入口是https://taotoken.net/api,注意这里不带任何路径后缀,LiteLLM 和 OpenAI SDK 会自动在它后面拼/v1/chat/completions这类路径。如果你手动加了/v1,反而可能拼成/v1/v1/...导致 404。这一点在配置LLM_BASE_URL时尤其要注意。
第二样是 API Key。登录 TaoToken 控制台,在 API Keys 页面创建一个新 Key。创建时建议给它起个能认出来的名字,比如openhands-local,方便以后排查是哪个环境在用。Key 只在创建时完整显示一次,复制下来存好。如果你还没账号,可以先到官网了解:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台里生成 Key。
第三样是 Model ID。TaoToken 的模型 ID 采用provider/model的格式,比如anthropic/claude-sonnet-4-20250514、openai/gpt-4o这类。具体有哪些可用模型,可以在模型对话页面直接试,或者查接入文档里的模型列表。选模型时注意一点:OpenHands 的代理循环对模型的工具调用能力有要求,最好选支持 function calling 的模型,否则代理可能无法正确触发文件编辑、命令执行这些动作。
把这三样记下来,格式大概是这样:
| 配置项 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | 不带/v1后缀 |
| API Key | sk-xxxxxxxx | 控制台创建,只显示一次 |
| Model ID | anthropic/claude-sonnet-4-20250514 | 按 provider/model 格式 |
拿到之后,建议先用 curl 单独验证一次,确认 Key 和模型都可用,再去改 OpenHands。这样能把「TaoToken 侧的问题」和「OpenHands 侧的问题」分开,排障时省很多时间。
验证命令如下,把$TAOTOKEN_KEY换成你的真实 Key:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_KEY" \ -d '{ "model": "anthropic/claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "reply with ok"}], "max_tokens": 16 }'如果返回里能看到choices数组和一段回复内容,说明 Key 和模型都没问题。如果返回 401,检查 Key 是否复制完整、有没有多余空格;如果返回 404 或模型不存在,检查 Model ID 拼写。这一步过了,再进 OpenHands 配置。
需要提醒的是,TaoToken 的 Key 要当成密码对待,不要写进会提交到 Git 的文件里。后面配置 OpenHands 时,我会用环境变量和.env文件的方式管理,避免 Key 泄漏。
3. 可复制配置:把 OpenHands 的模型 endpoint 统一改到 TaoToken
OpenHands 的模型配置有几个入口,最直接的是通过环境变量和config.toml。不同版本略有差异,但核心就是让 LiteLLM 拿到正确的 base_url、api_key 和 model。下面给出两种方式,你可以按自己的部署方式选。
方式一:Docker 环境变量。如果你用docker run启动 OpenHands,直接在命令里注入这几个变量:
docker run -it --rm \ --pull=always \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \ -e LOG_ALL_EVENTS=true \ -e LLM_API_KEY=$TAOTOKEN_KEY \ -e LLM_BASE_URL=https://taotoken.net/api \ -e LLM_MODEL=anthropic/claude-sonnet-4-20250514 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/.openhands-state:/.openhands-state \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ docker.all-hands.dev/all-hands-ai/openhands:0.20这里三个变量是关键:LLM_API_KEY放 TaoToken 的 Key,LLM_BASE_URL放https://taotoken.net/api,LLM_MODEL放模型 ID。注意LLM_BASE_URL不要带/v1,LiteLLM 会自己处理路径。
方式二:config.toml文件。OpenHands 支持从~/.openhands-state/config.toml读取配置,适合想持久化、不想每次敲一长串环境变量的场景。文件内容如下:
[core] workspace_base = "./workspace" [llm] model = "anthropic/claude-sonnet-4-20250514" api_key = "sk-xxxxxxxx" base_url = "https://taotoken.net/api" custom_llm_provider = "openai" [llm.retry] num_retries = 3 retry_min_wait = 2 retry_max_wait = 10这里custom_llm_provider = "openai"是让 LiteLLM 用 OpenAI 兼容协议去发请求,TaoToken 的接口正好是 OpenAI 兼容的,所以这样配能通。base_url同样不带/v1。api_key这里直接写了明文,生产环境建议改成从环境变量读,或者用密钥管理工具注入。
如果你用的是 OpenHands 的 CLI 模式,配置读取逻辑一致,只是启动命令不同。CLI 下可以用--config指定配置文件路径,或者同样用环境变量覆盖。
配置改完之后,重启 OpenHands 容器或进程。重启后,OpenHands 的 API 网关在收到对话请求时,会把模型调用转发到https://taotoken.net/api,而不是默认的 provider 地址。这一步做完,链路的上游就切过来了。
有个细节值得说:OpenHands 的会话状态存在~/.openhands-state里,如果你之前用别的 endpoint 跑过会话,重启后旧会话可能还带着旧的模型配置。稳妥做法是新建一个会话测试,别在旧会话里验证。
另外,如果你在 OpenHands 里同时配了多个模型,LiteLLM 会按model字段路由。想统一出口的话,把所有用到的模型 ID 都确认在 TaoToken 侧可用,避免某个模型没开导致代理中途报错。
4. 验证请求处理链路:用 curl 和日志确认请求真的走到了 TaoToken
配置改完不代表链路就通了,得实际验证。验证分两层:先验证 OpenHands 的 API 网关能正常接收请求,再验证它转发出去的模型请求确实到了 TaoToken。
第一层,验证 OpenHands 自己的 API。容器起来后,先打健康检查:
curl -s http://localhost:3000/api/health正常会返回类似{"status":"ok"}的 JSON。如果这里就失败,说明 OpenHands 没起来,先看容器日志docker logs openhands-app,别急着查模型配置。
第二层,通过 OpenHands 的对话接口发一条消息,观察它是否成功调用模型。OpenHands 的对话接口大致是POST /api/chat或通过 WebSocket 流式返回,具体路径随版本变化。更稳的验证方式是直接看容器日志里的 LiteLLM 调用记录。启动时加上-e LOG_ALL_EVENTS=true,日志里会打印每次模型请求的 provider、model 和响应状态。
发一条测试消息后,日志里应该能看到类似这样的记录:
LiteLLM completion() model=anthropic/claude-sonnet-4-20250514; provider=openai POST Request to https://taotoken.net/api/v1/chat/completions看到taotoken.net这个域名出现在日志里,就说明请求确实转发到了 TaoToken,而不是别的地址。如果日志里出现的是api.openai.com或api.anthropic.com,说明LLM_BASE_URL没生效,检查环境变量有没有拼错、容器有没有重启。
第三层,直接对 TaoToken 发一次带工具调用的请求,验证 function calling 链路。因为 OpenHands 依赖工具调用,光验证普通对话不够。命令如下:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_KEY" \ -d '{ "model": "anthropic/claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "What is the weather in Beijing?"}], "tools": [{ "type": "function", "function": { "name": "get_weather", "description": "Get weather for a city", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"] } } }], "tool_choice": "auto" }'如果返回的choices[0].message里带tool_calls字段,说明模型支持工具调用,OpenHands 的代理循环能正常工作。如果返回的是普通文本、没有tool_calls,那这个模型可能不支持 function calling,换一个支持工具调用的模型 ID。
验证通过后,回到 OpenHands 界面,新建一个会话,让它做一个简单任务,比如「列出当前目录下的文件」。如果代理能正常执行命令并返回结果,说明整条链路——从 OpenHands API 网关到 LiteLLM 再到 TaoToken——都通了。
这一步的日志观察很关键。很多人配置完只看界面能不能回复,忽略了日志里的实际请求地址,结果某天 provider 切换了都不知道。养成看日志的习惯,排障会快很多。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
配置过程中最容易撞上的几类报错,这里逐个对照。
401 AuthenticationError。日志里出现litellm.AuthenticationError: OpenAIException - Invalid API key或401 Unauthorized。原因通常是 Key 没传对:要么环境变量名写错(OpenHands 认的是LLM_API_KEY,不是OPENAI_API_KEY),要么 Key 里带了换行或空格,要么用了过期/被删的 Key。排查动作:在容器里执行echo $LLM_API_KEY确认值正确,再用第 2 节的 curl 单独验证 Key。如果 curl 能通、OpenHands 不通,那就是环境变量没注入到容器,检查docker run的-e参数。
local proxy failed / Connection error。日志里出现litellm.APIConnectionError: OpenAIException - Connection error或local proxy failed。这类多半是LLM_BASE_URL写错,或者容器网络出不去。常见错误是把 base_url 写成了http://localhost:3000(OpenHands 自己的地址),或者写成了https://taotoken.net/api/v1(多了/v1)。排查动作:确认 base_url 是https://taotoken.net/api,然后在容器内执行curl -sI https://taotoken.net/api看能不能通。如果容器内不通、宿主机通,检查 Docker 的 DNS 和网络配置。
reading choices / KeyError 'choices'。日志里出现KeyError: 'choices'或reading 'choices'。这通常说明返回的响应结构不是预期的 OpenAI 格式,可能是 base_url 指到了非兼容接口,或者模型 ID 不存在导致返回了错误结构。排查动作:用第 2 节的 curl 直接打 TaoToken,看返回的 JSON 里有没有choices。如果没有,检查 Model ID 是否正确、该模型是否在 TaoToken 侧可用。
OAuth / 登录态相关报错。如果你在 OpenHands 里启用了 GitHub 集成或 OAuth 登录,可能会看到OAuth token invalid这类报错。这跟模型 endpoint 是两回事,属于 OpenHands 自身的鉴权层。排查时先确认模型链路是否独立可用,别把 OAuth 问题和模型配置混在一起。如果只是想让模型走 TaoToken,OAuth 可以先不配。
模型不支持工具调用。表现是代理能回复文字,但不会执行文件操作或命令,日志里没有tool_calls。这不是报错,是模型能力问题。换一个支持 function calling 的模型 ID 即可。
配置改了但没生效。最常见的原因是容器没重启,或者旧会话缓存了旧配置。排查动作:docker restart openhands-app,然后新建会话测试。另外确认config.toml的路径对不对,OpenHands 读的是~/.openhands-state/config.toml,不是项目目录下的。
把这几类对照着看,基本能覆盖 90% 的配置问题。核心思路就一条:先用 curl 把 TaoToken 侧验证通,再查 OpenHands 侧的注入和转发,两层分开排。
6. 把模型出口收敛之后,下一步可以做什么
模型 endpoint 统一到 TaoToken 之后,OpenHands 的所有对话和工具调用都走同一个出口,管理起来清爽很多。你可以在这个基础上做几件事:一是把不同任务路由到不同模型,比如代码生成用强模型、简单问答用快模型,在 LiteLLM 层做路由;二是给 Key 加上用量监控,观察代理的 token 消耗;三是把配置模板化,团队里每个人用同一份config.toml,只替换自己的 Key。
如果你还没开始配,建议按这个顺序走:先到控制台创建 Key,用 curl 验证模型可用,再改 OpenHands 的环境变量或config.toml,重启后看日志确认请求打到了taotoken.net,最后在界面里跑一个真实任务验证工具调用。每一步都验证过再进下一步,比一次性改完再排障快得多。
需要长期跑编码代理、或者想把多个工具统一到一个模型出口的话,可以了解下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。配置过程中遇到鉴权或转发问题,接入文档里有更细的字段说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。想先试试模型效果,直接开模型对话页面发一条消息就行:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。Key 管理在控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后留一个实操建议:把LLM_BASE_URL、LLM_MODEL这些值写进一个.env文件,用docker run --env-file .env启动,别每次手敲。.env加进.gitignore,Key 就不会误提交。这个习惯在本地部署里能省不少事。