跳转到内容

配置参考

Secret Guard 通过一个 TOML 文件启动 (默认 secret-guard.toml,可用 --config 改路径), 另有一个程序自动写回的状态文件 (secret-guard.state.toml,WebUI 编辑结果)。两者分工:

文件 角色 谁写
secret-guard.toml 声明式配置 (本文档的主角),进程内只读,改动需重启 用户手写
secret-guard.state.toml WebUI 编辑结果 + 对声明式条目的覆盖决策,删除即重置 程序自动

同一实体可同时有 static 与 dynamic 来源,后者可覆盖前者。覆盖模式 (OverrideMode): Default (dynamic 作为增量覆盖) / PreferStatic (以 static 为准) / Disabled (禁用条目)。

配置文件可以只写需要的部分 — 所有 section 都可缺省,缺省条目为空、数值取下表默认值。

[[providers]]
id = "openai-main"
kind = "direct"
api_key = "sk-your-upstream-key"
[[providers.endpoints]]
protocol = "openai"
base_url = "https://api.openai.com"
[[secrets.entries]]
id = "my-github-token"
value = "ghp_0123456789abcdefghijklmnopqrstuvwxyz"

[server] — 监听与转发 (改动需重启)

Section titled “[server] — 监听与转发 (改动需重启)”
字段 类型 默认 说明
host string "127.0.0.1" 监听地址。默认只监听本机回环,防止意外暴露到局域网。
port u16 8787 监听端口。也可用 --port / 环境变量 SG_PORT 覆盖。
records_capacity usize 1024 内存中保留的转发记录条数上限 (超出按 FIFO 淘汰)。
upstream_connect_timeout_secs u64 15 连上游的 TCP+TLS 握手超时 (秒)。0 = 不限时。
upstream_response_header_timeout_secs u64 60 等上游响应头到达的超时 (秒),流式请求档: 只对显式 stream: true 的请求生效 (流式响应头在首 token 生成后即返回,60s 覆盖大上下文 prefill)。超时返回 504。0 = 不限时。
upstream_nonstream_response_header_timeout_secs u64 300 等上游响应头到达的超时 (秒),非流式请求档: 对其余所有请求生效 (缺 stream 字段也算非流式)。非流式响应头要等整个响应生成完才返回,大上下文 (几十 k token) 下总时长轻松超 60s,默认放宽到 300s。超时返回 504。0 = 不限时。
upstream_stream_idle_timeout_secs u64 120 流式响应两个 chunk 之间的最大空闲 (秒)。0 = 不限时。

[redact] — 脱敏行为 (改动需重启)

Section titled “[redact] — 脱敏行为 (改动需重启)”
字段 类型 默认 说明
global_mock_prefix string "" 所有 Auto 模式 Mock 的统一前缀 (如 "sgm_"),便于在日志 / WebUI 中一眼认出 Mock。设定后,Secret 真值不允许包含该前缀。
on_probe_exhausted "fail_open" | "fail_closed" "fail_open" Mock 候选探测耗尽时 (极罕见,需对抗性构造) 的策略: fail_open 跳过该 Secret 照常转发;fail_closed 拒绝转发整个请求 (503),宁可失败也不泄露。

[usage] — 模型用量统计 (改动需重启)

Section titled “[usage] — 模型用量统计 (改动需重启)”

token 用量数据 100% 来自上游响应回显的 usage 字段 (零本地估算);成本按 models.dev 价目表估算 (恒为估算,无价模型显示 “—”)。 明细持久化在与配置同目录的 <stem>.usage.jsonl (append-only,每请求一行)。

字段 类型 默认 说明
enabled bool true 总开关;false = 不采集不落盘 (零开销)。
retention_days u32 90 明细保留天数;0 = 永久。启动时清理过期行。
pricing_url string models.dev api.json 定价数据源,可指向自托管镜像。
pricing_refresh_secs u64 86400 定价表刷新间隔 (秒)。
pricing_override model → price 表 空 自定义模型定价 (优先于 models.dev),见下例。

定价覆盖示例 ($ / 1M tokens; cache_read / cache_write 可省略 — 回退到 input 价 / 1.25×input):

