type = "composite" 把裁判与阶段路由器结合起来。由裁判
决定阶段路由器回落到哪个档位,而信号评分本身不受影响:信号能定夺的回合由信号定夺,信号
无法定夺的回合落在裁判挑选的档位上,而不是固定的 picker。
如何配置
[[models]]
id = "claude-composite"
[models.router]
type = "composite"
[models.router.classifier]
target = "claude-haiku-4-5"
base_threshold = 0.5
classify_trigger = "user_turn"
[models.router.stage]
capable_target = "claude-opus-4-8"
efficient_target = "claude-sonnet-4-6"
confidence_threshold = 0.5
# recent_turn_window = 3
# capable_hold_turns = 0stage 表不接受 picker —— 那个档位由 classifier 提供 —— 所以在这里写 picker 会导致
启动错误。classifier.base_threshold 是仍然路由到高效档位的 p_solve 下限。
classifier.classify_trigger 是必填的,它决定档位多久重新挑一次:
user_turn在每一个人类用户回合重新挑一次档位。new_session只挑一次并保持。every_request在这里会被拒绝 —— 每个工具步骤都调一次裁判的代价,正是这个类型要 避免的。
classifier.message_hash_fallback 为不发送会话 id 的客户端改用第一条用户消息的哈希来保留
档位。stage.tool_semantics 接受与阶段路由器的
[models.router.tool_semantics] 相同的四张列表,
规则也相同。六个每次调用的上限放在
[models.router] 上,而不是放进两张子表里。
够不到裁判时
裁判够不到的回合回落到 stage.efficient_target,这既是上游的规则,也是没有请求体的那些接口
所报告的值。裁判调用被 max_judge_calls 拒绝的回合,如果会话有上一次保留的档位就保持该
档位,否则取 picker 的默认档位,因此档位不会在用户回合与其后的工具续接回合之间变动。不需要
裁判调用的回合不会被拒绝。
count_tokens 探测不给信号打分,也不调用裁判:会话有保留的档位就由该档位作答,否则由
stage.efficient_target 作答。
客户端看到什么
因为 stage 那一半就是 libsy 自己的 stage 路由,它定夺下来的回合仍然报告阶段路由器自己的
路由来源,而不是报告成 classifier 的决策 —— 也就是说,composite 里由信号驱动的回合,读起来
和一个普通 stage_router 的回合完全一样(见
客户端看到什么)。
信号无法定夺的一轮会 fall-open 到某个档位,其来源说明该档位从何而来:这一轮的裁判调用
设定了档位时是 llm-classifier,沿用之前某一轮保留的档位时是 classifier_retained。
没有结论定夺这一轮时 —— 够不到裁判,或 max_judge_calls 拒绝了调用 —— 来源是
classifier_fail_open,档位则如够不到裁判时所述。