P2 模型路由插件 — 按规则在 before_request 自动切换 LLM 模型。
v0.1 支持 3 个条件(:turn_gt / :cost_gt / :tokens_gt),v0.2 新增 4 个
业务友好的条件(:token_budget_lt / :task_complexity / :time_of_day_in /
:user_tier),并从 :intervene 系统消息改为更干净的 :switch_model
Plugin action(RFC C8)。
配置
{CMDC.Plugin.Builtin.ModelRouter,
default_model: "anthropic:claude-sonnet-4-5",
rules: [
# 成本托底
%{condition: {:cost_gt, 0.5}, model: "openai:gpt-4.1-mini"},
# token 快耗尽时降级
%{condition: {:token_budget_lt, 5_000}, model: "openai:gpt-4.1-mini"},
# 复杂任务用最强模型
%{condition: {:task_complexity, :complex}, model: "anthropic:claude-opus-4"},
%{condition: {:task_complexity_in, [:simple]}, model: "openai:gpt-4.1-mini"},
# 夜间(22:00-06:59 UTC)跑经济模型
%{condition: {:time_of_day_in, [22..23, 0..6]}, model: "openai:gpt-4.1-mini"},
# 免费用户强制降级
%{condition: {:user_tier, :free}, model: "openai:gpt-4.1-mini"},
%{condition: {:user_tier_in, [:pro, :enterprise]}, model: "anthropic:claude-sonnet-4-5"},
# 通用 user_data 断言
%{condition: {:user_data, :region, "eu-west"}, model: "mistral:mistral-large"},
%{condition: {:user_data, :priority, :gt, 5}, model: "anthropic:claude-opus-4"}
]
}规则条件一览
运行时:
{:turn_gt, n}— 对话轮次超过 n{:cost_gt, usd}— 累计成本超过 usd{:tokens_gt, n}— 累计 token 超过 n
v0.2 新增:
{:token_budget_lt, n}—user_data[:token_budget] - total_tokens < n(没配 budget 时视为不触发){:task_complexity, v}/{:task_complexity_in, [v, ...]}— 读user_data[:task_complexity](:simple/:normal/:complex){:time_of_day_in, ranges}— 当前 UTC 小时落在任一0..23Range 里{:user_tier, tier}/{:user_tier_in, [tier, ...]}— 读user_data[:user_tier](:free/:pro/:enterprise/ 任意原子){:user_data, key, value}—user_data[key] == value{:user_data, key, op, value}—op为:eq/:gt/:lt/:gte/:lte
规则匹配顺序
从上到下顺序匹配,命中第一条即 :switch_model,不再尝试后续规则。
把更严格 / 更具体的条件写在前面。
Action
- 命中且目标模型与当前不同 →
{:switch_model, model, state}Pipeline 汇总后把下一次 LLM 请求切换到新模型,并发:model_switched事件 - 无命中 / 目标相同 →
:continue
触发 Hook
{:before_request, messages} — 发送 LLM 请求前触发。
Summary
Types
@type condition() :: {:turn_gt, non_neg_integer()} | {:cost_gt, number()} | {:tokens_gt, non_neg_integer()} | {:token_budget_lt, non_neg_integer()} | {:task_complexity, atom()} | {:task_complexity_in, [atom()]} | {:time_of_day_in, [Range.t()]} | {:user_tier, atom()} | {:user_tier_in, [atom()]} | {:user_data, atom(), any()} | {:user_data, atom(), :eq | :gt | :lt | :gte | :lte, any()}