Skip to content

feat(extensions): policy_routing — c12n→cache policy mapping (T-0104)#2

Open
jadb wants to merge 1 commit into
mainfrom
feat/extensions-policy-routing-rebased
Open

feat(extensions): policy_routing — c12n→cache policy mapping (T-0104)#2
jadb wants to merge 1 commit into
mainfrom
feat/extensions-policy-routing-rebased

Conversation

@jadb
Copy link
Copy Markdown

@jadb jadb commented May 11, 2026

Summary

First concrete extensions/routellm_*-style sibling package, implementing the cache-policy table from scenario 3:

  • extensions/policy_routing/ — new pip-installable extension
  • Maps c12n classification labels (PII / Jailbreak / Toxicity / CodeContent / Domain / Complexity / OutputFormat) → CachePolicy(action, ttl, namespace, match)
  • Defaults namespace to workspace_id per US-0102 + T-0193
  • c12n-shape-agnostic — consumers pass any object exposing the documented signal attrs; bindings layer ships once c12n Python lib lands
  • Mirrors the extensions/routellm_x402/ shape described in docs/plans/2026-03-08-response-middleware-refactor.md — heavy/optional deps stay out of core

Refs

  • T-0104 (c12n → cache policy mapping)
  • US-0102 (cache policy hooks)
  • T-0193 (workspace_id default namespace)

Test plan

  • pytest tests/ -q → 30 passed
  • Verify wire-up to routellm core cache hook once US-0102 implementation ships (currently stubbed in tests via fake ClassificationResult)
  • Smoke-install: pip install -e extensions/policy_routing against a routellm checkout

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.
Copilot AI review requested due to automatic review settings May 11, 2026 22:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_routing package with classify_to_policy mapping logic and CachePolicy/CacheAction/MatchStrategy types.
  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants