跳转到正文
ThinkWatch
开始输入以搜索文档。
ThinkWatch Core · 配置手册

配置手册#

ThinkWatch Core 只读一个文件:config.yaml。本文逐项说明其中每个字段的作用、默认值、可选值,以及改动如何进入正在运行的进程。在服务器上运行 core、由桌面应用远程管理,见在服务器上运行 core。

本文的字段表由代码生成,与代码不一致时测试失败。表中列出的字段,就是程序实际读取的字段。

文件位置#

平台默认位置
macOS、Linux~/.thinkwatch/config.yaml
Windows%APPDATA%\ThinkWatch\config.yaml

THINKWATCH_HOME 替换整个目录;--config <路径> 为单条命令指定文件。core 的其余数据也在这个目录里:请求数据库(data.db)、配置历史(history/)、下载的价目表(model_prices.json),以及本地控制通道的 socket 文件(twcore.sock;Windows 上是回环端口,记录在 control.port 中)。目录只有所有者可访问(0700),配置文件权限为 0600:其中以明文保存密钥。

这两项权限就是这份文件在本机上的全部保护:挡得住其他用户,挡不住以同一用户身份运行的程序。这样的程序能读到文件里的每一把密钥,也能用其中的控制通道密钥经控制通道修改配置。出站脱敏不改变这一点:它保护的是请求带出本机的内容,不是磁盘上的文件。显示配置时给控制通道密钥打码,是为了让经控制通道的写入改不了它(见 listen.control),而不是对本机程序隐藏它;上游密钥和网关密钥按原样显示。

工具调用审查为这个目录内置了一条规则 thinkwatch-data:工具调用的路径或命令指向上述默认位置之一,或服务器上的 /var/lib/thinkwatch、/etc/thinkwatch 时记录下来,enforce 下切断响应。这样模型无法被诱导去读取这些密钥、改写约束它自己的防护。只是提到这个路径(例如在正在编辑的文档里)不算。THINKWATCH_HOME 指向别处时,可以在 security.inspect_tools 里为那个路径加一条自定义规则。

没有配置文件时,twcore serve 会写入一份初始配置;twcore init 也可以按需生成。两者生成的内容如下:

version: 1
listen:
  control:
    key: 6629…753d       # 自动生成
clients:
  - name: default
    key: tw-…            # 自动生成

这已是一份完整、合法的配置。其中还没有上游,因此控制面照常运行,请求会得到「尚未配置上游」的错误。加上一个上游即可转发:

providers:
  - name: anthropic
    base_url: https://api.anthropic.com
    key: ${ANTHROPIC_API_KEY}

读取规则#

  • **本文没有的字段名一律是错误。**把 port 写成 prot 时,不会被悄悄忽略、让网关在默认端口上起来;整份配置被拒绝,错误信息指出那个字段。
  • **默认值不写进文件。**没写的字段取下表中的默认值。应用和命令行只在取值不同于默认值时才写入字段,因此文件里出现的都是有人做出的选择。
  • ${VAR} 读取环境变量,适用于标注「可写 ${VAR}」的字段:上游密钥、请求头的值、代理密码。取值来自 core 进程的环境,在发送请求时读取;变量未设置时,该上游的请求失败,错误信息指出变量名。在 systemd 下,这个环境就是 unit 的 EnvironmentFile。
  • **名字即引用。**规则、策略组、密钥按名字引用上游、策略组、路由和价目表。指向不存在的名字,在加载时就报错,而不是成为一条永不命中的规则。在应用里改名时,所有引用在同一次写入中一起修改。以 __ 开头的名字保留给内置项。
  • version 是格式版本,目前为 1。版本更高的文件出自更新的 twcore,整份拒绝。

改动如何生效#

修改配置有三种途径:桌面应用、twcore config … 命令、用编辑器直接修改文件。三者走同一条路径。core 监视配置文件,保存后一秒内重新加载,无需重启。

新版本要通过以下每一关才会换入:

  1. 能按 YAML 解析。
  2. 符合结构:字段名已知,类型正确。
  3. 自洽:名字不重复、引用都能找到、正则能编译、CIDR 写法正确。
  4. 能据此建立运行时对象。

任何一关失败,原有配置继续服务,错误信息说明失败在哪一关、哪个位置。写错一个字不会让网关停下。在保存出合法版本之前,桌面应用会一直显示这次拒绝。

listen.gateway 的改动同样即时生效:core 打开新的监听;打不开时(例如端口被占用)保留原监听并报告原因。保留期限的改动在下一次每小时的清理时生效。

两方同时修改时(应用和手工编辑),后写入的一方因版本不一致被拒绝,不会覆盖先写入的内容。

历史与回滚#

每个生效过的版本都保存在配置文件旁边的 history/ 目录中,并记录来源(应用、命令行、外部编辑、回滚、凭据轮换)。保留最近 50 个版本。

twcore check                      # 只校验配置,不启动任何服务
twcore config show                # 打印当前配置及其版本
twcore config history             # 列出历史版本,最新的在前
twcore config rollback 3f9a2c     # 回滚到某个版本(写前几位即可)
twcore config set /listen/gateway/port 8790 --int

这些命令直接操作文件,因此 core 没有运行时也能使用,而那往往正是最需要回滚的时候。正在运行的 core 会像对待其他保存一样接收这些改动。

twcore config set <路径> <值> 修改文件中已经写出的一个值。路径中的列表项按其 name 定位(/providers/anthropic/base_url);数字表示下标(/routes/0/rules/1/to)。值默认按字符串写入,--int、--bool、--null 另作指定。写入前先校验结果。要添加文件中还没有的字段,请直接编辑文件。

网关写回的凭据#

有一种写入不是由人发起的。上游的 OAuth token 端点换发新的 refresh token 后,旧的随即作废,因此网关会把新 token(以及 access token 和过期时间)写回 providers[].oauth。只改这几个值,注释和排版保持原样。

字段参考#

每张表列出一节的全部字段。「默认值」一栏为「—」表示不写就没有这个字段,其含义见说明。

顶层#

字段类型默认值说明
version整数必填文件格式的版本,目前只有 1。更大的数字说明文件出自更新的 twcore,整份拒绝,不按一知半解的方式读。
listen对象,见 listen—网关和控制通道在哪里监听。
clients对象列表,见 clients[][]网关密钥。至少要有一把;twcore init 和首次 twcore serve 会写入一把名为 default 的。
providers对象列表,见 providers[][]上游。一个都没有也是合法配置:控制面照常运行,请求得到「尚未配置上游」的错误。
proxies对象列表,见 proxies[][]出站代理。在这里声明一次,由 providers[].proxy 按名字引用。
pricing对象,见 pricing—默认价目表是否定期刷新,以及自定义价目表。
client_probes对象,见 client_probes—客户端自行发出的辅助请求(连通性检查、预热、起标题)如何处理。
security对象,见 security—三项防护。出厂时都处在 observe,不改变、不拒绝任何请求。
retention对象,见 retention—请求日志保留多久。
failover对象,见 failover—上游失败后停用多久,以及流式回答的开头最多等多久。
aliases映射: 别名 → 字符串或字符串列表{}模型别名:同一个模型在各上游的名称合用一个名字,按顺序列出各上游的名称。请求别名时,提供其中任一名称的上游都能服务,发给它的是列表中它提供的第一个名称。别名不能列出别的别名。
groups对象列表,见 groups[][]策略组:多个上游合用一个名字,并规定如何在其中选择。
routes对象列表,见 routes[][]路由。一条都不写时,请求按上游的声明顺序故障转移。
default_route字符串—未指定路由的密钥走哪条路由。不写:名为 default 的路由;没有这条路由时走内置的故障转移。
default_key字符串—没有专用密钥的客户端使用哪一把。不写:名为 default 的那把,没有则取第一把。这把密钥不能停用。
plugins对象列表,见 plugins[][]脚本插件,按运行的顺序。由应用安装,每个插件的代码和设置是本文件旁边的一个文件。

