# `CMDC.Plugin.Builtin.ModelRouter`
[🔗](https://github.com/tupleyun/cmdc/blob/v0.5.3/lib/cmdc/plugin/builtin/model_router.ex#L1)

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 请求前触发。

# `condition`

```elixir
@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`

```elixir
@type rule() :: %{
  :condition =&gt; condition(),
  :model =&gt; String.t(),
  optional(:reason) =&gt; String.t()
}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
