route.py python
161 lines 5.3 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 9 hours ago
1 """``overseer route`` command (§PR.6)."""
2
3 from __future__ import annotations
4
5 from argparse import Namespace
6 from pathlib import Path
7
8 from adapters.config import OverseerConfig, load_config
9 from adapters.errors import ConfigError
10 from cli.context import CliContext
11 from cli.paths import PathEscapeError, confine_path, is_within_repo, resolve_config_path, resolve_repo_root
12 from cli.sanitize import config_exit_code, format_config_error
13 from tools.cost_awareness.derive import derive_cost_view
14 from tools.model_routing.labels import RoutingPolicyError, load_model_tier_cost_bands
15 from tools.model_routing.policy import load_routing_policy, validate_routing_policy
16 from tools.model_routing.resolve import resolve_route
17 from tools.model_routing.types import RouteSelector
18
19
20 def _resolve_policy_path(config: OverseerConfig, repo_root: Path) -> Path:
21 """Resolve configured policy path confined to repo root."""
22 return confine_path(repo_root, config.model_routing.policy)
23
24
25 def run_route_command(args: Namespace, ctx: CliContext) -> int:
26 """Execute ``overseer route`` (read-only; no model call, no network)."""
27 repo_root = resolve_repo_root(cwd=ctx.cwd, repo_arg=args.repo, command="route")
28 overseer_dir = repo_root / ".overseer"
29 if not overseer_dir.is_dir():
30 ctx.output.error("not initialized — run ok init first")
31 return 1
32
33 config_path = resolve_config_path(repo_root, args.config)
34 if not is_within_repo(repo_root, config_path):
35 ctx.output.error("refused: config path outside repo root")
36 return 4
37
38 try:
39 config = load_config(config_path)
40 except ConfigError as exc:
41 ctx.output.error(format_config_error(exc, repo_root))
42 return config_exit_code(exc)
43
44 for warning in config.extension_warnings:
45 ctx.output.warn(warning)
46
47 try:
48 policy_path = _resolve_policy_path(config, repo_root)
49 except PathEscapeError:
50 ctx.output.error("refused: model_routing.policy path outside repo root")
51 return 4
52
53 if args.validate:
54 return _run_validate(config=config, policy_path=policy_path, kit_root=ctx.kit, ctx=ctx)
55
56 query = RouteSelector(
57 position=args.position,
58 phase_tier=args.phase_tier,
59 gate=args.gate,
60 )
61 return _run_resolve(
62 config=config,
63 policy_path=policy_path,
64 kit_root=ctx.kit,
65 query=query,
66 ctx=ctx,
67 )
68
69
70 def _run_validate(
71 *,
72 config: OverseerConfig,
73 policy_path: Path,
74 kit_root: Path,
75 ctx: CliContext,
76 ) -> int:
77 result = validate_routing_policy(policy_path, kit_root=kit_root)
78 payload = {
79 "valid": result.valid,
80 "policy": config.model_routing.policy,
81 "violation": result.violation,
82 }
83 if ctx.output.json_mode:
84 ctx.output.emit_json(payload)
85 elif result.valid:
86 ctx.output.emit("routing policy: valid")
87 else:
88 ctx.output.error(result.violation or "routing policy invalid")
89 if result.valid:
90 return 0
91 if result.violation and "missing or unreadable" in result.violation:
92 return 31
93 return 30
94
95
96 def _run_resolve(
97 *,
98 config: OverseerConfig,
99 policy_path: Path,
100 kit_root: Path,
101 query: RouteSelector,
102 ctx: CliContext,
103 ) -> int:
104 try:
105 policy = load_routing_policy(policy_path, kit_root=kit_root)
106 except RoutingPolicyError as exc:
107 ctx.output.error(exc.message)
108 return exc.exit_code
109
110 try:
111 cost_bands = load_model_tier_cost_bands(kit_root, fail_closed=True)
112 except RoutingPolicyError as exc:
113 ctx.output.error(exc.message)
114 return exc.exit_code
115
116 decision = resolve_route(policy, query)
117 cost_class, paid_step_before_spend = derive_cost_view(decision.model_tier, cost_bands)
118 payload = {
119 "route_id": decision.route_id,
120 "model_tier": decision.model_tier,
121 "fallback": list(decision.fallback),
122 "query": {
123 "position": query.position,
124 "phase_tier": query.phase_tier,
125 "gate": query.gate,
126 },
127 "policy": config.model_routing.policy,
128 "cost_class": cost_class,
129 "paid_step_before_spend": paid_step_before_spend,
130 }
131 if ctx.output.json_mode:
132 ctx.output.emit_json(payload)
133 else:
134 ctx.output.emit(f"route_id: {decision.route_id}")
135 ctx.output.emit(f"model_tier: {decision.model_tier}")
136 ctx.output.emit(f"fallback: {', '.join(decision.fallback)}")
137 ctx.output.emit(f"cost_class: {cost_class}")
138 ctx.output.emit(f"paid_step_before_spend: {paid_step_before_spend}")
139 return 0
140
141
142 def routing_policy_status(config: OverseerConfig, repo_root: Path, *, kit_root: Path) -> dict:
143 """Read-only routing-policy validity for ``overseer status`` when enabled."""
144 if not config.model_routing.enabled:
145 return {"enabled": False}
146 try:
147 policy_path = _resolve_policy_path(config, repo_root)
148 except PathEscapeError:
149 return {
150 "enabled": True,
151 "policy": config.model_routing.policy,
152 "valid": False,
153 "violation": "model_routing.policy path escapes repo root",
154 }
155 result = validate_routing_policy(policy_path, kit_root=kit_root)
156 return {
157 "enabled": True,
158 "policy": config.model_routing.policy,
159 "valid": result.valid,
160 "violation": result.violation,
161 }
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 9 hours ago