listen#

core 在哪里接受连接。连接分两种:客户端发送请求的 AI 网关,以及桌面应用和 twcore 命令使用的控制通道。

字段类型默认值说明
gateway对象,见 listen.gateway—AI 网关,即客户端发送请求的地址。
control对象,见 listen.control—控制通道,即桌面应用和 twcore 命令连接 core 的途径。其中有控制密钥,因此每份配置都有这一节。

listen.gateway#

字段类型默认值说明
bindloopback | all | 网卡名 | IP 地址loopbackloopback 只有本机;all 所有网卡;网卡名(en0、eth0)每隔几秒重新解析,地址变了也能跟上,网卡暂时不在时先只监听 127.0.0.1,出现后再补上;写死的 IP 地址在地址变化后失效。绑定单张网卡时同时监听 127.0.0.1。
port整数8788网关的 TCP 端口。
allow_from字符串列表[10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7]本机以外允许连接的来源,写 CIDR 网段或单个地址。本机始终放行。[] 表示只有本机;放行所有来源要明确写 0.0.0.0/0。

bind 超出本机范围时,由 allow_from 决定允许谁连接。网关不提供 TLS:只在可信的网络中开放,或放在隧道、VPN 之后。

listen:
  gateway:
    bind: all
    port: 8788
    allow_from: [192.168.1.0/24]

listen.control#

控制通道供桌面应用,以及同一台机器上的 twcore config、twcore control-key 与 core 通信。本地通道是数据目录中的 socket 文件(Windows 上是回环端口);除非启用 remote,控制通道不开任何网络端口。

每条控制连接,无论本地还是远程,都以一次握手开始,证明双方都持有 key(Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s:密钥作为预共享密钥,每条连接协商新的会话密钥,通信内容加密)。不使用证书。不持有密钥的一方无法完成第一条握手消息。

字段类型默认值说明
key字符串自动生成控制密钥:64 个十六进制字符(32 字节)。所有控制连接,无论本地还是远程,都要证明持有这把密钥。缺失时 twcore serve 在开始监听前写入一把;格式不对的配置整份拒绝。用 twcore control-key 查看,twcore control-key --rotate 更换。
remote对象,见 listen.control.remote—供另一台机器上的桌面应用连接的网络端口。它是本地通道之外额外开的,不取代本地通道。

key 缺失时,由 twcore serve 在控制通道开始监听之前写入;twcore init 生成的配置也带有它。凡是显示配置或写入配置历史的地方,这个字段一律打码;把打码值原样存回时保留原值。密钥缺失或格式不对(不是 64 个十六进制字符)时整份配置无效,因此无法把它换成一把容易猜到的短密钥。

twcore control-key            # 显示密钥,用于粘贴到桌面应用
twcore control-key --rotate   # 更换密钥;已连接的应用需要重新连接

两条命令都在运行 core 的机器上执行。

listen.control.remote#

供另一台机器上的桌面应用连接的网络端口。它在本地通道之外额外开启,因此这里写错(端口被占用、allow_from 把自己挡在外面)也不会把本机锁在门外:twcore config 和本机应用照常可用。

字段类型默认值说明
enabled布尔false是否监听远程端口。不写或 false:不为控制面开任何网络端口。twcore remote enable / twcore remote disable 切换它;运行中的 core 在一秒内跟上。
bindloopback | all | 网卡名 | IP 地址all监听哪张网卡,写法同 listen.gateway.bind。
port整数必填TCP 端口。没有固定默认值:twcore init 和 twcore remote enable 写出这一节时随机写入 20000 到 32000 之间的一个端口(不会和网关相同)。不能是 0,也不能和网关端口相同。
allow_from字符串列表[10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7]允许连接的来源,写法同 listen.gateway.allow_from,但本机不会自动放行(本机有本地通道)。其他来源的连接在握手之前关闭,不回任何字节;收窄名单时,已经连着、不再放行的连接也随即断开。同一来源一分钟内握手失败 5 次,之后一分钟不理它。
listen:
  control:
    key: 9f2c…e41a          # 64 个十六进制字符
    remote:
      enabled: true
      bind: all
      port: 23483            # 随机生成,在生成这一节时写入
      allow_from: [192.168.1.0/24]

经远程端口的连接无论应用如何请求,都不能做三件事:关闭 core(它由 systemd 管理)、修改 listen.control(它进来的那扇门)、生成诊断包(诊断包会写在服务器上)。握手限时 5 秒;同一来源一分钟内握手失败 5 次,封禁一分钟。

clients#

网关密钥,即 Claude Code、Codex 等客户端向网关发送的密钥。密钥即身份:并发上限、模型范围、路由都按密钥设置。

字段类型默认值说明
name字符串必填密钥的名字,不能重复。路由规则用 when.client 匹配它。
key字符串必填客户端发送的密钥(放在 x-api-key 或 Authorization: Bearer 中)。生成的密钥以 tw- 开头,以免被误认作上游的密钥。不能重复。
max_concurrent整数—用这把密钥同时进行的请求数上限,超出的排队等待。不写:不限。0 会被拒绝。
allow字符串列表—这把密钥可用的模型,写模型 ID 或通配(claude-*)。写上游模型名,列有它的别名一并可用;写别名只放行别名。不写:全部模型。[]:一个都不给。
route字符串—这把密钥的请求走哪条路由。不写:default_route。
client字符串—这把密钥是为哪个客户端生成的(claude-code、codex 等),由桌面应用接管客户端时写入。一个客户端最多一把。
disabled布尔false拒绝使用这把密钥的所有请求,密钥本身保留。
limits对象列表,见 clients[].limits[]—用量上限:每分钟、每小时、每天、每周或每月的请求数、token 数或费用。请求要通过每一条。不写:不限。
clients:
  - name: default
    key: tw-a3f9c8d1e5b2h7k4m6n8p2q4
  - name: build-server
    key: tw-q8r2s4t6u8v2w4x6y8z2a4b6
    max_concurrent: 4
    allow: [claude-sonnet-*]
    route: cheap

clients[].limits#

一把密钥的用量上限:每分钟、每小时、每天、每周或每月最多多少个请求、多少 token、多少美元。 每一条只数其中一种;一把密钥可以有好几条,请求要通过每一条。

minute、hour 是滚动的:最近 60 秒、最近 60 分钟。用满之后,下一个空位在 failover.slot_wait_secs(默认 30 秒)之内空出来,请求就等它;等不到就拒绝。之后若还要等 上游空出位置,用的是同一段时间里剩下的部分。day、week、month 按 twcore 所在机器的 本地时区算,在零点、周一零点、每月一号零点重新开始;用满之后,到重新开始之前的请求一律拒绝。 机器换了时区时,当前这一天、这一周、这个月的用量按新的时区从请求记录里重新加起来。

