feat: rule routing v0 for model selection (shadow/active, fail-closed)
Pre-dispatch routing only fills in tasks with no explicit model, classified by structural signals only (plan > goal > default). Explicit task model and worker CLI --model win over policy; policy via CLOUD_ROUTING_POLICY_JSON/FILE + CLOUD_ROUTING_MODE. Shadow mode records a suggestion without changing dispatch. Only policy/shadow decisions emit a model_routing event, so the existing events contract (fallback tests) is unchanged. Fallback rules, effort/approval/sandbox/quota and provider are untouched. Tests: test_models_routing.py (20 tests).
This commit is contained in:
1 parent
e6e1e4b4f3
commit
e364970b46
3 files changed
+654
-3
No files matched your search
+48
-2
@@ -146,7 +146,9 @@ from cloud.cost import CostStore, estimate_tokens, price_usd_micros, \
|
||||
from cloud.db import connect as _db_connect
|
||||
from cloud.goal_command import parse_goal_command as _parse_goal_command
|
||||
from cloud.logging_config import get_logger, log_event, setup_logging
|
||||
from cloud.models import ModelRouter, is_fallbackable, record_fallback
|
||||
from cloud.models import ModelRouter, RoutingPolicy, decide_routing, \
|
||||
is_fallbackable, load_routing_policy, parse_routing_policy, \
|
||||
record_fallback, record_routing
|
||||
from cloud.auto_memory import AutoMemoryStore
|
||||
from cloud.auto_memory import build_injection_prefix as _auto_prefix
|
||||
from cloud.auto_memory import process_due_tasks as _auto_process_due
|
||||
@@ -424,6 +426,7 @@ class Worker:
|
||||
def __init__(self, db_path, host_py_path, config_path=None, repo=None,
|
||||
runner=None, env=None, env_key=None,
|
||||
codex_config_path=None, model=None, router=None,
|
||||
model_explicit=None, routing_policy=None,
|
||||
max_turns=None, max_cost_usd=None, executor=None,
|
||||
cost_store=None, process_factory=None,
|
||||
task_timeout=1800, max_retries=1,
|
||||
@@ -472,9 +475,32 @@ class Worker:
|
||||
self._env = env if env is not None else os.environ
|
||||
self.env_key = resolve_env_key(env_key, codex_config_path)
|
||||
self.model = _resolve_model(model, config_path)
|
||||
# CLI --model 是否显式:未传参时按“是否传了 model 参数”推断,
|
||||
# 传了即显式(路由绝不覆盖已钉死的部署模型);调用方也可
|
||||
# 显式传 model_explicit=False,把 model 当作普通默认值。
|
||||
if model_explicit is None:
|
||||
self.model_explicit = model is not None
|
||||
else:
|
||||
self.model_explicit = bool(model_explicit)
|
||||
if router is not None and not isinstance(router, ModelRouter):
|
||||
raise ValueError("router 必须是 cloud.models.ModelRouter")
|
||||
self.router = router
|
||||
# 规则路由策略 v0:显式 RoutingPolicy > dict(解析)> 未传时
|
||||
# 从环境读(CLOUD_ROUTING_POLICY_*,见 models.py)。
|
||||
# 解析失败 / 未配时为 None,派发回落旧链,绝不因此拒绝任务。
|
||||
# 策略里的 fallback_models 在 v0 不启用真实重试(见 models.py),
|
||||
# 只有 CLI --fallback-models 能启用,策略文件 alone 不能烧钱。
|
||||
if isinstance(routing_policy, RoutingPolicy):
|
||||
self.routing_policy = routing_policy
|
||||
elif isinstance(routing_policy, dict):
|
||||
self.routing_policy = parse_routing_policy(routing_policy)
|
||||
elif routing_policy is None:
|
||||
try:
|
||||
self.routing_policy = load_routing_policy(self._env)
|
||||
except Exception:
|
||||
self.routing_policy = None
|
||||
else:
|
||||
raise ValueError("routing_policy 必须是 RoutingPolicy 或 dict")
|
||||
# 真实 fallback 换模型列表:默认空 = 关闭,只记事件不换模型
|
||||
# (老行为,生产 worker 未配该参数时绝不突然多花模型钱)。
|
||||
# 归一化:去空白、去重保序、去掉与主模型相同的项(换成同一
|
||||
@@ -5817,7 +5843,27 @@ class Worker:
|
||||
|
||||
task = dict(task)
|
||||
if not task.get("model"):
|
||||
task["model"] = self.model
|
||||
# 规则路由 v0(只在模型仍为空时跑一次,整个派发只算一次,
|
||||
# 后续重试/fallback/续跑都复用这里定下的 task["model"],
|
||||
# 不再重算;网关步已填的 sub2api 默认模型在此之前已生效,
|
||||
# 路由不覆盖它)。显式任务模型与 worker CLI 显式模型
|
||||
# 在 decide_routing 内同样优先,见 models.py 的优先级。
|
||||
decision = decide_routing(
|
||||
task, self.model, policy=self.routing_policy,
|
||||
worker_model_explicit=self.model_explicit)
|
||||
# 只在策略真正介入时记事件与审计:worker_default /
|
||||
# worker_explicit 就是旧行为,记一条 model_routing 会
|
||||
# 打破现有调用方对 events 的假设(例如 fallback 单测
|
||||
# 断言 events[0] 就是 fallback 事件、无事件时为空),
|
||||
# 而旧行为本身已由 task["model"] / actual_model 体现,
|
||||
# 不需要再用一条事件重复说明。
|
||||
if decision.source in ("policy", "shadow"):
|
||||
record_routing({"id": task_id, "events": events}, decision)
|
||||
self._audit(task["project_id"], "worker_routing",
|
||||
"%s tier=%s source=%s applied=%s" % (
|
||||
task_id, decision.tier, decision.source,
|
||||
decision.applied))
|
||||
task["model"] = decision.chosen_model or self.model
|
||||
actual_model = task["model"]
|
||||
# 有限重试与真实 fallback 的优先级:取消不重试;一次尝试失败
|
||||
# 且可 fallback、且显式配了 fallback_models 时,优先真换模型
|
||||
|
||||
Reference in new issue
Block a user