一个类似litellm的llm模型代理服务,主打一个轻量和本地化,适合个人本地聚合多个模型的场景。
支持的功能:
- 1 可以配置
chat/completions(下面称chat),responses和messages三种接口(暂不支持google接口)的模型供应商,并且同时对外暴露这三种接口,带/v1前缀。 - 2 可以配置修改请求中的
headers和body,传自定义数据,其中body支持深度合并。 - 3 可以配置兜底方案,设置兜底分组,如果调用的模型下游接口失败,并且在某个分组中,则会尝试分组其他模型。
- 4 支持配置文件热更新和本地管理页:
models、fallback、server.ttfb_timeout、record.max_size保存后立即生效,server.port写回后需重启进程。
Example:
server:
port: 3000 # default 3000
ttfb_timeout: 5000 # optional, upstream first-byte timeout in ms
record:
max_size: 100 # optional, default 10
models:
- name: gpt-5.4-a
# responses规范
provider: openai-responses
base_url: https://example.com/v1
api_key: YOUR_KEY1
model: openai/gpt-5.4
- name: gpt-5.4-b
# responses规范
provider: openai-responses
base_url: https://example.com/v1
api_key: YOUR_KEY1
model: openai/gpt-5.4
- name: glm5.1
# chat/completions规范
provider: openai-chat
base_url: https://example.com/v1
api_key: YOUR_KEY2
model: glm5.1
image: true # optional, default true; only effective for openai-chat provider
ttfb_timeout: 3000 # optional, overrides server.ttfb_timeout
proxy: http://127.0.0.1:7890 # optional, overrides HTTPS_PROXY/HTTP_PROXY for this model
headers:
user-agent: nanollm
body:
temperature: 1
store: false
text: '{"verbosity":"high"}'
bodyExpression: |
({
...body,
messages: body.messages?.map((message) => ({
...message,
updatedAt: Date.now()
}))
})
- name: claude-sonnet-4-6
# messages规范
provider: anthropic
base_url: https://example.com/v1
api_key: ${YOUR_KEY3_FROM_ENV_VAR}
model: claude-sonnet-4-6
ignore_invalid_history: true # optional, default true; Anthropic转换时丢弃空signature的thinking历史
fallback:
gpt-5.4:
- gpt-5.4-a
- gpt-5.4-b
- glm5.1Run the proxy server:
npx nanollm --config /path/to/config.yaml对外提供的模型为所有models[i].name和fallback.[group_name]例如上面demo配置就提供了
gpt-5.4-a
gpt-5.4-b
glm5.1
claude-sonnet-4-6
gpt-5.4
这样5个模型,其中gpt-5.4是兜底分组名,当使用这个模型的时候,会在下属列表的模型中寻找可用的模型,尝试顺序为按max(0, 最近5min失败次数-1)升序;如果分数相同,则保持配置里的原始顺序。
models[*].bodyExpression 可以在请求发往上游前动态改写最终 request body。表达式运行时会拿到变量 body,并且必须同步返回新的 body;执行顺序是先应用 body 深度合并,再执行 bodyExpression。
models:
- name: gpt-5.4-a
provider: openai-chat
base_url: https://example.com/v1
api_key: YOUR_KEY1
model: openai/gpt-5.4
bodyExpression: |
({
...body,
messages: body.messages?.map((message, index) => ({
...message,
content: index === 0 ? `${message.content}\nextra prompt` : message.content
}))
})models[*].ignore_invalid_history 目前只影响 provider: anthropic 的协议转换,默认值为 true。当 OpenAI Chat/Responses 历史消息里的明文 reasoning 被转换到 Anthropic Messages 时,如果对应 thinking block 没有 signature 或 signature 为空字符串,默认会丢弃该 thinking block,避免 Anthropic 上游校验空签名时报错。
如果需要保留旧行为,可以显式设置为 false,这时无签名 thinking 会继续带着空字符串 signature 发往 Anthropic 上游:
models:
- name: claude-sonnet
provider: anthropic
base_url: https://example.com/v1
api_key: YOUR_KEY
model: claude-sonnet-4-6
ignore_invalid_history: falsemodels[*].proxy 可以为单个模型配置请求下游供应商时使用的 HTTP proxy URL:
models:
- name: claude-sonnet
provider: anthropic
base_url: https://example.com/v1
api_key: YOUR_KEY
model: claude-sonnet-4-6
proxy: http://127.0.0.1:7890代理优先级为:
models[*].proxyHTTPS_PROXYHTTP_PROXY- 直连
当 proxy 为空字符串或未配置时,会继续回退到环境变量;当前支持 http:// 和 https:// 代理 URL。
models[*].name 支持后缀通配写法,可以把一类未显式配置的模型名路由到同一个上游配置:
models:
- name: gpt-*
provider: openai-chat
base_url: https://example.com/v1
api_key: YOUR_KEY
model: openai/gpt-*
- name: gpt-5.5-a
provider: openai-chat
base_url: https://example.com/v1
api_key: YOUR_KEY
model: openai/gpt-5.5
- name: "*"
provider: openai-chat
base_url: https://example.com/v1
api_key: YOUR_KEY
model: fallback-model
fallback:
gpt-5.5:
- gpt-5.5-a规则:
*必须只出现一次,并且只能放在结尾。合法例子:gpt-*、claude-*、*;非法例子:gpt-*-x、g*p*t、gpt**。- 匹配优先级是:精确 fallback 分组名 > 精确 model 名 > 通配 model 名。
- 如果多个通配 model 都能匹配,选择
*前缀最长的那个;前缀长度相同则按models配置顺序。 - 单独的
*可以匹配任意请求模型名,适合作为最后兜底。 /v1/models会直接展示配置中的通配名称,例如gpt-*和*。
以上面配置为例:
- 请求
gpt-5.5:优先命中 fallback 分组gpt-5.5。 - 请求
gpt-5.5-a:命中同名 modelgpt-5.5-a。 - 请求
gpt-5.6:没有同名分组或同名 model,于是命中gpt-*。 - 请求
llama-4:命中最后的*。
通配命中时,下游 model 字段里的 * 会被替换为请求中被 models[*].name 捕获的部分。例如:
name: gpt-*- 请求模型名:
gpt-5.6 - 捕获部分:
5.6 model: openai/gpt-*- 实际发给上游的
model:openai/gpt-5.6
如果下游 model 中没有 *,则始终使用固定模型名;如果下游 model 中有多个 *,会全部替换为同一个捕获部分。
models[*].image 目前只对 provider: openai-chat 生效,主要用于兼容不同 OpenAI-compatible chat 服务对图片输入的支持差异。默认值为 true。
image: true(默认):如果请求中包含图片,转为 chat 接口时保留 OpenAI chat 多模态content数组,例如:
{
"role": "user",
"content": [
{ "type": "text", "text": "请解释这张图" },
{ "type": "image_url", "image_url": { "url": "https://example.com/cat.png" } }
]
}image: false:用于 DeepSeek 等只接受content: string的 chat 上游;图片、文件、音频等非文本内容会降级为字符串描述,文本内容用换行拼接,例如:
{
"role": "user",
"content": "请解释这张图\nAttached image: https://example.com/cat.png"
}注意:image: false 当前不影响 provider: openai-responses 或 provider: anthropic,这两类上游仍按各自协议保留图片结构。
也可以指定配置文件运行:
npx nanollm --config /path/to/config.yaml如果希望 /status 和 /record 跨进程重启保留最近数据,可以启用 SQLite 存储:
npx nanollm --config /path/to/config.yaml --storage sqlite不传 --storage 时默认使用 memory,行为与旧版本一致。SQLite 文件固定保存在 ~/.nanollm/nanollm.sqlite3。
如果当前目录就有 config.yaml,也可以直接运行:
npx nanollm注意:npm 发布包不会包含作者本地的 config.yaml,需要你自己准备配置文件。
提供了 http://localhost:3000/admin 的本地配置管理页。
- 页面使用表单方式编辑常用配置项:全局设置、模型列表和 fallback 分组;
server.port仅展示当前运行值,不提供页面编辑。 - 常见使用方式是:先在
/admin中新增或修改模型,再调整 fallback 分组成员顺序,最后点击“保存并应用”立即生效。 - 页面内提供跳转到
/status和/record的快捷入口,方便保存后继续查看当前模型状态和最近请求记录。 - 如果只是想放弃当前改动,可以点击“撤销未保存修改”;如果配置文件已被外部改动,可以点击“从服务端刷新”重新加载最新内容。
- 点击保存后会先把表单数据转换成 YAML、校验配置,再原子写回配置文件。
models、fallback、server.ttfb_timeout、record.max_size会立即热更新到新请求。server.port会写回文件,但需要重启进程后才会真正生效。- 已有模型上未在表单中展开的高级字段会在保存时自动保留。
- 如果你在外部手动修改
config.yaml,服务也会自动检测并加载新配置;若新内容非法,则继续保留上一份有效配置并在管理页显示错误。
注意:/admin/config 设计目标是本机单用户管理,不建议暴露到局域网或公网。
提供了http://localhost:3000/status的监控页面,可以查看模型健康状态。
提供了http://localhost:3000/record的采样记录页面,可以查看请求记录,对debug非常有用(默认只保留最新10次请求,可通过record.max_size配置修改)。
默认情况下,上述数据都只存在内存中,进程结束即消失。使用 --storage sqlite 启动后,/status 会在 SQLite 中保留最近 1 个月的稀疏 5 分钟统计 bucket(页面仍只展示最近 6 小时),/record 会持久化最近 record.max_size 条请求记录。