被拒的请求收到 HTTP 429,错误格式和客户端自己的一致,写明是哪把密钥、哪一条上限、用了多少、 什么时候重置;流量列表里也有这一条。没有发到上游的请求不计入任何上限:被规则、内容过滤或 上限拒绝的,以及因为上游都已达到 max_concurrent 而被退回的。费用按每个请求记下的费用算,所以没有价格的模型、 billing: free 的上游算 0。还在进行的请求先按输入的估算计入,记下之后换成实际用量。 重启之后,这一天、这一周、这个月的用量从请求记录里重新加起来,所以请求记录要留够那一期: 设了按天的上限时 retention.row_days 至少 1,按周至少 7,按月至少 31;按分钟、按小时的 上限从零开始。

Responses 的 WebSocket 连接上,每个 response.create 各算一个请求:带着各自的用量和费用记下, 按这些上限检查;被拒的那一个收到 response.failed,连接保持不断。Realtime 的连接 (/v1/realtime)整条算一个请求:连上时按这些上限检查,这条连接上所有回答的 token 和费用在 断开时计入。

字段类型默认值说明
perminute | hour | day | week | month必填按多长一段时间算。minute、hour 是滚动的(最近 60 秒、最近 60 分钟);day、week、month 在本地时间的零点、周一零点、每月一号零点重新算。
requests整数—最多这么多个请求。数 token 的请求、网关自己答的、没有发到上游的不算。
tokens整数—最多这么多 token:未命中缓存的输入、写入缓存的和输出。
cost数字—最多花这么多美元,按每个请求记下的费用算;至少 0.01。没有价格的模型、billing: free 的上游算 0。
cache_reads布尔false把读取缓存的 token 也算进去。只有 tokens 上限能写。
clients:
  - name: build-server
    key: tw-q8r2s4t6u8v2w4x6y8z2a4b6
    limits:
      - { per: minute, requests: 30 }
      - { per: day, cost: 5 }
      - { per: month, tokens: 20000000, cache_reads: true }

providers#

上游,即请求被转发到的接口。

字段类型默认值说明
name字符串必填上游的名字,不能重复,也不能和策略组同名。以 __ 开头的名字保留给内置项。
base_url字符串必填接口地址,http:// 或 https://,按服务商文档写到版本段为止(https://api.anthropic.com、https://api.openai.com/v1)。Bedrock 写所在区域的推理地址:https://bedrock-runtime.<区域>.amazonaws.com。
key字符串,可写 ${VAR}—API 密钥,放进协议规定的请求头:x-api-key(Anthropic)、Authorization: Bearer(OpenAI,以及 Bedrock API Key)、x-goog-api-key(Gemini)。上游不需要密钥、或凭据写在 headers 里时不写。不能和 oauth、aws 同时写。
headers请求头名 → 值的映射{}额外的请求头,按书写顺序发送;值可以用 ${VAR},配置了 oauth 时可以用 {{access_token}}。最多 32 个。HTTP 或网关管理的请求头(host、content-length、connection 等)不能设置。
oauth对象,见 providers[].oauth—OAuth 凭据:用 refresh token 换取 access token。与 key 二选一。
aws对象,见 providers[].aws—Bedrock 上游的 AWS 访问密钥,写在这里或者从 AWS 的 profile 读:每个请求用它们签名(SigV4)。与 key(Bedrock API Key)二选一。
protocolanthropic | openai-chat | openai-responses | gemini | chatgpt | bedrock—上游的接口格式。不写:官方地址按 base_url 识别(Bedrock 的推理地址是 bedrock),其余按 anthropic 处理。
forward_client_identity布尔false同时发送客户端自己的身份:它的 User-Agent、x-app 和 originator 等身份请求头,以及请求体中的身份字段(如 metadata.user_id)。发送的都是客户端的原值,不做伪造。关闭时请求使用 ThinkWatch 的 User-Agent,不带客户端身份。用于只接受特定客户端的上游(Kimi For Coding、百炼 Coding Plan、只允许官方客户端的中转站)。chatgpt 不可用。
proxy字符串directdirect;system,即 core 进程环境变量 HTTPS_PROXY、HTTP_PROXY、ALL_PROXY 中的代理;或 proxies 中某一项的名字。
on_proxy_failfail | directfail代理不可用时:请求失败(fail),或改为直连(direct)。
models字符串列表[]上游不支持 /v1/models 时,按这份清单认定它提供的模型。
models_only字符串列表—只使用这家的这些模型,写 ID 或通配。范围外的模型不出现在模型列表里,也不会路由到这家。不写:全部。写空列表会被拒绝,暂停使用请用 disabled。
billingper-token | freeper-tokenper-token:费用为用量乘以所选价目表中的单价,订阅账号同样如此。free:费用记为 0。
pricing字符串—pricing.sheets 中某张价目表的名字。不写:默认价目表。
model_specs映射: 模型 ID → providers[].model_specs.*{}手写这家上游某些模型的上下文窗口、输出上限、会不会推理、收不收图,按模型 ID 完全匹配。写了就优先于价目表,用于价目表里没有或写错的模型。
max_concurrent整数—同时发给这家的请求最多几个,取值 1 到 1000。满了的时候,留在这家的对话等空位,别的请求换下一家;等多久见 failover.slot_wait_secs。不写:不限。
disabled布尔false不参与路由,模型也不出现在模型列表里;配置原样保留。

凭据有四种写法:key,放进协议规定的请求头;oauth,用 refresh token 换取 token;aws,Bedrock 上游签名请求用的访问密钥;headers,用于上游自有的鉴权方式。headers 可以和其余几种同时使用,但不能再设置已经承载凭据的那个请求头。

providers:
  - name: anthropic
    base_url: https://api.anthropic.com
    key: ${ANTHROPIC_API_KEY}

  - name: relay
    base_url: https://relay.example.com/v1
    protocol: openai-chat
    headers:
      X-Relay-Token: ${RELAY_TOKEN}
    proxy: office
    models_only: [gpt-4.1*, o3]
    pricing: relay-discount
    max_concurrent: 4

  - name: local
    base_url: http://127.0.0.1:11434/v1
    protocol: openai-chat
    billing: free

每个请求只带请求本身和上游需要的请求头,客户端的其他信息一律不发:凭据和 headers 中写的请求头、ThinkWatch 自己的 User-Agent,以及客户端请求中该上游协议使用的请求头(Anthropic 为 anthropic-*,OpenAI 为 Idempotency-Key 和 X-Client-Request-Id,Gemini 没有)。客户端自动填写的身份字段(如 Claude Code 的 metadata.user_id)从请求体中去掉。只接受特定客户端的上游,打开 forward_client_identity。

ChatGPT 账号上游(protocol: chatgpt)只接受桌面应用登录得到的凭据,不能手写。不支持 Claude 和 Google 的订阅登录,请使用 API 密钥。

有的中转站和账号同时只接受几个请求,多出来的直接拒绝。max_concurrent 让网关守住这个数:请求发出时占用这家的一个位置,回答完整交给客户端、或者客户端断开时归还。这家满了的时候,为复用提示缓存而留在这家的对话等空位,别的请求直接换下一家。最多等多久由 failover.slot_wait_secs 决定。等待不算失败,这家不会因此停用。只计算 token 数的请求不占位置。Responses 的 WebSocket 连接上,每个 response.create 从发出起占一个位置,直到它的回答结束,密钥的 max_concurrent 也一样;空闲的连接不占位置。

providers[].oauth#