[usage.pricing_override."my-relay/gpt-fork"]
input = 0.5
output = 2.0

已知限制: OpenAI 流式请求仅在客户端设置 stream_options.include_usage = true 时上游才回显 usage — 此类请求在 Usage 页只计请求数,无 token 统计 (页面有提示)。

[[providers]] — 上游 LLM 端点 (平铺数组)

Section titled “[[providers]] — 上游 LLM 端点 (平铺数组)”

每个 Provider 是两种构造之一: 直连 (Direct,真实上游端点) 或 路由 (Router,按请求 model 分流的路由端点)。由 kind 字段判别 ("direct" / "router",必填 — 缺失时启动 fail-fast),对应构造的字段写在同一个 [[providers]] 表内。构造不匹配的字段会被静默忽略: Router 条目写 base_url/api_key、Direct 条目 写 routes 均不生效 — 请勿依赖,配置以 kind 为准。

共享字段 (两种构造均可配):

字段 类型 默认 说明
id string (必填) 唯一标识。1..=64 字符,以字母/数字开头,只允许字母数字、_、-。出现在转发 URL 中 (/o/<id>/...)。
kind string (必填) 构造判别: "direct" (直连) 或 "router" (路由)。
enabled bool true false 时转发到该 Provider 返回 503。
name string — 可选的人类可读名称 (仅 WebUI 显示)。

Direct 构造 (kind = "direct"):

字段 类型 默认 说明
protocol string (必填) 上游协议: openai / anthropic / openairesponses (OpenAI Responses API);gemini / ollama 为实验性透传 (无 codec — Secret Redact 不生效,配置 secrets 时这两族的请求默认被拒绝,见 协议支持)。
base_url string (必填) 上游 base URL。必须以 http:// 或 https:// 开头,末尾不带 / (路径由 Secret Guard 拼接)。如 https://api.openai.com。
api_key string "" 上游 API key 明文。与 api_key_file 互斥 (同时设置启动报错)。
api_key_file path — 从文件读 API key (适合 sops-nix / systemd LoadCredential 等外部注入,让 toml 本身不含敏感数据)。每次请求时读取,读不到按空 key 处理并打 WARN。文件内容自动去首尾空白。

Router 构造 (kind = "router") — 无 protocol / base_url / api_key 字段:

Router 自身不转发,按请求 model 匹配 routes 路由后链式解析到链尾的 Direct Provider。 ingress 协议由请求 URL 决定,egress 协议由链尾实体决定 (不同则自动跨协议翻译) — 因此 Router 没有 protocol 字段 (残留字段会被静默忽略)。

路由写在 [[providers.routes]] 子表数组 (至少一条,空表启动报错):

字段 类型 默认 说明
model_pattern string (必填) model 名通配符: 仅 * 是元字符 (匹配任意串,含空串),其余字符字面匹配,大小写敏感。"*" 匹配一切。非空,≤64 字符。
target string (必填) 目标 Provider id,可指向另一 Router (链式解析到链尾实体)。悬空目标 (不存在) 写入放行,请求时 503。启用路由自环启动报错 (禁用路由不构成环检查的边);跨条目环在 WebUI 写入时拒绝 (手改配置产生的环在请求时返回 503,不挂起)。
upstream_model string — (透传) 出站 model 重写: 命中该路由的请求 body 顶层 model 字段被替换为该值。省略 = 透传客户端原 model。链上多跳重写是 pipeline 语义: 改写值参与下一跳 Router 的路由匹配,后者覆盖前者。非空 / 非纯空白 / ≤128 字符 (清空请省略字段)。代价: 重写生效时该请求放弃字节直传 (上游前缀缓存失效);Gemini/Ollama 无 codec 无法改写: WARN + 原样透传。
priority i64 — (禁用) 优先级,越大越优先。省略 (null) = 该路由禁用 (不参与匹配,也不构成环检查的边)。同优先级按列表出现顺序取先者。
[[providers]]
id = "anthropic-main"
kind = "direct"
api_key_file = "/run/credentials/anthropic.key" # 与 api_key 二选一; 同为条目级字段, 须在 [[providers.endpoints]] 之前
[[providers.endpoints]]
protocol = "anthropic"
base_url = "https://api.anthropic.com"

