OpenAI 与 Anthropic API
用任意 OpenAI 或 Anthropic SDK 驱动无头 NevoFlux 容器——Chat Completions、Responses API 与 Messages API,阻塞或流式均可——并把每个模型名路由到 agent 循环或某个 Python 脚本后端。
无头容器支持三种 LLM 线格式。把官方 SDK 指向它,提示词就会变成一个任务:由 agent (或你提供的脚本)驱动浏览器,它的回答作为 assistant 消息返回。
| 协议 | 端点 | 所在端口 | 起始版本 |
|---|---|---|---|
| OpenAI Chat Completions | POST /v1/chat/completions | --http-addr 与 --openai-addr | v0.3.10(流式自 v0.3.14) |
| OpenAI 模型列表 | GET /v1/models | --http-addr 与 --openai-addr | v0.3.14 |
| OpenAI Responses | POST /v1/responses | --http-addr 与 --openai-addr | v0.3.16 |
| Anthropic Messages | POST /v1/messages | --http-addr 与 --anthropic-addr | v0.3.16 |
这四个端点都同时挂在主任务端口上,因此镜像默认命令
(--daemon --headless --http-addr 0.0.0.0:8080)已经提供它们。专用标志只是额外再开一个
监听端口。
这些端点都不校验 API 密钥。
Authorization: Bearer …与x-api-key会被接受并忽略 ——SDK 坚持要发点什么,随便填个占位值即可。任何能访问该端口的人,都能用容器的服务商 密钥和浏览器运行任务。网络边界(私有网络、带认证的反向代理)是唯一的闸门。
用官方 SDK 快速上手
from openai import OpenAI
client = OpenAI(base_url="http://host:8080/v1", api_key="unused")
# Chat Completions
client.chat.completions.create(
model="nevoflux",
messages=[{"role": "user", "content": "open example.com, report the title"}])
# Responses API
client.responses.create(model="nevoflux", input="open example.com, report the title")import anthropic
client = anthropic.Anthropic(base_url="http://host:8080", api_key="unused")
client.messages.create(
model="nevoflux", max_tokens=1024,
messages=[{"role": "user", "content": "open example.com, report the title"}])注意 base URL 的差别:OpenAI SDK 要 …/v1,Anthropic SDK 要裸主机地址(它会自己拼上
/v1/messages)。
用 curl:
curl -s localhost:8080/v1/chat/completions -H 'content-type: application/json' \
-d '{"model":"gpt-4","messages":[{"role":"user","content":"open example.com, report title"}]}'
# → {"object":"chat.completion","choices":[{"message":{"role":"assistant",
# "content":"...Example Domain"},"finish_reason":"stop"}],"usage":{...}, ...}请求如何变成任务
- 提示词取最后一条非空的 user 消息——
messages中(Chat Completions、Messages)或input中(Responses;裸字符串也算)的最后一条user。没有任何 user 文本的请求返回400。 - 其余一切来自环境变量。 这些请求不携带
mode、profile、policy或各种上限,因此 使用容器的NEVOFLUX_TASK_*/NEVOFLUX_POLICY_*/NEVOFLUX_WALL_CLOCK_SECS默认值 (见任务 API → 环境变量默认值)。 客户端无法通过发送不同的 JSON 来放宽策略。 - 采样参数不作用于 agent。
temperature、max_tokens、system/instructions以及 之前的轮次会作为数据传给脚本后端;agent 循环会忽略它们——它用自己配置的 模型把提示词当作任务运行。 - 任务共用一个队列。 每个请求都是与
POST /tasks同一个串行队列里的一个任务,所以 一个慢请求会拖后下一个。
流式
设置 "stream": true,各协议都会以自身的事件形状流式返回,SDK 的流式辅助函数无需改动
即可使用:
| 协议 | 帧 |
|---|---|
| Chat Completions | chat.completion.chunk 数据帧(先 role,再若干 content 增量,最后一个带 finish_reason 的块),以 data: [DONE] 结束 |
| Responses | 有名事件——response.created → response.output_item.added → response.content_part.added → response.output_text.delta… → response.output_text.done → response.content_part.done → response.output_item.done → response.completed——每个都带单调递增的 sequence_number |
| Messages | message_start → content_block_start → content_block_delta… → content_block_stop → message_delta → message_stop |
首帧与末帧之间到达什么,取决于后端:
- agent 循环在任务结束时才产出答案,所以流中会看到开头几帧、任务运行期间的保活帧, 然后整段答案作为一个增量到达。流式仍有价值——长任务期间连接保持存活——但不是逐 token 输出。
- 脚本后端会把它用
emit_text(…)发出的内容流式推送;emit_progress(…)则变成 SSE 注释帧,保持连接存活但不会显示在客户端里。
流开始后的错误以内容形式送达:在 Chat Completions 上,错误信息作为一个 content 增量
发送,并带 finish_reason: "error"——因为很多客户端会丢弃没有 choices 的数据帧,否则
用户只会看到一个空回答。Responses 与 Messages 使用各自协议的 error 事件。
客户端断开。 流式客户端离开时会置位一个取消标志。脚本后端会在下一个工具调用边界
停下;agent 循环不读取该标志,会把任务跑到自己结束为止。请设置
NEVOFLUX_WALL_CLOCK_SECS,以免被丢弃的请求无限期占用队列。
模型名与路由
GET /v1/models 列出本容器接受的模型名,请求中的 model 字段决定由哪个后端来运行。
单后端模式(默认——未设置 NEVOFLUX_OPENAI_MODELS):
- 列出的名字是
NEVOFLUX_HEADLESS_MODEL(默认nevoflux-script)。 - 接受任意模型名并原样回显,所以写死了
"model": "gpt-4"的客户端照常可用。 - 每个请求都运行 agent 循环;若设置了
NEVOFLUX_HEADLESS_SCRIPT,则运行该脚本。
多后端模式——把名字映射到脚本:
NEVOFLUX_OPENAI_MODELS='google-search=/opt/nevoflux/google-search.py,agent='- 每个
name=path条目就是一个模型;路径为空(agent=)表示"运行真正的 agent 循环", 而不是"回落到NEVOFLUX_HEADLESS_SCRIPT"。 GET /v1/models按顺序列出所有键。- 未知名字会被拒绝,返回
404 model_not_found——一旦名字有了含义,悄悄换成别的后端 比报错更糟。 model为空时选第一个条目。
| 环境变量 | 作用 | 默认值 |
|---|---|---|
NEVOFLUX_OPENAI_MODELS | name=/path/script.py,… 路由表;路径为空 = agent 循环 | 未设置(单后端) |
NEVOFLUX_HEADLESS_MODEL | 单后端模式下广播的模型名 | nevoflux-script |
NEVOFLUX_HEADLESS_SCRIPT | 单后端模式下每个任务运行的脚本(见固定脚本模式) | 未设置(agent 循环) |
脚本后端
脚本后端是一个 Python 文件,与固定脚本模式 运行在同一个沙箱解释器里,由它自己回答聊天请求——通常是在容器的浏览器里驱动某个网页聊天 界面,本身不发起任何 LLM 调用。请求被翻译成一次脚本调用,返回值再被翻译回协议的响应。
两个入口;服务端会在源码中查找 def chat( 并优先使用它:
def chat(request): # 完整契约(推荐)
emit_progress("opening the page") # SSE 注释帧,保持流存活
nav = browser_navigate(url="https://example.com")
md = browser_get_markdown(tab_id=nav["tab_id"])["markdown"]
emit_text(md[:200]) # 一个流式 content 增量
return {"content": md[:200]}
def run(task): # 旧式:只收到提示词字符串
...request 是一个 dict,包含(但不限于)protocol(openai、responses 或
anthropic)、model、messages(完整历史,含纯文本 content 与原始
content_parts)、task(最后一条 user 消息)、客户端的 tools / tool_choice、
stream、采样 params,以及 metadata.budget_secs——沙箱对本次调用实际执行的挂钟预算
(任务挂钟减去 10 秒余量)。
脚本只返回以下之一:
{"content": "the answer"}
{"tool_calls": [{"id": "call_1", "name": "read_file", "arguments": {"path": "/tmp/a"}}]}
{"error": {"message": "...", "type": "server_error", "code": "..."}}外加可选的 finish_reason 与 usage。tool_calls 会作为真正的工具调用透传给客户端
(网关负责把 arguments 字符串化),因此一个没有原生工具通道的聊天页面,也能服务会用
工具的客户端。旧式 run(task) 的返回值原样透传——返回 dict 时,整个 dict 会以 JSON 形式
成为 assistant 消息。
失败如何映射到 HTTP(非流式):
| 情形 | HTTP | type / code |
|---|---|---|
| 请求体格式错误、没有 user 消息 | 400 | invalid_request_error |
| 模型不在路由表中 | 404 | model_not_found |
| 脚本抛异常 / 沙箱失败 | 502 | server_error / script_error |
脚本返回 {"error": …} | 502 | 脚本提供的 code |
| 超时 | 504 | timeout |
完整契约——请求字段、浏览器工具的结果封装、带预算的轮询、在仅有聊天的页面上合成工具
调用——见 agent 仓库中的 docs/reference/script-backend-contract.md,示例位于
docs/reference/script-backends/。
脚本后端拿到的是浏览器,而不是策略。 它直接在沙箱里调用浏览器工具;限制 agent 的 shell 与文件写入的每任务
policy,并不会限制脚本对页面做什么。只部署你读过的脚本。
需要登录的后端。 驱动网页应用的脚本通常需要一个已登录的 base 配置。每个任务都在一次性
克隆上运行,所以请以任务的形式登录一次(不要用 /v1/chat/completions,它无法按请求
放宽挂钟)并设置 save_profile,之后用 NEVOFLUX_TASK_PROFILE 让后续请求指向保存好的
base。见任务 API → 按任务选择后端。
值得了解的协议差异
- Anthropic 有自己的端口标志(
--anthropic-addr),而不是挂在--openai-addr上:它的 请求头(x-api-key、anthropic-version)与错误封装({"type":"error","error":{…}}) 都是 Anthropic 自己的。 /v1/messages接受但不执行max_tokens——协议要求必填,但并没有据此采样。- 用量。 这些响应里的
usage来自后端终帧携带的内容:脚本可以设置它,agent 循环下 则为零。agent 循环真正的逐轮 token 统计(包括提示缓存读取)在任务记录上——GET /tasks/:id,见任务 API → 状态与结果。 - 响应 id 带有任务 id:
chatcmpl-task-7、resp_task-7、msg_task-7都属于任务task-7,可以接着用GET /tasks/task-7查看。