字段类型默认值说明
access字符串—当前的 access token。每次刷新后由网关写回;不写则在第一次使用时换取。
expires_at字符串—access 的过期时间,RFC 3339(UTC),随 token 一起写回。不写:一直用到上游返回 401。
refresh字符串必填Refresh token。token 端点换发新的之后旧的即作废,因此网关会把新的写回本文件。
endpoint字符串必填token 端点的地址。
client_id字符串—OAuth 客户端 ID,端点需要时填写。
client_secret字符串—OAuth 客户端密钥,端点需要时填写。
refresh_before时长(30s、5m、1h)—提前多久刷新。不写或写法无法识别:5m。

access token 默认放进协议的鉴权请求头。要放在别处,在 headers 中写出那个请求头,用 {{access_token}} 标出 token 的位置:

    oauth:
      refresh: ${VENDOR_REFRESH_TOKEN}
      endpoint: https://auth.example.com/oauth/token
      client_id: my-client
    headers:
      X-Access: Token {{access_token}}

providers[].aws#

字段类型默认值说明
access_key_id字符串,可写 ${VAR}—访问密钥 ID。与 secret_access_key 一起写;也可以改写 profile。
secret_access_key字符串,可写 ${VAR}—私有访问密钥。
session_token字符串,可写 ${VAR}—临时凭证(如 STS 签发的)的会话令牌。过期之后请求会被拒绝,直到换上新的。
profile字符串—从 AWS 凭证文件读访问密钥时用的 profile,代替把密钥写在这里:~/.aws/credentials 和 ~/.aws/config,或 AWS_SHARED_CREDENTIALS_FILE、AWS_CONFIG_FILE 指定的文件,读的是 core 所在机器上的。文件变了会重新读。
region字符串—签名用的区域。不写:取 base_url 里的区域,这时 base_url 必须是标准的推理地址。base_url 是 VPC 端点或代理时必须写。

Bedrock 上游有两种认证方式。Bedrock API Key 写在 key 中,以 Authorization: Bearer 发送。AWS 访问密钥写在 aws 中,或者用 aws.profile 从 AWS 凭证文件里的某个 profile 读取:每个请求在请求体定稿之后用它们签名(SigV4),密钥本身不会发送。写在配置中的密钥可以用 ${VAR} 从环境变量读取。profile 读的是 core 所在机器上的文件,文件变了会重新读,因此把临时密钥刷新进 ~/.aws/credentials 的工具不需要重启 core。获取凭证时不执行任何命令,因此经 IAM Identity Center 登录(aws sso login)、运行 credential_process 或扮演角色的 profile 无法使用,请改为导出它们生成的密钥。

providers:
  - name: bedrock
    base_url: https://bedrock-runtime.us-east-1.amazonaws.com
    key: ${AWS_BEARER_TOKEN_BEDROCK}

  - name: bedrock-keys
    base_url: https://bedrock-runtime.eu-west-1.amazonaws.com
    aws:
      access_key_id: ${AWS_ACCESS_KEY_ID}
      secret_access_key: ${AWS_SECRET_ACCESS_KEY}
      session_token: ${AWS_SESSION_TOKEN}

  - name: bedrock-profile
    base_url: https://bedrock-runtime.us-west-2.amazonaws.com
    aws:
      profile: dev

请求转换为 Converse 格式。模型清单取自所在区域的控制面:可按需调用的基础模型、AWS 预设的推理配置(us.anthropic.claude-…),以及账号自己创建的应用推理配置(按调用时使用的 ARN 列出)。列出清单需要 bedrock:ListFoundationModels 和 bedrock:ListInferenceProfiles 权限;没有这两项权限时请求照常转发,可以在 models 中手动列出模型。使用 VPC 端点或代理时,在 base_url 中写它的地址,在 aws.region 中写区域;模型清单也向该地址获取。

providers[].model_specs#

模型的上下文窗口和输出上限取自价目表。中转站自有的模型常常不在价目表里,价目表偶尔也会写错。这时在这里按这家上游、按它模型清单里的 ID(完全匹配)手写。写了的一项优先于价目表,没写的一项仍取价目表。两项至少写一项,都不能是 0。

各处用的是同一个数:各种客户端格式的 /v1/models、由这家上游服务的别名、网关判断一段对话是否还装得下当前模型,以及请求转换为 Anthropic 格式且没有写输出上限时补上的值。同一个模型由几家上游提供时,/v1/models 按 providers 中排在最前的那一家给出。

字段类型默认值说明
context_window整数—上下文窗口,即一次请求最多输入多少 token。不写:取价目表的。
max_output_tokens整数—一次回答最多输出多少 token。不写:取价目表的。
reasoning布尔—这个模型会不会推理。模型列表(GET /v1/models)带着它,客户端据此给出推理档位。不写:取价目表的;价目表也没写时,列表里不给这一项。
image_input布尔—这个模型收不收图。模型列表里以 input_modalities 给出。不写:取价目表的;价目表也没写时,列表里不给这一项。
providers:
  - name: relay
    base_url: https://relay.example.com/v1
    protocol: openai-chat
    model_specs:
      glm-5-air: { context_window: 128000, max_output_tokens: 16384 }
      claude-sonnet-4-5: { context_window: 1000000 }

proxies#

出站代理。不同上游需要的代理往往不同,因此没有全局开关:由每个上游用 proxy 选择。

字段类型默认值说明
name字符串必填providers[].proxy 引用的名字。direct 和 system 是内置的。
typesocks5 | socks5h | http | httpssocks5hsocks5h 把域名交给代理解析;socks5 先在本地解析。http 和 https 是 HTTP 代理。
addr字符串必填代理的 host:port。
auth对象,见 proxies[].auth—代理需要时填写用户名和密码。

proxies[].auth#

字段类型默认值说明
user字符串必填用户名。
pass字符串,可写 ${VAR}必填密码。
proxies:
  - name: office
    type: http
    addr: proxy.example.com:3128
    auth:
      user: alice
      pass: ${PROXY_PASSWORD}

on_proxy_fail 默认为 fail:静默改为直连会让请求走一条意料之外的路径,而使用者仍以为请求经过了代理。

pricing#

一次请求的费用为用量乘以模型单价。单价来自默认价目表(LiteLLM 的公开数据集,程序内置一份,每天联网刷新),或来自上游选用的自定义价目表。单价变动只影响此后的请求,不改变已记录请求的费用。

字段类型默认值说明
auto_update布尔true每天联网刷新一次默认价目表,保存为 config.yaml 旁边的 model_prices.json;此前以及离线时使用内置于程序中的价目表。
sheets对象列表,见 pricing.sheets[][]自定义价目表。上游用 providers[].pricing 选用。

pricing.sheets#

字段类型默认值说明
name字符串必填价目表的名字,不能重复。
multiplier数字1作用于默认价目表的全部单价,包括缓存和长上下文单价。
models映射: 模型 ID → pricing.sheets[].models.*{}单独定价的模型。它们取代默认价目表中该模型的单价,不乘倍率。

pricing.sheets[].models#

单价以每百万 token 的美元计,与厂商价格页上的写法一致。每个字段都要写明,计价时不做任何推算。