路由 endpoint (routes) — 按请求 model 的路由转发

Section titled “路由 endpoint (routes) — 按请求 model 的路由转发”

客户端固定连接路由 endpoint 的 URL (如 /o/my-model/v1/chat/completions), 实际打到哪个上游由请求 body 里的 model 名决定 — dispatch 在请求 body 收集后 提取顶层 model,按路由匹配转发:

[[providers]]
id = "openai-main"
kind = "direct"
api_key = "sk-..."
[[providers.endpoints]]
protocol = "openai"
base_url = "https://api.openai.com"
[[providers]]
id = "anthropic-main"
kind = "direct"
api_key_file = "/run/credentials/anthropic.key"
[[providers.endpoints]]
protocol = "anthropic"
base_url = "https://api.anthropic.com"
[[providers]]
id = "my-model"
kind = "router"
[[providers.routes]]
model_pattern = "claude-*" # claude 系模型走 Anthropic, 并重写为固定版本
target = "anthropic-main"
upstream_model = "claude-sonnet-4"
priority = 100
[[providers.routes]]
model_pattern = "*" # 兜底: 其余 (含无 model 字段的请求) 走 OpenAI
target = "openai-main"
priority = 0

语义要点:

  • 路由选择: 启用 (priority 非 null) 且 model_pattern 匹配请求 model 的路由中 priority 最大者;同优先级按列表出现顺序取先者。请求 model 取 JSON 顶层 string model 字段 — 非 JSON / 无该字段 / 非 string / 空 body 视为 "" (只有 "*" 类 model_pattern 能命中)。
  • model 重写是 pipeline: 命中路由的 upstream_model 改写立即生效 — 链上下一个 Router 按改写后的 model 匹配,多跳重写后者覆盖前者。适配 gpt-4o 与 claude 等跨模型切换。
  • 切换只影响新请求 (per-request 解析,body 收集后执行),in-flight 请求按已 解析目标完成。
  • 错误行为 (均 503): 无匹配路由 / 链上目标悬空 / 目标 enabled = false / 成环。启用路由自环启动即报错 (禁用路由不构成环边),跨条目环在 WebUI 写入时拒绝。
  • 目标协议与 URL 协议不同时自动走跨协议翻译 (非流式;流式跨协议返回 501, 与直连行为一致)。
  • WebUI 覆盖静态 Router 的方式 = 提交完整的 routes 数组 (详见 WebUI 指南)。

[[secrets.entries]] — 需要保护的 Secret (嵌套在 [secrets] 下)

Section titled “[[secrets.entries]] — 需要保护的 Secret (嵌套在 [secrets] 下)”
字段 类型 默认 说明
id string (必填) 唯一标识,规则同 Provider id。
name string — 可选的人类可读名称 (仅 WebUI 显示)。
category string "apikey" 类别: password / apikey / token / cookie / privatekey / other。
value string "" Secret 明文。最少 3 字节;不允许包含 global_mock_prefix;不允许包含 Unicode 私用区字符 (U+E000..U+F8FF)。与 value_file 互斥。
value_file path — 启动时一次性从文件读取 (适合 sops-nix 等外部注入),读取失败会拒绝启动 (fail-fast: Secret 缺失会让保护静默失效,必须在部署时暴露)。文件内容自动去首尾空白。
mock_strategy table (见下) 替身 (Mock) 的生成策略。缺省时自动按真值推断: 等长、同字符集的确定性随机值。
[[secrets.entries]]
id = "my-github-token"
value = "ghp_0123456789abcdefghijklmnopqrstuvwxyz"
[secrets.entries.mock_strategy]
# 维度一: 初始值. 缺省 "auto" (系统生成).
# 固定替身写法:
# initial = { kind = "fixed", value = "ghp_FAKEplaceholder" }
# 维度二: 生成策略 (仅 initial = "auto" 时生效). 缺省按真值推断.
[secrets.entries.mock_strategy.gen]
prefix = "ghp_mock_" # 固定前缀
length_range = [24, 36] # [最小, 最大] 字符数 (闭区间)
[secrets.entries.mock_strategy.gen.charset]
lowercase = true
digits = true
# 其他开关: uppercase / underscore / hyphen / other = ['$', '#']

