refactor: audit batch 3 (I-3 capability verdict, I-4 policy merge)
- I-3: new models.evaluate_capability returning a CapabilityVerdict (ok/missing/unknown from check, known/unknown_missing from split, status/verb derivations); decide_routing, worker gateway check, worker persisted recheck, dry-run tier+gateway checks and server shadow endpoint all call it. Blocking rules stay at the call sites (gateway has no worker-default fallback, recheck blocks only on known, dry-run/shadow never block); all user-visible audit details, dry-run notes and shadow notes are byte-identical. - I-4: new models.merge_policy_documents shared by load_routing_policy and dry_run_from_env; the old tiers-update block is kept verbatim (it was a no-op after update(): JSON tiers replace the whole map, file tiers absent from JSON are dropped -- preserved, not fixed, as that would be a behaviour change). dropped_tiers/partial_failures remain load-time records, not merge products. - Tests: test_audit_batch3.py, 13 tests (verdict matrix + parity with check/split, decide/dry-run wording, merge boundaries + 300 seeded random pairs + loader/dry-run agreement). Full suite: 1694 OK (skipped=11), baseline 1681 + 13, 0 failures.
This commit is contained in:
1 parent
a7f77c8a98
commit
06e4e16bbb
5 files changed
+483
-66
No files matched your search
+117
-17
@@ -289,6 +289,115 @@ def split_model_capability_missing(capabilities, model, requires):
|
|||||||
return sorted(known), sorted(unknown)
|
return sorted(known), sorted(unknown)
|
||||||
|
|
||||||
|
|
||||||
|
class CapabilityVerdict:
|
||||||
|
"""Structured result of one capability check (audit batch 3, I-3).
|
||||||
|
|
||||||
|
One call replaces the previous check+split pair at the five
|
||||||
|
capability sites (decide_routing, worker gateway check, worker
|
||||||
|
persisted-model recheck, dry-run tier/gateway checks, server
|
||||||
|
shadow endpoint). The fields are exactly what those sites used
|
||||||
|
before, so no site changes behaviour by adopting it:
|
||||||
|
|
||||||
|
- ``ok`` / ``missing`` / ``unknown`` are the total-check fields
|
||||||
|
from check_model_capabilities (decide/dry-run/shadow wording);
|
||||||
|
- ``known_missing`` / ``unknown_missing`` are the split fields
|
||||||
|
from split_model_capability_missing (gateway explicit split,
|
||||||
|
persisted recheck);
|
||||||
|
- ``status`` is "ok", or "unknown" when not ok and any part is
|
||||||
|
unverifiable, else "fail" (the dry-run/shadow status rule);
|
||||||
|
- ``verb`` is "cannot verify" when unknown else "lacks" (the
|
||||||
|
dry-run/gateway note wording); it is only meaningful when
|
||||||
|
not ok.
|
||||||
|
|
||||||
|
Edge preserved, not fixed: for a model absent from the
|
||||||
|
directory, ``missing`` lists every requires key (the total
|
||||||
|
check's rule) while ``unknown_missing`` lists only recognised
|
||||||
|
requirement keys (the split's rule). Callers that previously
|
||||||
|
used one function keep getting that function's values; the
|
||||||
|
blocking rules themselves stay at the call sites (gateway has
|
||||||
|
no worker-default fallback, recheck blocks only on known,
|
||||||
|
dry-run/shadow never block), they are not folded into this
|
||||||
|
verdict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ok, missing, unknown, known_missing,
|
||||||
|
unknown_missing):
|
||||||
|
self.ok = bool(ok)
|
||||||
|
self.missing = list(missing)
|
||||||
|
self.unknown = bool(unknown)
|
||||||
|
self.known_missing = list(known_missing)
|
||||||
|
self.unknown_missing = list(unknown_missing)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
if self.ok:
|
||||||
|
return "ok"
|
||||||
|
return "unknown" if self.unknown else "fail"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verb(self):
|
||||||
|
return "cannot verify" if self.unknown else "lacks"
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_capability(capabilities, model, requires):
|
||||||
|
"""Check one model against requires, returning a CapabilityVerdict.
|
||||||
|
|
||||||
|
Pure: it only calls check_model_capabilities and
|
||||||
|
split_model_capability_missing, so its fields are definitionally
|
||||||
|
identical to what the five call sites computed before with two
|
||||||
|
calls. ``requires`` is expected to be already cleaned by
|
||||||
|
parse_tier_requires (all five sites pass tier_requirements);
|
||||||
|
uncleaned keys behave exactly as the two underlying functions
|
||||||
|
behave (see CapabilityVerdict).
|
||||||
|
"""
|
||||||
|
ok, missing, unknown = check_model_capabilities(
|
||||||
|
capabilities, model, requires)
|
||||||
|
known, unknown_missing = split_model_capability_missing(
|
||||||
|
capabilities, model, requires)
|
||||||
|
return CapabilityVerdict(ok, missing, unknown, known,
|
||||||
|
unknown_missing)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_policy_documents(base, override):
|
||||||
|
"""Merge a file policy document with a JSON override document.
|
||||||
|
|
||||||
|
This is the merge load_routing_policy and dry_run_from_env
|
||||||
|
always shared line-for-line, now in one place (audit batch 3,
|
||||||
|
I-4). ``base`` is the file document (or None when unread/absent),
|
||||||
|
``override`` is the JSON document (or None when absent/unparsed).
|
||||||
|
Rules, exactly as before:
|
||||||
|
|
||||||
|
- override not a dict: base unchanged;
|
||||||
|
- base not a dict: override;
|
||||||
|
- otherwise: a shallow copy of base updated with override, so
|
||||||
|
JSON replaces whole keys — including the whole "tiers" map.
|
||||||
|
Per-tier merging does NOT happen: the old tiers-update block
|
||||||
|
after update() copied override's tiers back onto themselves
|
||||||
|
(merged["tiers"] already WAS override's tiers), i.e. it was
|
||||||
|
a no-op, and it is kept verbatim below so this function stays
|
||||||
|
a line-for-line move, not a semantic "fix". File tiers absent
|
||||||
|
from the JSON document are therefore dropped, not preserved;
|
||||||
|
changing that would be a behaviour change for another batch.
|
||||||
|
|
||||||
|
Source-level failure records (dropped_tiers/partial_failures
|
||||||
|
from batch 2) are NOT a product of this merge: they are filled
|
||||||
|
in by parse/load afterwards, and this function neither reads
|
||||||
|
nor writes them.
|
||||||
|
"""
|
||||||
|
if not isinstance(override, dict):
|
||||||
|
return base
|
||||||
|
if not isinstance(base, dict):
|
||||||
|
return override
|
||||||
|
merged = dict(base)
|
||||||
|
merged.update(override)
|
||||||
|
if isinstance(merged.get("tiers"), dict) and \
|
||||||
|
isinstance(override.get("tiers"), dict):
|
||||||
|
tiers = dict(merged["tiers"])
|
||||||
|
tiers.update(override["tiers"])
|
||||||
|
merged["tiers"] = tiers
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
def prompt_len_bucket(prompt) -> str:
|
def prompt_len_bucket(prompt) -> str:
|
||||||
"""Bucket a prompt by length for logging only, never for routing.
|
"""Bucket a prompt by length for logging only, never for routing.
|
||||||
|
|
||||||
@@ -578,16 +687,7 @@ def load_routing_policy(env=None):
|
|||||||
failures.append("CLOUD_ROUTING_POLICY_JSON: %s" % exc)
|
failures.append("CLOUD_ROUTING_POLICY_JSON: %s" % exc)
|
||||||
override = None
|
override = None
|
||||||
if isinstance(override, dict):
|
if isinstance(override, dict):
|
||||||
if not isinstance(merged, dict):
|
merged = merge_policy_documents(merged, override)
|
||||||
merged = override
|
|
||||||
else:
|
|
||||||
merged = dict(merged)
|
|
||||||
merged.update(override)
|
|
||||||
if isinstance(merged.get("tiers"), dict) and \
|
|
||||||
isinstance(override.get("tiers"), dict):
|
|
||||||
tiers = dict(merged["tiers"])
|
|
||||||
tiers.update(override["tiers"])
|
|
||||||
merged["tiers"] = tiers
|
|
||||||
policy = parse_routing_policy(merged)
|
policy = parse_routing_policy(merged)
|
||||||
if policy is None:
|
if policy is None:
|
||||||
if configured:
|
if configured:
|
||||||
@@ -741,13 +841,13 @@ def decide_routing(task, worker_model, policy=None,
|
|||||||
policy_version=policy.version, applied=True,
|
policy_version=policy.version, applied=True,
|
||||||
reason="%s tier mode is off" % tier, signals=signals)
|
reason="%s tier mode is off" % tier, signals=signals)
|
||||||
requires = policy.tier_requirements.get(tier) or {}
|
requires = policy.tier_requirements.get(tier) or {}
|
||||||
cap_ok, cap_missing, cap_unknown = check_model_capabilities(
|
cap = evaluate_capability(
|
||||||
policy.model_capabilities, tier_model, requires)
|
policy.model_capabilities, tier_model, requires)
|
||||||
cap_note = ""
|
cap_note = ""
|
||||||
if requires and not cap_ok:
|
if requires and not cap.ok:
|
||||||
cap_note = " (capability %s: %s)" % (
|
cap_note = " (capability %s: %s)" % (
|
||||||
"unknown" if cap_unknown else "lacks",
|
"unknown" if cap.unknown else "lacks",
|
||||||
", ".join(cap_missing))
|
", ".join(cap.missing))
|
||||||
if effective_mode == "shadow":
|
if effective_mode == "shadow":
|
||||||
return RoutingDecision(
|
return RoutingDecision(
|
||||||
task_id=task.get("id"), tier=tier, chosen_model=worker_model,
|
task_id=task.get("id"), tier=tier, chosen_model=worker_model,
|
||||||
@@ -756,7 +856,7 @@ def decide_routing(task, worker_model, policy=None,
|
|||||||
reason="%s tier suggests %s (shadow, not applied)%s"
|
reason="%s tier suggests %s (shadow, not applied)%s"
|
||||||
% (tier, tier_model, cap_note), signals=signals)
|
% (tier, tier_model, cap_note), signals=signals)
|
||||||
# effective_mode == "active"
|
# effective_mode == "active"
|
||||||
if cap_ok:
|
if cap.ok:
|
||||||
return RoutingDecision(
|
return RoutingDecision(
|
||||||
task_id=task.get("id"), tier=tier, chosen_model=tier_model,
|
task_id=task.get("id"), tier=tier, chosen_model=tier_model,
|
||||||
suggested_model=None, source="policy",
|
suggested_model=None, source="policy",
|
||||||
@@ -767,9 +867,9 @@ def decide_routing(task, worker_model, policy=None,
|
|||||||
# default against the same requirements before blocking:
|
# default against the same requirements before blocking:
|
||||||
# a capable worker default keeps the task moving without
|
# a capable worker default keeps the task moving without
|
||||||
# violating the requirement.
|
# violating the requirement.
|
||||||
worker_ok, _w_missing, _w_unknown = check_model_capabilities(
|
worker_cap = evaluate_capability(
|
||||||
policy.model_capabilities, worker_model, requires)
|
policy.model_capabilities, worker_model, requires)
|
||||||
if worker_ok:
|
if worker_cap.ok:
|
||||||
return RoutingDecision(
|
return RoutingDecision(
|
||||||
task_id=task.get("id"), tier=tier, chosen_model=worker_model,
|
task_id=task.get("id"), tier=tier, chosen_model=worker_model,
|
||||||
suggested_model=None, source="worker_default",
|
suggested_model=None, source="worker_default",
|
||||||
|
|||||||
+19
-24
@@ -72,14 +72,16 @@ try:
|
|||||||
from cloud.models import ROUTING_MODES, ROUTING_TIERS
|
from cloud.models import ROUTING_MODES, ROUTING_TIERS
|
||||||
from cloud.models import parse_routing_policy
|
from cloud.models import parse_routing_policy
|
||||||
from cloud.models import parse_tier_requires
|
from cloud.models import parse_tier_requires
|
||||||
from cloud.models import check_model_capabilities
|
from cloud.models import evaluate_capability
|
||||||
|
from cloud.models import merge_policy_documents
|
||||||
except ImportError: # run as a script from the repo root
|
except ImportError: # run as a script from the repo root
|
||||||
sys.path.insert(0, os.path.join(
|
sys.path.insert(0, os.path.join(
|
||||||
os.path.dirname(os.path.abspath(__file__)), ".."))
|
os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||||
from cloud.models import ROUTING_MODES, ROUTING_TIERS
|
from cloud.models import ROUTING_MODES, ROUTING_TIERS
|
||||||
from cloud.models import parse_routing_policy
|
from cloud.models import parse_routing_policy
|
||||||
from cloud.models import parse_tier_requires
|
from cloud.models import parse_tier_requires
|
||||||
from cloud.models import check_model_capabilities
|
from cloud.models import evaluate_capability
|
||||||
|
from cloud.models import merge_policy_documents
|
||||||
|
|
||||||
|
|
||||||
def _tier_dropped_reason(raw_entry):
|
def _tier_dropped_reason(raw_entry):
|
||||||
@@ -210,12 +212,11 @@ def dry_run_policy_document(data, env_mode_override=None,
|
|||||||
cap = {"required": None, "missing": [], "unknown": False,
|
cap = {"required": None, "missing": [], "unknown": False,
|
||||||
"status": "not_applicable"}
|
"status": "not_applicable"}
|
||||||
else:
|
else:
|
||||||
cap_ok, cap_missing, cap_unknown = check_model_capabilities(
|
cap_verdict = evaluate_capability(
|
||||||
policy.model_capabilities, policy.tiers[tier], requires)
|
policy.model_capabilities, policy.tiers[tier], requires)
|
||||||
cap = {"required": requires, "missing": cap_missing,
|
cap = {"required": requires, "missing": cap_verdict.missing,
|
||||||
"unknown": cap_unknown,
|
"unknown": cap_verdict.unknown,
|
||||||
"status": "ok" if cap_ok else (
|
"status": cap_verdict.status}
|
||||||
"unknown" if cap_unknown else "fail")}
|
|
||||||
tiers_report[tier] = {
|
tiers_report[tier] = {
|
||||||
"model": policy.tiers[tier], "mode": mode,
|
"model": policy.tiers[tier], "mode": mode,
|
||||||
"mode_source": mode_source, "status": status,
|
"mode_source": mode_source, "status": status,
|
||||||
@@ -268,17 +269,18 @@ def dry_run_policy_document(data, env_mode_override=None,
|
|||||||
" -- cannot verify it against the capability"
|
" -- cannot verify it against the capability"
|
||||||
" directory" % (tier, sorted(_req)))
|
" directory" % (tier, sorted(_req)))
|
||||||
else:
|
else:
|
||||||
_ok, _missing, _unknown = check_model_capabilities(
|
_gw_verdict = evaluate_capability(
|
||||||
policy.model_capabilities, gateway_model, _req)
|
policy.model_capabilities, gateway_model, _req)
|
||||||
_gw["tiers"][tier] = {
|
_gw["tiers"][tier] = {
|
||||||
"status": "ok" if _ok else (
|
"status": _gw_verdict.status,
|
||||||
"unknown" if _unknown else "fail"),
|
"missing": _gw_verdict.missing,
|
||||||
"missing": _missing, "unknown": _unknown,
|
"unknown": _gw_verdict.unknown,
|
||||||
"tier_mode": _tstatus}
|
"tier_mode": _tstatus}
|
||||||
_note = ("tier %r gateway model %r %s capability:"
|
_note = ("tier %r gateway model %r %s capability:"
|
||||||
" %s" % (tier, gateway_model,
|
" %s" % (tier, gateway_model,
|
||||||
"cannot verify" if _unknown else "lacks",
|
"cannot verify"
|
||||||
", ".join(_missing)))
|
if _gw_verdict.unknown else "lacks",
|
||||||
|
", ".join(_gw_verdict.missing)))
|
||||||
if _gw["tiers"][tier]["status"] in ("fail", "unknown"):
|
if _gw["tiers"][tier]["status"] in ("fail", "unknown"):
|
||||||
if _tstatus == "active":
|
if _tstatus == "active":
|
||||||
_gw_blocked = True
|
_gw_blocked = True
|
||||||
@@ -326,7 +328,9 @@ def dry_run_policy_document(data, env_mode_override=None,
|
|||||||
|
|
||||||
|
|
||||||
def dry_run_from_env(env=None):
|
def dry_run_from_env(env=None):
|
||||||
"""Dry-run from an env mapping, mirroring load_routing_policy.
|
"""Dry-run from an env mapping, sharing merge_policy_documents
|
||||||
|
with load_routing_policy (audit batch 3, I-4) — the merge is
|
||||||
|
one function now, not a mirrored copy.
|
||||||
|
|
||||||
Returns the report dict. File/JSON failures that the loader
|
Returns the report dict. File/JSON failures that the loader
|
||||||
silently ignores are reported as warnings here.
|
silently ignores are reported as warnings here.
|
||||||
@@ -354,16 +358,7 @@ def dry_run_from_env(env=None):
|
|||||||
"(%s); the loader would ignore it" % exc)
|
"(%s); the loader would ignore it" % exc)
|
||||||
override = None
|
override = None
|
||||||
if isinstance(override, dict):
|
if isinstance(override, dict):
|
||||||
if not isinstance(merged, dict):
|
merged = merge_policy_documents(merged, override)
|
||||||
merged = override
|
|
||||||
else:
|
|
||||||
merged = dict(merged)
|
|
||||||
merged.update(override)
|
|
||||||
if isinstance(merged.get("tiers"), dict) and \
|
|
||||||
isinstance(override.get("tiers"), dict):
|
|
||||||
tiers = dict(merged["tiers"])
|
|
||||||
tiers.update(override["tiers"])
|
|
||||||
merged["tiers"] = tiers
|
|
||||||
_gw_model = (env.get("CLOUD_SUB2API_DEFAULT_MODEL") or "").strip()
|
_gw_model = (env.get("CLOUD_SUB2API_DEFAULT_MODEL") or "").strip()
|
||||||
report = dry_run_policy_document(
|
report = dry_run_policy_document(
|
||||||
merged, env_mode_override=env.get("CLOUD_ROUTING_MODE") or None,
|
merged, env_mode_override=env.get("CLOUD_ROUTING_MODE") or None,
|
||||||
|
|||||||
+6
-6
@@ -70,7 +70,7 @@ from cloud.auth import AuthStore
|
|||||||
from cloud.coordinator import parse_parts as _coord_parse_parts
|
from cloud.coordinator import parse_parts as _coord_parse_parts
|
||||||
from cloud.coordinator import validate_parts as _coord_validate_parts
|
from cloud.coordinator import validate_parts as _coord_validate_parts
|
||||||
from cloud.cost import CostStore, unpriced_token_cap
|
from cloud.cost import CostStore, unpriced_token_cap
|
||||||
from cloud.models import check_model_capabilities as _check_caps
|
from cloud.models import evaluate_capability as _evaluate_capability
|
||||||
from cloud.models import load_routing_policy as _load_routing_policy
|
from cloud.models import load_routing_policy as _load_routing_policy
|
||||||
from cloud.routing_shadow import evaluate_shadow as _evaluate_shadow
|
from cloud.routing_shadow import evaluate_shadow as _evaluate_shadow
|
||||||
from cloud.db import connect as _db_connect
|
from cloud.db import connect as _db_connect
|
||||||
@@ -4348,12 +4348,12 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
|||||||
"model": None, "missing": [],
|
"model": None, "missing": [],
|
||||||
"unknown": False}
|
"unknown": False}
|
||||||
else:
|
else:
|
||||||
_ok, _missing, _unknown = _check_caps(
|
_verdict = _evaluate_capability(
|
||||||
_policy.model_capabilities, _model, _req)
|
_policy.model_capabilities, _model, _req)
|
||||||
_cap = {"status": "ok" if _ok else (
|
_cap = {"status": _verdict.status,
|
||||||
"unknown" if _unknown else "fail"),
|
"model": _model,
|
||||||
"model": _model, "missing": _missing,
|
"missing": _verdict.missing,
|
||||||
"unknown": _unknown}
|
"unknown": _verdict.unknown}
|
||||||
_bucket["capability"] = _cap
|
_bucket["capability"] = _cap
|
||||||
if _cap["status"] in ("fail", "unknown"):
|
if _cap["status"] in ("fail", "unknown"):
|
||||||
_cap_flagged.append(
|
_cap_flagged.append(
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
"""Audit batch 3 (I-3, I-4): capability verdict unification and policy
|
||||||
|
merge unification. Pure refactor: no behaviour change.
|
||||||
|
|
||||||
|
Run: python3 -m unittest discover -s cloud/tests -p "test_audit_batch3.py" -v
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
|
||||||
|
from cloud.models import (
|
||||||
|
check_model_capabilities,
|
||||||
|
decide_routing,
|
||||||
|
evaluate_capability,
|
||||||
|
load_routing_policy,
|
||||||
|
merge_policy_documents,
|
||||||
|
parse_routing_policy,
|
||||||
|
split_model_capability_missing,
|
||||||
|
)
|
||||||
|
from cloud.routing_dryrun import dry_run_from_env, dry_run_policy_document
|
||||||
|
|
||||||
|
REQUIRES = {"tool_calling": True, "vision": True,
|
||||||
|
"min_context_tokens": 1000}
|
||||||
|
|
||||||
|
CAPS = {
|
||||||
|
"good": {"tool_calling": True, "vision": True,
|
||||||
|
"context_tokens": 200000},
|
||||||
|
"no-tool": {"tool_calling": False, "vision": True,
|
||||||
|
"context_tokens": 200000},
|
||||||
|
"no-vision-field": {"tool_calling": True,
|
||||||
|
"context_tokens": 200000},
|
||||||
|
"small-ctx": {"tool_calling": True, "vision": True,
|
||||||
|
"context_tokens": 100},
|
||||||
|
"no-ctx": {"tool_calling": True, "vision": True},
|
||||||
|
"mixed": {"tool_calling": False, "context_tokens": 200000},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _policy(mode, tier_model, requires, caps, worker_model=None):
|
||||||
|
doc = {
|
||||||
|
"version": "batch3",
|
||||||
|
"mode": mode,
|
||||||
|
"tiers": {"default": {"model": tier_model,
|
||||||
|
"requires": requires}},
|
||||||
|
"model_capabilities": caps,
|
||||||
|
}
|
||||||
|
return parse_routing_policy(doc)
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluateCapabilityTestCase(unittest.TestCase):
|
||||||
|
def test_matrix_known_unknown_mixed_absent_empty(self):
|
||||||
|
cases = [
|
||||||
|
# (model, ok, missing, unknown, known, unknown_missing,
|
||||||
|
# status, verb)
|
||||||
|
("good", True, [], False, [], [], "ok", None),
|
||||||
|
("no-tool", False, ["tool_calling"], False,
|
||||||
|
["tool_calling"], [], "fail", "lacks"),
|
||||||
|
("no-vision-field", False, ["vision"], True,
|
||||||
|
[], ["vision"], "unknown", "cannot verify"),
|
||||||
|
("small-ctx", False, ["min_context_tokens"], False,
|
||||||
|
["min_context_tokens"], [], "fail", "lacks"),
|
||||||
|
("no-ctx", False, ["min_context_tokens"], True,
|
||||||
|
[], ["min_context_tokens"], "unknown", "cannot verify"),
|
||||||
|
("mixed", False, ["tool_calling", "vision"], True,
|
||||||
|
["tool_calling"], ["vision"], "unknown", "cannot verify"),
|
||||||
|
("absent-model", False,
|
||||||
|
["min_context_tokens", "tool_calling", "vision"], True,
|
||||||
|
[], ["min_context_tokens", "tool_calling", "vision"],
|
||||||
|
"unknown", "cannot verify"),
|
||||||
|
]
|
||||||
|
for (model, ok, missing, unknown, known, unknown_missing,
|
||||||
|
status, verb) in cases:
|
||||||
|
with self.subTest(model=model):
|
||||||
|
verdict = evaluate_capability(CAPS, model, REQUIRES)
|
||||||
|
self.assertEqual(verdict.ok, ok)
|
||||||
|
self.assertEqual(verdict.missing, missing)
|
||||||
|
self.assertEqual(verdict.unknown, unknown)
|
||||||
|
self.assertEqual(verdict.known_missing, known)
|
||||||
|
self.assertEqual(verdict.unknown_missing, unknown_missing)
|
||||||
|
self.assertEqual(verdict.status, status)
|
||||||
|
if verb is not None:
|
||||||
|
self.assertEqual(verdict.verb, verb)
|
||||||
|
|
||||||
|
def test_empty_requires_always_ok_even_for_absent_model(self):
|
||||||
|
verdict = evaluate_capability(CAPS, "absent-model", {})
|
||||||
|
self.assertTrue(verdict.ok)
|
||||||
|
self.assertEqual(verdict.missing, [])
|
||||||
|
self.assertFalse(verdict.unknown)
|
||||||
|
self.assertEqual(verdict.status, "ok")
|
||||||
|
|
||||||
|
def test_parity_with_old_check_and_split(self):
|
||||||
|
# The refactor contract: every verdict field equals what the
|
||||||
|
# two old functions returned for the same inputs, across a
|
||||||
|
# matrix of directories, models and requires — including the
|
||||||
|
# preserved edge where an absent model plus an uncleaned
|
||||||
|
# requires key makes the total check list the invalid key
|
||||||
|
# while the split lists only recognised keys. That edge is
|
||||||
|
# documented here as preserved parity, not as desired
|
||||||
|
# semantics; changing it would be a behaviour change.
|
||||||
|
requires_sets = [
|
||||||
|
REQUIRES,
|
||||||
|
{"tool_calling": True},
|
||||||
|
{"min_context_tokens": 500},
|
||||||
|
{},
|
||||||
|
{"tool_calling": True, "bogus_key": True},
|
||||||
|
]
|
||||||
|
models = list(CAPS) + ["absent-model", "", None]
|
||||||
|
for requires in requires_sets:
|
||||||
|
for model in models:
|
||||||
|
with self.subTest(model=model, requires=requires):
|
||||||
|
verdict = evaluate_capability(
|
||||||
|
CAPS, model, requires)
|
||||||
|
ok, missing, unknown = check_model_capabilities(
|
||||||
|
CAPS, model, requires)
|
||||||
|
known, unknown_missing = \
|
||||||
|
split_model_capability_missing(
|
||||||
|
CAPS, model, requires)
|
||||||
|
self.assertEqual(verdict.ok, ok)
|
||||||
|
self.assertEqual(verdict.missing, missing)
|
||||||
|
self.assertEqual(verdict.unknown, unknown)
|
||||||
|
self.assertEqual(verdict.known_missing, known)
|
||||||
|
self.assertEqual(
|
||||||
|
verdict.unknown_missing, unknown_missing)
|
||||||
|
|
||||||
|
|
||||||
|
class DecideRoutingWordingTestCase(unittest.TestCase):
|
||||||
|
def test_active_known_missing_falls_back_to_worker_default(self):
|
||||||
|
policy = _policy("active", "no-tool", REQUIRES, CAPS)
|
||||||
|
decision = decide_routing({}, "good", policy=policy)
|
||||||
|
self.assertEqual(decision.source, "worker_default")
|
||||||
|
self.assertTrue(decision.capability_fallback)
|
||||||
|
self.assertEqual(decision.chosen_model, "good")
|
||||||
|
self.assertIn("capability lacks: tool_calling",
|
||||||
|
decision.reason)
|
||||||
|
|
||||||
|
def test_active_both_fail_blocks_with_exact_reason_shape(self):
|
||||||
|
policy = _policy("active", "no-tool", REQUIRES, CAPS)
|
||||||
|
decision = decide_routing({}, "no-tool", policy=policy)
|
||||||
|
self.assertEqual(decision.source, "capability_blocked")
|
||||||
|
self.assertIsNone(decision.chosen_model)
|
||||||
|
self.assertIn("refusing to dispatch an incapable or"
|
||||||
|
" unverifiable model", decision.reason)
|
||||||
|
|
||||||
|
def test_shadow_unknown_and_known_notes(self):
|
||||||
|
policy = _policy("shadow", "no-vision-field", REQUIRES, CAPS)
|
||||||
|
decision = decide_routing({}, "good", policy=policy)
|
||||||
|
self.assertEqual(decision.source, "shadow")
|
||||||
|
self.assertFalse(decision.applied)
|
||||||
|
self.assertIn("capability unknown: vision", decision.reason)
|
||||||
|
policy = _policy("shadow", "no-tool", REQUIRES, CAPS)
|
||||||
|
decision = decide_routing({}, "good", policy=policy)
|
||||||
|
self.assertIn("capability lacks: tool_calling",
|
||||||
|
decision.reason)
|
||||||
|
|
||||||
|
|
||||||
|
class DryRunWordingTestCase(unittest.TestCase):
|
||||||
|
def _doc(self, mode, tier_model):
|
||||||
|
return {
|
||||||
|
"version": "batch3",
|
||||||
|
"mode": mode,
|
||||||
|
"tiers": {"default": {"model": tier_model,
|
||||||
|
"requires": REQUIRES}},
|
||||||
|
"model_capabilities": CAPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_tier_check_notes_and_block_flags(self):
|
||||||
|
report = dry_run_policy_document(
|
||||||
|
self._doc("active", "no-tool"))
|
||||||
|
tier = report["tiers"]["default"]
|
||||||
|
self.assertEqual(tier["capabilities"]["status"], "fail")
|
||||||
|
self.assertTrue(report["blocked"])
|
||||||
|
self.assertTrue(any(
|
||||||
|
"lacks capability: tool_calling" in w
|
||||||
|
and "would block 'default' tasks at dispatch" in w
|
||||||
|
for w in report["warnings"]))
|
||||||
|
report = dry_run_policy_document(
|
||||||
|
self._doc("shadow", "no-vision-field"))
|
||||||
|
tier = report["tiers"]["default"]
|
||||||
|
self.assertEqual(tier["capabilities"]["status"], "unknown")
|
||||||
|
self.assertFalse(report["blocked"])
|
||||||
|
self.assertTrue(any(
|
||||||
|
"cannot verify capability: vision" in w
|
||||||
|
and "shadow mode does not block dispatch" in w
|
||||||
|
for w in report["warnings"]))
|
||||||
|
|
||||||
|
def test_gateway_check_notes_and_block_flags(self):
|
||||||
|
report = dry_run_policy_document(
|
||||||
|
self._doc("active", "good"), gateway_model="no-tool")
|
||||||
|
gw = report["gateway"]["tiers"]["default"]
|
||||||
|
self.assertEqual(gw["status"], "fail")
|
||||||
|
self.assertTrue(report["blocked"])
|
||||||
|
self.assertTrue(any(
|
||||||
|
"gateway model 'no-tool' lacks capability: tool_calling"
|
||||||
|
in w
|
||||||
|
and "would block 'default' tasks at the gateway gate" in w
|
||||||
|
for w in report["warnings"]))
|
||||||
|
report = dry_run_policy_document(
|
||||||
|
self._doc("shadow", "good"), gateway_model=None)
|
||||||
|
gw = report["gateway"]["tiers"]["default"]
|
||||||
|
self.assertEqual(gw["status"], "unknown")
|
||||||
|
self.assertTrue(gw["unknown"])
|
||||||
|
self.assertTrue(any(
|
||||||
|
"gateway model is not set in CLOUD_SUB2API_DEFAULT_MODEL"
|
||||||
|
in w for w in report["warnings"]))
|
||||||
|
|
||||||
|
|
||||||
|
class MergePolicyDocumentsTestCase(unittest.TestCase):
|
||||||
|
def test_boundary_cases(self):
|
||||||
|
self.assertIsNone(merge_policy_documents(None, None))
|
||||||
|
self.assertEqual(merge_policy_documents(None, {"a": 1}),
|
||||||
|
{"a": 1})
|
||||||
|
self.assertEqual(merge_policy_documents({"a": 1}, None),
|
||||||
|
{"a": 1})
|
||||||
|
self.assertEqual(merge_policy_documents({"a": 1}, ["x"]),
|
||||||
|
{"a": 1})
|
||||||
|
self.assertEqual(merge_policy_documents(["x"], {"a": 1}),
|
||||||
|
{"a": 1})
|
||||||
|
# No tiers anywhere: plain shallow update.
|
||||||
|
self.assertEqual(
|
||||||
|
merge_policy_documents({"mode": "shadow", "version": "v1"},
|
||||||
|
{"mode": "active"}),
|
||||||
|
{"mode": "active", "version": "v1"})
|
||||||
|
|
||||||
|
def test_json_replaces_whole_tiers_map_not_per_tier(self):
|
||||||
|
# Preserved (not fixed) semantics, documented in
|
||||||
|
# merge_policy_documents: JSON tiers replace the whole map.
|
||||||
|
# File tiers absent from JSON are dropped, not preserved.
|
||||||
|
base = {"mode": "shadow",
|
||||||
|
"tiers": {"plan": "m-file-plan",
|
||||||
|
"default": "m-file-default"}}
|
||||||
|
override = {"tiers": {"plan": "m-json-plan"}}
|
||||||
|
self.assertEqual(merge_policy_documents(base, override),
|
||||||
|
{"mode": "shadow",
|
||||||
|
"tiers": {"plan": "m-json-plan"}})
|
||||||
|
|
||||||
|
def test_inputs_not_mutated(self):
|
||||||
|
base = {"mode": "shadow",
|
||||||
|
"tiers": {"plan": "m-file-plan",
|
||||||
|
"default": "m-file-default"}}
|
||||||
|
override = {"tiers": {"plan": "m-json-plan"}}
|
||||||
|
base_before, override_before = copy.deepcopy(base), \
|
||||||
|
copy.deepcopy(override)
|
||||||
|
merge_policy_documents(base, override)
|
||||||
|
self.assertEqual(base, base_before)
|
||||||
|
self.assertEqual(override, override_before)
|
||||||
|
|
||||||
|
def test_property_random_pairs_match_shallow_update(self):
|
||||||
|
rng = random.Random(20261102)
|
||||||
|
tier_values = ["m1", "m2", {"model": "m3"},
|
||||||
|
{"model": "m4", "mode": "shadow"},
|
||||||
|
{"model": "m5", "mode": "bogus"},
|
||||||
|
{"model": ""}, " ", 7, None]
|
||||||
|
top_values = ["shadow", "active", "v9", 3, None,
|
||||||
|
{"x": 1}, ["a"]]
|
||||||
|
|
||||||
|
def _rand_doc():
|
||||||
|
doc = {}
|
||||||
|
if rng.random() < 0.8:
|
||||||
|
doc["mode"] = rng.choice(["shadow", "active", "off"])
|
||||||
|
if rng.random() < 0.5:
|
||||||
|
doc["version"] = rng.choice(top_values)
|
||||||
|
if rng.random() < 0.8:
|
||||||
|
tiers = {}
|
||||||
|
for tier in rng.sample(
|
||||||
|
["plan", "goal", "default", "weird"],
|
||||||
|
rng.randint(0, 3)):
|
||||||
|
tiers[tier] = rng.choice(tier_values)
|
||||||
|
if rng.random() < 0.1:
|
||||||
|
doc["tiers"] = rng.choice(["x", 5, None])
|
||||||
|
else:
|
||||||
|
doc["tiers"] = tiers
|
||||||
|
return doc
|
||||||
|
|
||||||
|
for _ in range(300):
|
||||||
|
base = _rand_doc()
|
||||||
|
override = _rand_doc()
|
||||||
|
got = merge_policy_documents(base, override)
|
||||||
|
expected = dict(base)
|
||||||
|
expected.update(override)
|
||||||
|
self.assertEqual(got, expected)
|
||||||
|
|
||||||
|
def test_loader_and_dryrun_agree_on_same_env(self):
|
||||||
|
# Loader and dry-run now share merge_policy_documents: for
|
||||||
|
# the same file+JSON env, the parsed policy tiers and the
|
||||||
|
# dry-run report tiers (and fingerprints) must agree.
|
||||||
|
cases = [
|
||||||
|
({"mode": "shadow",
|
||||||
|
"tiers": {"plan": "m-file-plan",
|
||||||
|
"default": "m-file-default"}},
|
||||||
|
{"tiers": {"plan": "m-json-plan"}}),
|
||||||
|
({"mode": "active",
|
||||||
|
"tiers": {"default": {"model": "m1",
|
||||||
|
"requires": {"tool_calling": True}}},
|
||||||
|
"model_capabilities": {"m1": {"tool_calling": True}}},
|
||||||
|
{"mode": "shadow"}),
|
||||||
|
]
|
||||||
|
for base_doc, override_doc in cases:
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w", suffix=".json", delete=False) as fh:
|
||||||
|
json.dump(base_doc, fh)
|
||||||
|
path = fh.name
|
||||||
|
try:
|
||||||
|
env = {"CLOUD_ROUTING_POLICY_FILE": path,
|
||||||
|
"CLOUD_ROUTING_POLICY_JSON":
|
||||||
|
json.dumps(override_doc)}
|
||||||
|
policy = load_routing_policy(env)
|
||||||
|
report = dry_run_from_env(env)
|
||||||
|
self.assertIsNotNone(policy)
|
||||||
|
self.assertTrue(report["valid"])
|
||||||
|
for tier, model in policy.tiers.items():
|
||||||
|
self.assertEqual(
|
||||||
|
report["tiers"][tier]["model"], model)
|
||||||
|
self.assertEqual(report["content_fingerprint"],
|
||||||
|
policy.content_fingerprint())
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+15
-19
@@ -165,10 +165,9 @@ from cloud.db import connect as _db_connect
|
|||||||
from cloud.goal_command import parse_goal_command as _parse_goal_command
|
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.logging_config import get_logger, log_event, setup_logging
|
||||||
from cloud.models import ModelRouter, RoutingPolicy, decide_routing, \
|
from cloud.models import ModelRouter, RoutingPolicy, decide_routing, \
|
||||||
check_model_capabilities, classify_task, \
|
classify_task, evaluate_capability, \
|
||||||
is_fallbackable, load_routing_policy, parse_routing_policy, \
|
is_fallbackable, load_routing_policy, parse_routing_policy, \
|
||||||
record_fallback, record_routing, \
|
record_fallback, record_routing
|
||||||
split_model_capability_missing
|
|
||||||
from cloud.routing_shadow import parse_worker_routing_detail as \
|
from cloud.routing_shadow import parse_worker_routing_detail as \
|
||||||
_parse_routing_detail
|
_parse_routing_detail
|
||||||
from cloud.auto_memory import AutoMemoryStore
|
from cloud.auto_memory import AutoMemoryStore
|
||||||
@@ -7032,15 +7031,15 @@ class Worker:
|
|||||||
if mode not in ("active", "shadow"):
|
if mode not in ("active", "shadow"):
|
||||||
return None
|
return None
|
||||||
model = task.get("model")
|
model = task.get("model")
|
||||||
ok, missing, unknown = check_model_capabilities(
|
verdict = evaluate_capability(
|
||||||
self.routing_policy.model_capabilities, model, requires)
|
self.routing_policy.model_capabilities, model, requires)
|
||||||
if ok:
|
if verdict.ok:
|
||||||
return None
|
return None
|
||||||
known, _unk = split_model_capability_missing(
|
known = verdict.known_missing
|
||||||
self.routing_policy.model_capabilities, model, requires)
|
|
||||||
detail = ("%s model=%s tier=%s mode=%s missing=%s unknown=%s"
|
detail = ("%s model=%s tier=%s mode=%s missing=%s unknown=%s"
|
||||||
% (task["id"], model, tier, mode,
|
% (task["id"], model, tier, mode,
|
||||||
",".join(missing) or "-", unknown))
|
",".join(verdict.missing) or "-",
|
||||||
|
verdict.unknown))
|
||||||
if mode == "shadow":
|
if mode == "shadow":
|
||||||
self._audit(task["project_id"],
|
self._audit(task["project_id"],
|
||||||
"worker_capability_recheck_shadow", detail)
|
"worker_capability_recheck_shadow", detail)
|
||||||
@@ -7444,17 +7443,17 @@ class Worker:
|
|||||||
if _gw_requires:
|
if _gw_requires:
|
||||||
_gw_mode = self.routing_policy.tier_mode(_gw_tier)
|
_gw_mode = self.routing_policy.tier_mode(_gw_tier)
|
||||||
if _gw_mode in ("active", "shadow"):
|
if _gw_mode in ("active", "shadow"):
|
||||||
_gw_ok, _gw_missing, _gw_unknown = \
|
_gw_verdict = evaluate_capability(
|
||||||
check_model_capabilities(
|
|
||||||
self.routing_policy.model_capabilities,
|
self.routing_policy.model_capabilities,
|
||||||
task.get("model"), _gw_requires)
|
task.get("model"), _gw_requires)
|
||||||
if not _gw_ok:
|
if not _gw_verdict.ok:
|
||||||
_gw_note = ("gateway model %s %s capability:"
|
_gw_note = ("gateway model %s %s capability:"
|
||||||
" %s" % (
|
" %s" % (
|
||||||
task.get("model"),
|
task.get("model"),
|
||||||
"cannot verify"
|
"cannot verify"
|
||||||
if _gw_unknown else "lacks",
|
if _gw_verdict.unknown
|
||||||
", ".join(_gw_missing)))
|
else "lacks",
|
||||||
|
", ".join(_gw_verdict.missing)))
|
||||||
# Audit fix 4(第三遍审计分档建议):
|
# Audit fix 4(第三遍审计分档建议):
|
||||||
# 显式模型按 known/unknown 分档。known
|
# 显式模型按 known/unknown 分档。known
|
||||||
# 是目录明确写了不满足(布尔 False、
|
# 是目录明确写了不满足(布尔 False、
|
||||||
@@ -7463,10 +7462,7 @@ class Worker:
|
|||||||
# 无该模型、或条目缺字段,目录陈旧
|
# 无该模型、或条目缺字段,目录陈旧
|
||||||
# 不应惩罚用户的正确选择,只审计不拦。
|
# 不应惩罚用户的正确选择,只审计不拦。
|
||||||
# shadow 档两种情况都只审计。
|
# shadow 档两种情况都只审计。
|
||||||
_gw_known, _gw_unk = \
|
_gw_known = _gw_verdict.known_missing
|
||||||
split_model_capability_missing(
|
|
||||||
self.routing_policy.model_capabilities,
|
|
||||||
task.get("model"), _gw_requires)
|
|
||||||
_gw_block = False
|
_gw_block = False
|
||||||
_gw_error = None
|
_gw_error = None
|
||||||
if _explicit_model:
|
if _explicit_model:
|
||||||
@@ -7483,8 +7479,8 @@ class Worker:
|
|||||||
" missing=%s unknown=%s blocked=%s"
|
" missing=%s unknown=%s blocked=%s"
|
||||||
% (task_id, task.get("model"),
|
% (task_id, task.get("model"),
|
||||||
_gw_tier, _gw_mode,
|
_gw_tier, _gw_mode,
|
||||||
",".join(_gw_missing) or "-",
|
",".join(_gw_verdict.missing) or "-",
|
||||||
_gw_unknown,
|
_gw_verdict.unknown,
|
||||||
_gw_blocked))
|
_gw_blocked))
|
||||||
if _gw_blocked:
|
if _gw_blocked:
|
||||||
_gw_block = True
|
_gw_block = True
|
||||||
|
|||||||
Reference in new issue
Block a user