字段类型默认值说明
input数字必填每百万输入 token 的美元价格。
output数字必填每百万输出 token 的美元价格。
cache_read数字必填每百万缓存读取 token 的美元价格。
cache_write_5m数字必填每百万写入 5 分钟缓存 token 的美元价格。
cache_write_1h数字必填每百万写入 1 小时缓存 token 的美元价格。
input_above_200k数字—单次请求的输入(连同缓存读写)超过 200K token 后的输入单价。与 output_above_200k 同时写或都不写;缓存单价仍按上面写的算。
output_above_200k数字—单次请求的输入(连同缓存读写)超过 200K token 后的输出单价。
pricing:
  sheets:
    - name: relay-discount
      multiplier: 0.8
      models:
        claude-sonnet-4-5-thinking:
          input: 3
          output: 15
          cache_read: 0.3
          cache_write_5m: 3.75
          cache_write_1h: 6

client_probes#

客户端发出的请求中,有一部分并非出自使用者:连通性检查、预热、会话标题、话题检测、建议。每一类要么在本地应答(intercept,不向上游发送任何内容),要么转发(forward)。转发的请求和普通请求一样经过路由规则,并带着自己的类别,规则可以用 when.intent 匹配。默认只拦下拦了也不会少任何东西的那几类。

字段类型默认值说明
health_checkintercept | forwardintercept连通性检查(max_tokens: 1)。默认在本地应答,不影响任何功能。
warmupintercept | forwardintercept预热请求。默认在本地应答。
titlingintercept | forwardforward为会话起标题的请求。默认转发:拦下后所有会话都会是同一个标题。
topic_detectintercept | forwardforward话题检测。默认转发。
suggestionintercept | forwardforward建议。默认转发。

security#

三项防护,对所有上游一视同仁。每一项都有 mode:off、observe(检测并记录,不改变任何行为)、enforce(处置)。出厂时三项都是 observe。各项在 enforce 下的处置不同:出站脱敏替换,工具调用审查切断响应,内容过滤按每条规则的处置拒绝、删除或只记录。

字段类型默认值说明
redact对象,见 security.redact—出站脱敏:请求发出前,把其中任何位置的凭据和个人信息替换掉。
inspect_tools对象,见 security.inspect_tools—工具调用审查:模型返回的工具调用中出现危险命令时切断响应。
content对象,见 security.content—内容过滤:调用方发送的内容中出现指定的词、写法或字符(包括隐藏字符)时,按规则拒绝请求、删除命中的内容或只记录。

security.redact#

请求发出前,在整个请求中(包括系统提示、之前的回答和工具调用)查找凭据和个人信息。enforce 下将其替换为占位符,回答中重复出现时再换回原值。图片、文件等 base64 内容不查。

字段类型默认值说明
modeoff | observe | enforceobserveoff 不检测;observe 检测并记录,不改变任何行为;enforce 检测并处置。
enable字符串列表[]打开出厂时关着的内置规则,按 id。
disable字符串列表[]关掉内置规则,按 id。
custom对象列表,见 security.redact.custom[][]自定义规则:正则匹配到的内容和凭据一样替换。

字段类型默认值说明
name字符串必填日志和应用里显示的名字,也是规则的标识;同一项防护里不能重名。
pattern字符串必填正则表达式。
label字符串SECRET占位符名称:正则匹配到的内容替换为 <<TW_名称_序号>>,每个名称各自编号。只能使用大写字母、数字和下划线,以字母开头,最多 24 个字符。
disabled布尔false停用这条规则,规则本身留在文件里。

内置规则:

id名称出厂
anthropic-api-keyAnthropic API key开
openai-project-keyOpenAI project key开
openai-api-keyOpenAI API key开
github-personal-tokenGitHub personal access token开
github-oauth-tokenGitHub OAuth token开
github-server-tokenGitHub server token开
github-user-tokenGitHub user token开
github-fine-grained-tokenGitHub fine-grained token开
slack-bot-tokenSlack bot token开
slack-user-tokenSlack user token开
slack-app-tokenSlack app token开
aws-access-key-idAWS access key ID开
aws-temporary-key-idAWS temporary access key ID开
google-api-keyGoogle API key开
google-oauth-tokenGoogle OAuth token开
gitlab-tokenGitLab token开
stripe-live-keyStripe live key开
stripe-restricted-keyStripe restricted key开
npm-tokennpm token开
digitalocean-tokenDigitalOcean token开
sendgrid-keySendGrid key开
private-keyPrivate key开
jwtJWT开
conn-string-passwordConnection string password开
cn-resident-idChinese resident ID number开
bank-cardBank card number开
emailEmail address关
cn-mobile-phoneChinese mainland mobile number关
internal-ipInternal IP address关
internal-domainInternal domain关

cn-resident-id、bank-card、email、cn-mobile-phone 查找的是个人信息而不是凭据。前两条出厂开启,只认结构上核对得上的:

  • cn-resident-id:18 位的中华人民共和国居民身份证号码。前两位须是省级行政区划代码,出生日期须是 1900 年 1 月 1 日至今天之间的真实日期,末位须是正确的校验码(ISO 7064 MOD 11-2)。15 位的旧号码不认。
  • bank-card:卡号。开头和位数须属于银联、Visa、Mastercard、American Express、JCB、Discover 或 Diners Club,并通过 Luhn 校验;连续书写,或四位一组、以单个空格或单个连字符分隔均可(American Express 另认 4-6-5,Diners Club 另认 4-6-4)。Stripe、Braintree、Adyen 公开的测试卡号不认。

