feat(extensions): policy_routing — c12n→cache policy mapping (T-0104)#2
Open
jadb wants to merge 1 commit into
Open
feat(extensions): policy_routing — c12n→cache policy mapping (T-0104)#2jadb wants to merge 1 commit into
jadb wants to merge 1 commit into
Conversation
Implements the cache policy table from scenario 3 spec: PII/Jailbreak/Toxicity/CodeContent/Domain/Complexity/OutputFormat labels → CachePolicy(action, ttl, namespace, match). Mirrors x402 extension shape. CachePolicy dataclass aligns with US-0102 (cache-policy-hooks, paper). Defaults namespace to workspace_id per US-0102+T-0193. c12n classification stubbed in tests via fake ClassificationResult. Wire-up to routellm core's cache hook drops in once US-0102 ships. Refs T-0104 + scenario 3.
There was a problem hiding this comment.
Pull request overview
Adds a new installable extension package (extensions/policy_routing) that maps c12n-style classification signals to cache policy decisions intended to plug into the planned cache_policy hook (US-0102), with a scenario-3 policy table reference implementation and tests.
Changes:
- Introduces
policy_routingpackage withclassify_to_policymapping logic andCachePolicy/CacheAction/MatchStrategytypes. - Adds a thin integration adapter (
build_cache_hook) to produce a callable compatible with the planned hook usage. - Adds README, packaging (
pyproject.toml), and a comprehensive policy-table-driven pytest suite.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| extensions/policy_routing/policy_routing/policy.py | Implements the policy table mapping and defines the extension’s CachePolicy-related types/constants. |
| extensions/policy_routing/policy_routing/integrations.py | Provides build_cache_hook adapter for staged integration with routellm core. |
| extensions/policy_routing/policy_routing/init.py | Exposes the extension’s public API surface. |
| extensions/policy_routing/tests/test_policy.py | Adds tests covering each scenario-3 table row plus precedence and hook smoke tests. |
| extensions/policy_routing/README.md | Documents installation, usage, and the policy table. |
| extensions/policy_routing/pyproject.toml | Defines the new extension as a pip-installable package. |
| extensions/policy_routing/tests/init.py | Marks the tests package. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+97
to
+123
| @dataclass(frozen=True) | ||
| class CachePolicy: | ||
| """Cache-hook decision returned to routellm core. | ||
|
|
||
| Mirrors routellm/types.py CachePolicy (planned by US-0102). | ||
| Frozen so it's hashable + safe to pass between threads. | ||
|
|
||
| Attributes | ||
| ---------- | ||
| action: SKIP / LOOKUP_ONLY / LOOKUP_AND_STORE / STORE_ONLY. | ||
| ttl: store TTL in seconds; overrides CacheConfig.ttl_seconds. | ||
| namespace: cache key prefix. DEFAULT_NAMESPACE_KEY means | ||
| "resolve workspace_id from request"; TENANT_NAMESPACE_KEY | ||
| means "resolve tenant_id"; any other string is used verbatim | ||
| (e.g. "global:domain:math"). | ||
| match: exact / semantic / both. | ||
| reason: human-readable trace label for audit log + debugging. | ||
| metadata: free-form dict for downstream hooks (audit, routing). | ||
| """ | ||
|
|
||
| action: CacheAction = CacheAction.LOOKUP_AND_STORE | ||
| ttl: int = TTL_DEFAULT | ||
| namespace: str = DEFAULT_NAMESPACE_KEY | ||
| match: MatchStrategy = MatchStrategy.BOTH | ||
| reason: str = "default" | ||
| metadata: tuple = field(default_factory=tuple) | ||
|
|
Comment on lines
+144
to
+145
| def _has_pii(classification: Any) -> bool: | ||
| labels = _attr(classification, "pii_labels") or () |
Comment on lines
+26
to
+51
| # Type aliases for readability. `Any` because c12n bindings + routellm | ||
| # Request types may not be importable yet at adoption time. | ||
| Request = Any | ||
| ClassificationResult = Any | ||
| Classifier = Callable[[Request], ClassificationResult] | ||
| CacheHook = Callable[[Request, ClassificationResult], CachePolicy] | ||
|
|
||
|
|
||
| def build_cache_hook( | ||
| classifier: Optional[Classifier] = None, | ||
| ) -> CacheHook: | ||
| """Return a cache_policy hook for routellm's controller. | ||
|
|
||
| If `classifier` is provided AND the caller invokes the hook with | ||
| `classification=None`, the hook will run the classifier on the | ||
| request first. Otherwise the caller is expected to pass an | ||
| already-classified result (the common case — classification | ||
| middleware sits upstream of the cache hook). | ||
|
|
||
| Returns a callable matching US-0102 contract. | ||
| """ | ||
|
|
||
| def hook( | ||
| request: Request, | ||
| classification: ClassificationResult = None, | ||
| ) -> CachePolicy: |
Comment on lines
+134
to
+141
| def _attr(obj: Any, name: str, default: Any = None) -> Any: | ||
| """Safe attribute getter — works for dataclasses, namedtuples, | ||
| plain classes, dict-like, TypedDict instances.""" | ||
| if obj is None: | ||
| return default | ||
| if isinstance(obj, dict): | ||
| return obj.get(name, default) | ||
| return getattr(obj, name, default) |
Comment on lines
+56
to
+72
| @pytest.mark.parametrize("label", ["EMAIL", "PHONE", "SSN"]) | ||
| def test_pii_label_skips_cache_with_tenant_namespace(label): | ||
| classification = FakeClassification(pii_labels=(label,)) | ||
| policy = classify_to_policy(REQUEST, classification) | ||
| assert policy.action is CacheAction.SKIP | ||
| assert policy.ttl == 0 | ||
| assert policy.namespace == TENANT_NAMESPACE_KEY | ||
| assert policy.reason == "pii_detected" | ||
|
|
||
|
|
||
| def test_pii_label_unknown_does_not_trigger(): | ||
| # Non-blocking PII labels (e.g. "NAME") should NOT skip cache; | ||
| # only the explicit blocking set fires. | ||
| classification = FakeClassification(pii_labels=("NAME",)) | ||
| policy = classify_to_policy(REQUEST, classification) | ||
| assert policy == DEFAULT_POLICY | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First concrete
extensions/routellm_*-style sibling package, implementing the cache-policy table from scenario 3:extensions/policy_routing/— new pip-installable extensionCachePolicy(action, ttl, namespace, match)workspace_idper US-0102 + T-0193extensions/routellm_x402/shape described indocs/plans/2026-03-08-response-middleware-refactor.md— heavy/optional deps stay out of coreRefs
Test plan
pytest tests/ -q→ 30 passedroutellmcore cache hook once US-0102 implementation ships (currently stubbed in tests via fakeClassificationResult)pip install -e extensions/policy_routingagainst a routellm checkout