约束: length_range 两个值都须大于 0 且 min 不大于 max;prefix 长度不能超过 min; charset 至少开启一类。固定替身不能等于真值,也不能包含真值的长子串 (启动时校验)。

[auth] — 认证 (默认关闭 = 单用户模式)

Section titled “[auth] — 认证 (默认关闭 = 单用户模式)”

默认 enabled = false: 所有路由无认证,适合个人本机使用。需要多用户 / 暴露给 局域网时启用 OIDC 登录 + API key:

字段 类型 默认 说明
enabled bool false true 时: WebUI 需 OIDC 登录,转发路径需 API key。
oidc table — OIDC Provider 配置 (enabled = true 时必填)。
api_keys array [] 静态预设 API key (也可在 WebUI 的 API Keys 页签发)。

[auth.oidc] 字段:

字段 类型 说明
issuer_url string OIDC issuer (如 https://sso.example.com/realms/main)。
client_id string OAuth2 client id。
client_secret_file path 可选,client secret 文件路径。
redirect_url string 可选,回调 URL。只能换 scheme/host/port,path 固定为 /oauth2/callback。监听地址非浏览器可达时 (如反代域名) 必须配置。

[[auth.api_keys]] 字段: label (唯一标识),key 或 key_file (二选一,语义同上)。

[auth]
enabled = true
[auth.oidc]
issuer_url = "https://sso.example.com/realms/main"
client_id = "secret-guard"
[[auth.api_keys]]
label = "ci-pipeline"
key = "sg_ci_abc123..."

secret-guard run 启动网关。CLI / 环境变量优先于配置文件:

参数 环境变量 默认 说明
--host SG_HOST 回退 [server] host (127.0.0.1) 监听地址。
--port SG_PORT 回退 [server] port (8787) 监听端口。
-c, --config SG_CONFIG secret-guard.toml 声明式配置文件路径。
--state SG_STATE 由 config 路径派生 状态文件路径 (secret-guard.toml → secret-guard.state.toml)。
症状 原因与解法
``TOML parse error … [[providers]] missing field kindvalue` 旧版 kind = "virtual" 已删除: 改为 kind = "router",原 route_to 指向改写为一条 [[providers.routes]] (model_pattern = "*" + target = ...),原 model_override 改为路由的 upstream_model 字段。
启动 WARN unknown field route_to / unknown field model_override 这两个 providers 顶层字段已删除 (残留字段告警,不生效): 改写为 [[providers.routes]] 子表 (见上文 Router 构造)。
TOML parse error ... [[secrets]] invalid type: map, expected a sequence Secret 必须写 [[secrets.entries]],不能写 [[secrets]]。报错前会先输出一行 WARN did you mean [[secrets.entries]]? 指路。
启动 WARN unknown field ... did you mean ...? 字段拼写错误。可选字段拼错会被忽略不生效 (仅 WARN 提示),“以为配了实际没配”,须修正;必填字段 (如 protocol) 拼错则 WARN 后再报 missing field 启动失败。
base_url must not end with '/' 去掉末尾 /,路径由 Secret Guard 拼接。
secret value too short (min 3 bytes) Secret 真值至少 3 字节。
key and key_file are mutually exclusive / api_key 与 api_key_file 同时设置 二选一,删掉其中一个。
provider ... must declare at least one route Router 构造的 routes 为空 — 空路由 Router 无法转发任何请求,启动即拒。至少配一条 (兜底可用 model_pattern = "*")。
请求返回 404 not_found URL 里 Provider id 不存在,或 proto 前缀拼错 (见 快速上手 — 路由约定)。
请求返回 503 unavailable Provider enabled = false,或路由坏链: message 含 router provider '...' has no route matching model '...' (无匹配路由 — 检查 model_pattern 与 priority) / route target '...' not found (悬空目标) / route target '...' is disabled / route cycle detected (手改配置产生的环)。

配置文件缺失时进程仍可启动 (空配置 + 默认值,但没有任何 Provider 可转发,启动日志有 WARN)。