email 和 cn-mobile-phone 出厂关闭:它们没有可核对的结构,代码和文档中形似的内容很多。

  • email:邮箱地址,域名至少两段、最后一段为两个以上的字母。URL 中的用户名(https://user@host)和 icon@2x.png 这类文件名不认。
  • cn-mobile-phone:中国大陆手机号,11 位数字,以 1 开头、第二位为 3 至 9,前后不紧挨其他数字。

夹在更长的一串字母或数字中间的号码不认;请求体中以 JSON 数值写出的号码(例如工具调用的参数)也不认,替换它会使请求体不再是合法的 JSON。占位符写明原来是什么(<<TW_ID_NUMBER_1>>、<<TW_CARD_NUMBER_1>>、<<TW_EMAIL_1>>、<<TW_PHONE_1>>),安全日志中号码只显示最后四位,邮箱只显示第一个字和域名。自定义规则替换为 <<TW_SECRET_1>>,写了占位符名称(label)时用它。

security.inspect_tools#

按规则检查模型返回的工具调用。enforce 下命中处置为 cut 的规则时切断响应,客户端拿不到可执行的完整调用。

字段类型默认值说明
modeoff | observe | enforceobserveoff 不检测;observe 检测并记录,不改变任何行为;enforce 检测并处置。
enable字符串列表[]打开出厂时关着的内置规则,按 id。
disable字符串列表[]关掉内置规则,按 id。
actions映射: 内置规则 id → cut | record{}内置规则在 enforce 下的处置,只写与出厂不同的(rm-rf-root: record)。
custom对象列表,见 security.inspect_tools.custom[][]自定义规则,按工具调用的参数匹配。

字段类型默认值说明
name字符串必填日志和应用里显示的名字,也是规则的标识;同一项防护里不能重名。
pattern字符串必填正则表达式。
actioncut | recordrecordenforce 下切断响应(cut),或只记录(record)。
disabled布尔false停用这条规则,规则本身留在文件里。

内置规则:

id名称enforce 下出厂处置
curl-pipe-shDownload and runcut
base64-decode-execDecode and runcut
exfil-envSend out environment variablescut
exfil-credentialsSend out a credential filecut
exfil-credentials-reversedSend out a credential file (verb first)cut
ssh-key-readRead a private key or cloud credentialcut
secret-to-unknown-hostSend a credential to an unknown hostcut
thinkwatch-dataRead or change ThinkWatch’s own datacut
write-startup-itemWrite a startup itemcut
crontab-installInstall a scheduled jobcut
rm-rf-rootDelete home or rootrecord
chmod-777World-writable permissionsrecord
upload-file-to-hostUpload a local file to an external hostrecord

security.content#

调用方发送的内容中(用户消息及其中的工具结果,不含系统提示和模型自己的回答)出现的词、写法或字符。每条规则按关键词(contains)、正则(regex)或码位(codepoints)匹配,并写明 enforce 下的处置:block 拒绝请求,strip 把命中的内容从调用方的正文中全部删除后发出,record 只记录。删除之后会再检查一遍:被隐藏字符拆开的关键词,删掉隐藏字符后照样命中。

字段类型默认值说明
modeoff | observe | enforceobserveoff 不检测;observe 检测并记录,不改变任何行为;enforce 检测并处置。
enable字符串列表[]打开出厂时关着的内置规则,按 id。
disable字符串列表[]关掉内置规则,按 id。
actions映射: 内置规则 id → block | strip | record{}内置规则在 enforce 下的处置,只写与出厂不同的。
custom对象列表,见 security.content.custom[][]自定义规则。

字段类型默认值说明
name字符串必填日志和应用里显示的名字,也是规则的标识;同一项防护里不能重名。
pattern字符串必填关键词;match: regex 时为正则表达式;match: codepoints 时为码位(U+200B, U+E0000–U+E007F)。关键词和正则不区分大小写。
matchcontains | regex | codepointscontainscontains:正文包含 pattern。regex:pattern 是正则表达式。codepoints:正文中出现 pattern 所列码位或码位范围内的字符,多个之间用逗号分隔。
actionblock | strip | recordrecordenforce 下拒绝请求(block)、删除命中的内容后发出(strip),或只记录(record)。
disabled布尔false停用这条规则,规则本身留在文件里。

内置规则:

id名称分组出厂enforce 下出厂处置
unicode-tagsUnicode tag charactersinvisible开strip
bidi-controlsBidirectional controlsinvisible开strip
zero-widthZero-width charactersinvisible关strip
private-usePrivate-use charactersinvisible关strip
ignore-previous-instructionsIgnore previous instructionsinjection开block
ignore-all-previousIgnore all previousinjection开block
disregard-your-instructionsDisregard your instructionsinjection开block
jailbreakJailbreakinjection关block
danDANinjection关block
developer-modeDeveloper modeinjection关block
you-are-nowPersona manipulationpersona关block
new-personaNew personapersona关record
act-asAct aspersona关record
pretend-to-bePretend to bepersona关record
system-promptSystem prompt extractionpersona关record
reveal-your-instructionsReveal instructionspersona关record
what-are-your-rulesWhat are your rulespersona关record
base64-wallBase64 smugglingpersona关record
zh-ignore-previousIgnore previous instructions (Chinese)chinese关block
zh-forget-yourForget your instructions (Chinese)chinese关block
zh-do-not-followDo not follow (Chinese)chinese关block
zh-you-are-nowYou are now (Chinese)chinese关block
zh-role-playRole-play (Chinese)chinese关record
zh-reveal-yourReveal your instructions (Chinese)chinese关record
zh-system-promptSystem prompt (Chinese)chinese关record
zh-jailbreakJailbreak (Chinese)chinese关block

invisible(隐藏字符)一组匹配人看不见、模型读得到的字符。Unicode 标签字符(U+E0000–U+E007F)和双向控制符(U+202A–U+202E、U+2066–U+2069)出厂开启;零宽字符(U+200B–U+200D、U+2060、U+FEFF)和私用区字符(U+E000–U+F8FF、U+F0000–U+FFFFD、U+100000–U+10FFFD)出厂关闭,表情符号、波斯文和图标字体也会用到它们。四条在 enforce 下都删除命中的字符。

security:
  redact:
    mode: enforce
    enable: [internal-ip]
    custom:
      - name: employee-id
        pattern: 'EMP-\d{6}'
        label: EMPLOYEE
  inspect_tools:
    mode: enforce
  content:
    mode: enforce
    enable: [zero-width]
    custom:
      - name: project-x
        pattern: project-x
        action: strip

retention#

设两个期限,是因为两类数据的体积相差三个数量级:一条请求的正文有几十 KB,一条请求记录只有几百字节。字节上限用于应对用量突增。

每份请求和响应正文最多保存 4 MiB,更长的只保存开头。正文写入磁盘前已去掉凭据和个人号码:enforce 下保存的请求带着发给上游的占位符,脱敏规则(security.redact)认出的其他内容一律打码保存,off 时也一样。

字段类型默认值说明
body_days整数7请求和响应正文保留的天数。
row_days整数90每条请求记录(时间、模型、用量、费用)保留的天数。
body_max_bytes整数5368709120正文最多占用的字节数,超出时从最早的日期开始删除。默认 5 GiB。

failover#

上游失败后会停用一段时间,接下来的请求直接交给下一个候选。停用多久取决于上游 给出的原因:余额不足要等充值,额度用完要等到上游说的重置时刻,限流通常几秒钟就 过去。只有一个候选的请求不受影响。Responses 的 WebSocket 连接上,每个 response.create 在这里和一个请求一样算:第一段内容到达之前因为上游出错而失败的,算作一次失败;上游 拒绝或连不上的连接也一样。

流式回答在第一段内容交给客户端之前,上游在流里报的错误和错误状态码一样,会把 请求换到下一个候选。

上游也可能开头很慢:收下请求之后很久都不发内容。开启 next_on_slow_start 后,请求 发出 stream_start_wait_secs 秒仍没有内容,就换到下一个候选。默认关闭,因为先思考 再输出的模型本来就可能很久才开始;开启时建议等 30 秒以上。最后一个候选总是等下去, 被放弃的上游不会停用。到点那一刻并发数已满(max_concurrent)的候选不算下一个:请求 留在慢的那一家。

failover:
  stream_start_wait_secs: 30
  next_on_slow_start: true

上游的并发数满了(max_concurrent)时,一个请求等空位合计最多 slot_wait_secs 秒。等密钥的 minute、hour 上限也算在这段时间里,两样加起来不超过 slot_wait_secs。 进行中的对话等它留在的那一家,到时还没有空位就换下一家,缓存在那边从头建; 新的对话遇到满着的上游直接跳过。候选全满时,请求等先空出来的那一家;都没有空出来, 客户端收到 429 和 Retry-After,说明上游都忙。如果有上游收到过这个请求并且失败了, 客户端收到的是那次失败。

字段类型默认值说明
failures_to_pause整数3没有说明原因的失败(5xx、连接失败)连续几次后停用这家上游,取值 1 到 100。
pause_secs整数60这类失败第一次停用的秒数。之后每停用一次翻一倍,直到 max_pause_secs;成功一次后回到这个值。
max_pause_secs整数600翻倍后的停用上限,单位秒,不小于 pause_secs。
no_balance_pause_secs整数1800上游报告余额不足时停用的秒数。
quota_pause_secs整数3600上游报告额度用完、但没有给出重置时间时停用的秒数。给出了重置时间的,停用到那一刻。
rate_limit_max_pause_secs整数3600被限流的上游按它给的 Retry-After 停用,最多这么多秒。没有 Retry-After 的按没有说明原因的失败计。
stream_start_wait_secs整数15流式回答在第一段内容到达前最多暂存的秒数。在此之前上游报错,请求换到下一家;超过这个时间,已收到的部分照常交给客户端。取值 1 到 120。
next_on_slow_start布尔false流式回答在请求发出 stream_start_wait_secs 秒后仍没有内容时,放弃这家上游,把请求交给下一家。最后一家总是等下去。被放弃的上游不会停用。开启时 stream_start_wait_secs 至少为 5。
slot_wait_secs整数30一个请求合计最多等的秒数,从过了密钥自己的 max_concurrent 时算起:等密钥的 minute、hour 上限空出名额,和等并发数满了(max_concurrent)的上游空出位置,都算在里面。密钥的上限到时空不出来就拒绝;等不到上游的空位就换下一家,候选全满时回 429。0:不等。取值 0 到 300。

aliases#

模型别名是同一个模型的另一个名称。客户端像请求其他模型一样请求它,每家上游收到的是自己对这个模型的叫法。

aliases:
  deepseek-v4.1: DeepSeek-v4.1-flash
  claude-sonnet-5:
    - claude-sonnet-5
    - us.anthropic.claude-sonnet-5-v1:0
    - anthropic/claude-sonnet-5
  • 上游在 models_only 范围内提供列出的任一名称,就能服务这个别名,收到的是列表里它提供的第一个名称。能服务同一别名的上游互为备用。
  • GET /v1/models 在上游模型旁一并列出别名,原来的名称照常可用。
  • 别名优先于同名的上游模型:上例中请求 claude-sonnet-5 只会发给提供这三个名称之一的上游。要让某家上游继续用自己的同名模型提供服务,把它的名称列进去。
  • 密钥的 allow 和规则的 when.model 写上游模型名时,对列有这个名称的别名同样有效;写别名时只对别名有效。
  • 上游答的是发给它的那个模型时,回答里的模型名写成客户端请求的名称;答的是别的模型,原样转发。
  • 计价和上游体检按发给上游的名称;请求记录保留客户端请求的名称,每次尝试记录实际发出的名称。
  • 要把某把密钥的请求发到某家上游的某个模型,用带指定模型的规则(routes[].rules[].to),不要用别名:别名会改变这个名称对所有客户端的含义。

groups#

策略组让多个上游合用一个名字。规则用 to 把请求交给策略组。

字段类型默认值说明
name字符串必填策略组的名字,不能重复,也不能和上游同名。
typefallback | select | load-balance | url-test | cheapestfallbackfallback:按顺序取第一个健康的。select:取 selected 指定的那个。load-balance:请求按成员的权重分;进行中的对话留在原来那一家。url-test:按实测从发出请求到回答第一段内容的时间取最快的。cheapest:取输入单价最低的。
providers列表,每项是字符串或对象,对象见 groups[].providers[]必填成员上游的名字,不能是策略组。同一个上游在一个策略组中只出现一次。load-balance 组的成员可以写成 {name, weight}。
selected字符串—select 类型选中的成员。
balance_byweights | latency | health | latency-healthweightsload-balance 类型用:成员的权重再乘上什么。weights:不乘,只按权重。latency:越快的上游分得越多。health:越少失败的上游分得越多。latency-health:两者都看。其他类型只能是 weights。

默认类型为 fallback:单个使用者的机器上没有需要分散的负载。

load-balance 组的成员可以带权重,取值 1 到 100;只写名字的成员权重为 1。权重决定组内请求怎么分:写成 { name: anthropic, weight: 7 } 和 relay 时,每十个请求有七个由官方 API 服务。进行中的对话留在回答它的那一家(见下文),也算进那一家的份额,因此份额靠新对话从哪一家开始来补齐。因失败处于冷却、并发数已满(max_concurrent)、或服务不了某个请求的上游不参与这一次分配,其余成员按各自的权重分。新的 WebSocket 连接也这样分配,算作一个请求;之后在这条连接上发的都交给它连上的那一家。其他类型的策略组不用权重。

字段类型默认值说明
name字符串必填上游的名字。只写名字的成员权重为 1。
weight整数1成员在 load-balance 组中分到的请求份额,与其他成员的权重成比例。取值 1 到 100。其他类型的策略组只能写 1。
groups:
  - name: pool
    type: load-balance
    providers:
      - { name: anthropic, weight: 7 }
      - relay

无论哪种类型,一段对话都留在上次回答它的那一家上游,让上游缓存着的那部分被再次读取,而不是换一家全价重算。同一轮之内(客户端正在回传工具结果)一律不换;跨轮时,上一次回答读或写了至少 1024 个 token 的 prompt cache、且距今不到五分钟,才继续留下。上游因失败进入冷却时,对话随之放开;故障转移之后接下回答的那一家,就是之后留下的那一家。一轮开始时命中的规则也沿用到这一轮结束:按输入大小或图片分流的规则不会让一轮半路换家,除非输入已经超出规则所指模型的上下文窗口。因此 load-balance 的权重是长期看各家分到的请求的比例:进行中的对话留在原来的上游,也算进那一家的份额。

balance_by 让 load-balance 组再看各上游最近的表现:每个成员的权重乘上一个系数,组内请求按乘出来的结果照上文的方式分。

  • weights(默认):只按权重。
  • latency:越快的上游分得越多。快慢看典型的从发出请求到回答第一段内容的时间,与 url-test 使用同一份测量。比组内居中者快一倍的上游,权重乘以四;最多乘以十,最少乘以十分之一。
  • health:越少失败的上游分得越多。依据是最近 30 分钟内的最近 50 次请求:服务器错误、限流、额度或余额用尽、凭据被拒、超时和连接失败算作失败;请求本身导致的错误不算,客户端取消、因开头太慢而换走、因并发数满了(max_concurrent)而跳过也不算。经常失败的上游至少保留权重的二十分之一,仍会偶尔分到请求,以便发现它已经恢复;完全失败的上游照旧由 failover 暂停。
  • latency-health:两个系数相乘。

快慢只在流式回答上测,从请求发给这家上游的那一刻算起:之前的等待、之前失败的上游都不算在内。因开头太慢而被放弃的上游(failover.next_on_slow_start),按等满的那段时间计。Responses 的 WebSocket 连接上,每个 response.create 在快慢和成败上都算一个请求,快慢从上游开始回答它的那一刻算起。

测量还不够的上游按中等对待。与只按权重时一样,进行中的对话留在原来的上游,差额由新对话补齐。

groups:
  - name: 均摊
    type: load-balance
    balance_by: latency-health
    providers:
      - { name: 官方, weight: 3 }
      - 中转

routes#

一条路由是一组自上而下求值的规则。每把密钥使用其 route 指定的路由;没有指定时用 default_route;再没有时用名为 default 的路由;一条路由都没有时,请求按上游的声明顺序故障转移。

字段类型默认值说明
name字符串必填路由的名字,不能重复。default 是密钥默认使用的那条。
rules对象列表,见 routes[].rules[][]自上而下求值;第一条匹配且带有 to 或 deny 的规则决定请求去向。

routes[].rules#

字段类型默认值说明
name字符串必填日志和流量详情中显示的名字。
when对象,见 routes[].rules[].when—条件,须全部满足。不写:匹配所有请求。
to字符串,或对象列表,见 routes[].rules[].to[]—上游或策略组的名字;__all__ 表示按声明顺序的全部上游。也可以指定模型:列出上游及发给它的模型,按顺序备用。不能与 when.provider_would_be 同时写。
set对象,见 routes[].rules[].set—改写请求参数。从所有匹配的规则累积,不只第一条。
deny字符串—以这句原因拒绝请求。

routes[].rules[].to#

to 写上游或策略组的名称,或者指定模型:一组上游,每家写明发给它的模型,按顺序备用。指定模型原样发出,不经过别名,也不受 set.model 影响;所以即使同名别名指向别处,规则仍能把某把密钥的请求发到某家上游的这个模型。

routes:
  - name: default
    rules:
      - name: Opus 走 Bedrock
        when: { model: claude-opus-5 }
        to:
          - { provider: bedrock, model: us.anthropic.claude-opus-5-v1:0 }
          - { provider: anthropic, model: claude-opus-5 }

字段类型默认值说明
provider字符串必填上游的名字,不能是策略组。同一个上游在列表中只出现一次。
model字符串必填发给这个上游的模型名,原样发出:不经过别名,也不受 set.model 改写。同一条规则里不能再写 set.model。

routes[].rules[].when#

字段类型默认值说明
model字符串—请求的模型,可用通配(claude-opus-*)。写上游模型名,也匹配请求列有它的别名的请求;写别名只匹配请求这个别名的。
client字符串—请求所用网关密钥的名字,精确匹配。
dialect字符串—客户端使用的接口格式:anthropic、openai-chat、openai-responses、gemini。
input_tokens比较式(>200k、<=4k、==3)—估算的输入 token 数。
max_tokens比较式(>200k、<=4k、==3)—请求中的 max_tokens。未写该参数的请求不匹配。
tool_count比较式(>200k、<=4k、==3)—请求中提供的工具数量。
cache布尔—请求是否使用 prompt cache。
tools布尔—请求是否带工具。
image布尔—请求是否包含图片。
thinking布尔—是否开启扩展思考。
stream布尔—是否流式返回。
intent字符串或字符串列表—客户端的辅助请求:assistant_internal 表示任意一类,也可以写具体的一类(titling)。在 client_probes 中设为 intercept 的类别由本地应答,不会进入路由。
provider_would_be字符串或字符串列表—路由选中的上游。这类规则在路由完成后求值,只能 set 或 deny,不能写 to。

比较式以 >、>=、<、<= 或 == 开头,数字可以带 k 或 m 后缀:">200k"、"<=4k"。不带运算符是错误,不当作相等:单写 "200k" 会被拒绝。

routes[].rules[].set#

set.model 可以写别名,每家上游收到各自对它的叫法。和别名一样,回答里的模型名写成客户端请求的名称。

字段类型默认值说明
model字符串—换成另一个模型发送。该会话的 prompt cache 随之失效。
max_tokens整数—替换 max_tokens。
thinking布尔—开启或关闭扩展思考。
only_at_session_start布尔false只在会话开始时应用。目前只记录和显示,尚未生效。
groups:
  - name: fast
    type: url-test
    providers: [anthropic, relay]

routes:
  - name: default
    rules:
      - name: 长上下文走官方
        when: { input_tokens: ">200k" }
        to: anthropic
      - name: 标题用便宜模型
        when: { intent: titling }
        set: { model: claude-haiku-4-5 }
      - name: 其余
        to: fast
default_route: default

plugins#

脚本插件在请求发往上游之前改写请求,在回答到达客户端之前改写回答。插件运行在 core 内部的沙箱中,无法访问文件、网络,也看不到密钥的真实值。插件由应用安装:代码写入本文件旁边的 plugins/<id>.js,批准过的代码另存一份在 plugins/.approved/<id>.js,代码的 SHA-256 写入 sha256。

插件的设置也在它自己的文件里。文件开头的 manifest 写着插件出错时怎么办(on_error)、处理哪些请求(match)和每个设置项的值(settings.<名称>.value)。在应用里改这几项时,应用只改写文件里的 manifest,并同时更新 sha256。本列表只记录插件本身、批准的哈希和是否启用。core 0.58 写下的条目里还有 on_error、scope 和 settings:这些字段不再生效,下一次在应用里改动插件时会被去掉。

只有文件的哈希与批准时一致,插件才会运行。磁盘上的文件被改动或删除后,插件会在几秒内停止运行,应用里会列出改动供审阅。批准之前,按批准过的文件里写的,插件覆盖的请求会被拒绝(on_error: "reject",默认),或者跳过这个插件照常发出(on_error: "skip")。加载失败的插件按同样的方式处理。两种情况都不影响配置其余部分生效。

插件按本列表的顺序运行。

match 里的 clients、models 和 upstreams 都是名字或通配(* 可以写在任意位置)的列表,不区分大小写;列表为空或不写表示全部。clients 是客户端应用(claude-code、codex 等),认不出应用的请求只有空列表才算在内。models 按发给上游的模型匹配:路由规则改了模型名的,按改名之后的匹配。upstreams 对请求和回答都适用。

插件在路由之后改写请求,请求每发往一个上游改写一次。故障转移到另一个上游时,从客户端发来的原样重新开始;插件看得到这一次发往哪个上游、用哪个模型名。路由、模型准入和会话归组看的都是客户端发来的原样。插件改了模型名,只是换掉发给这个上游的名字:不会重新路由,新的名字仍要在这把密钥可用的模型之内(clients[].allow),否则请求不发出。

插件处理它在代码里声明的那几种请求:对话(Anthropic Messages、OpenAI Chat Completions 和 Responses、Gemini,连同它们的数 token 和压缩)、嵌入(/v1/embeddings、Gemini 的 :embedContent 和 :batchEmbedContents)和旧版补全(/v1/completions)。没有声明的插件只处理对话。插件不处理的那种请求不经过它,不论 on_error 怎么设。其他接口(图片、音频等)不经过任何插件。

字段类型默认值说明
id字符串必填小写字母、数字和连字符,1 到 40 个字符,不能重复。
file字符串必填插件的代码,相对本文件所在的目录。只能是 plugins/<id>.js,由应用写入。
sha256字符串必填批准过的代码的 SHA-256,64 个小写十六进制字符。文件的哈希与它不符时插件停止运行,直到在应用里批准这次改动。批准过的代码另存在 plugins/.approved/<id>.js。
enabled布尔true是否运行这个插件。false:插件保留,不参与任何请求。
plugins:
  - id: add-date
    file: plugins/add-date.js
    sha256: 9f2b6c0e4a1d8f3b7c5e2a9d6f1b4c8e3a7d0f5b2c9e6a1d4f8b3c7e0a5d2f9b
    enabled: true

plugins/add-date.js 的开头:

export const manifest = {
  name: "Add date",
  api: 1,
  permissions: ["system"],
  match: { clients: ["claude-code"], models: ["claude-*"], upstreams: [] },
  on_error: "reject",
  settings: {
    note: { type: "string", label: "Note", value: "用中文回答。" },
  },
};

环境变量#

变量作用
THINKWATCH_HOME数据目录,替代 ~/.thinkwatch(Windows 上为 %APPDATA%\ThinkWatch)。
TWCORE_LOG日志过滤,tracing 语法(info、debug、tw_gateway=debug)。
HTTPS_PROXY、HTTP_PROXY、ALL_PROXY、NO_PROXY供 proxy: system 的上游使用。
其他配置中写 ${NAME} 的位置读取。

本页取自 ThinkWatch Core 仓库 docs/config.zh-CN.md (v0.64.0)。

最后更新于 2026-10-05 在 GitHub 上编辑