CMDC.Plugin.Builtin.ModelRouter (cmdc v0.5.3)

Copy Markdown View Source

P2 模型路由插件 — 按规则在 before_request 自动切换 LLM 模型。

通过 :switch_model Plugin action 干净地切换模型,不污染消息历史。

配置

{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

业务友好条件:

  • {: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..23 Range 里
  • {: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

condition()

@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()}

rule()

@type rule() :: %{
  :condition => condition(),
  :model => String.t(),
  optional(:reason) => String.t()
}