<!-- OpenSmartRoute: osr-decorator-sdk. Source https://github.com/isathish/OpenSmartRoute/blob/main/.claude/skills/osr-decorator-sdk/SKILL.md; HTML https://opensmartroute.ai/docs/skills/osr-decorator-sdk -->
---
name: osr-decorator-sdk
description: Extend OpenSmartRoute in Python with the decorator SDK. Write a custom strategy scorer, a custom policy rule, a signal extractor, middleware or telemetry sink and register them with @strategy, @policy_rule, @signal, @middleware, @telemetry; declare handlers as targets with @tool, @skill, @agent or @target; assemble everything with ComponentRegistry and components.router() or components.builder(); load plugins; change default weights and thresholds through Settings, configure() and OSR_* environment variables. Use when the user wants custom routing logic, a new strategy or rule, or to register components with decorators.
license: Apache-2.0
compatibility: OpenSmartRoute >= 0.4, Python >= 3.10
metadata:
  author: opensmartroute
  osr-domains: "coding general"
  osr-tags: "opensmartroute sdk decorators settings"
  osr-quality-prior: "0.85"
  osr-primary: "false"
---

# OpenSmartRoute decorator SDK

`opensmartroute.sdk` stores *blueprints* (factories) in a `ComponentRegistry`; decorators are
transparent (they return the decorated object) and `osr.components` is the process-wide registry the
top-level decorators bind to. Use a private `ComponentRegistry()` in tests and multi-tenant apps.

## Declaring components

```python
import opensmartroute as osr

@osr.tool("weather", domains=["travel"], actions=["lookup"], cost=0.0, examples=["weather in paris"])
def weather(request, **kw):
    """Current weather for a city."""            # first docstring paragraph -> target.description
    return lookup(request.text)                  # handler(request, **kw) -> response

@osr.agent("planner", domains=["travel"], min_complexity=0.3, quality_prior=0.9, latency_ms=4000)
def planner(request, **kw): ...

@osr.skill("cite-sources", domains=["research"], primary=False)   # skill slot pre-processor
def cite(request):                                               # may return RouteRequest | str | None
    return "Always cite primary sources."

@osr.strategy(weight=0.8)                        # fn(request, signals, candidates) -> {id: 0..1 | StrategyScore}
def geo_affinity(request, signals, candidates):
    region = request.constraints.region
    return {t.id: 1.0 if region in t.constraints.regions else 0.4 for t in candidates}

@osr.strategy                                    # Strategy subclass; name inferred -> "recency"
class RecencyStrategy(osr.Strategy):
    def score(self, request, signals, candidates): ...
    def update(self, outcome): ...               # optional: learn from Outcome

@osr.signal(order=-1)                            # order < 0 runs before built-in extractors
def urgency(request, signals):
    return {"urgent": "asap" in request.text.lower()}   # Signals fields are set; other keys -> signals.extra

@osr.policy_rule                                 # fn(target, request, signals) -> rejection reason | None
def business_hours_only(target, request, signals):
    if target.kind is osr.TargetKind.HUMAN and request.context.get("after_hours"):
        return "human queues are closed"
    return None

@osr.middleware                                  # fn(request, next_) -> RouteDecision
def tag_tenant(request, next_):
    request.context.setdefault("tenant", "public")
    return next_(request)

@osr.telemetry                                   # Telemetry subclass only
class Collect(osr.enterprise.Telemetry):
    def on_decision(self, request, decision): ...
```

`@target(id, kind, *, name, description, domains, actions, languages, modalities, tags,
min_complexity, max_complexity, supports_tools, cost, latency_ms, quality_prior, examples,
instructions, primary, constraints, metadata, family, effort)` is the general form.

## Assembling

```python
router = osr.components.router(registry)                    # Router; registered strategies replace defaults
app = osr.components.builder(registry).with_auto_learning().build()      # EnterpriseRouter
RouterBuilder(registry).with_defaults().with_components(reg, middleware=False)  # selective wiring
```

- `registry.strategies() / weights() / extractors() / policy() / middlewares() / telemetry_sinks() /
  targets() / target_registry(base)` materialise fresh instances.
- `add(*instances, weight=)` registers ready-made objects; duplicates raise `ConfigurationError`
  (`register(..., replace=True)` to override); `unregister`, `clear`, `merge` manage the catalogue.
- Plugins: `registry.include("pkg.module")` (decorators run on import; `"pkg.module:setup"` calls
  `setup(registry)`), `registry.discover()` loads `opensmartroute.plugins` entry points.

## Settings (all tunables, no hardcoded numbers)

```python
from opensmartroute.settings import RoutingSettings, WeightSettings
osr.configure(routing=RoutingSettings(narrow_above=200), weights=WeightSettings(rules=3.0))
osr.Router(reg)                               # uses configured settings
osr.Router(reg, settings=osr.Settings())      # isolated library defaults
```

Groups: `routing` (softmax_temperature, slot_quality_floor, skill_set_margin, narrow_above,
narrow_to, cost/latency_norm_scale), `policy` (jailbreak_threshold, safety_tag, data_boundaries,
wildcard_language), `rules`, `capability`, `bandit`, `weights` (one per strategy name).
Environment overlay `OSR_<GROUP>_<FIELD>` (e.g. `OSR_WEIGHTS_CAPABILITY=1.5`); bad values raise
`ConfigurationError(details={"env": key})`. `osr settings [--json]` lists every key.

## Conventions when writing components

- Strategy names are snake_case nouns; decorated classes drop the `Strategy`/`Signal`/`Middleware`/
  `Telemetry` suffix automatically. Scores are clipped to `[0, 1]`; omit a candidate to abstain.
- Never read thresholds from literals - take a `settings: Settings | None = None` argument and call
  `opensmartroute.settings.resolve(settings)`.
- Brand-derived names come from `opensmartroute.branding` (`env_key`, `error_code`, `metadata_key`,
  `logger`, `user_agent`).
