Skip to content
OpenSmartRoute
Documentation
Agent skills

osr-decorator-sdk

Agent skills 3 min read

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, @llm, @persona, @human or @target; bind code to a catalogue's targets with @handler; declare routing rules in code with rule() / @rule; refuse or rewrite requests with @guard; hook decisions, outcomes and errors with @on_decision, @on_outcome, @on_error; route in one line with osr.route(), osr.run() and osr.feedback(); 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.

Package
.claude/skills/osr-decorator-sdk
Compatibility
OpenSmartRoute >= 0.4, Python >= 3.10
License
Apache-2.0
Domains
coding general
Quality prior
0.85
Tags
opensmartroute sdk decorators settings

Install by copying .claude/skills/osr-decorator-sdk/ into the skills folder of your coding assistant. To load every package as a routing target: osr --skills .claude/skills route "..." --plan.

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

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; @llm, @persona (primary=False) and @human are the other shorthands. The docstring's first paragraph is the description (what routing matches), the rest is instructions (what the model is told once selected).

The short forms (reach for these first)

osr.components.load("targets.yaml", "rules.yaml")     # a catalogue and its rules become components

@osr.handler("gpt-4o", "gpt-4o-mini")                 # code for targets the catalogue declares
def chat(request, **kw): ...                           # unknown id -> ConfigurationError (lists known ids)

osr.rule("pii-stays-home", prefer=["onprem"], pin=True, contains_pii=True)   # rules.yaml in code

@osr.rule(prefer=["human-desk"], weight=0.8, domains=["support"])   # + a predicate fn(request, signals) -> bool
def angry(request, signals): return "unacceptable" in request.text.lower()

@osr.guard                                             # fn(request) -> None | str/False (SecurityError) | RouteRequest
def no_empty(request): return "empty request" if not request.text.strip() else None

@osr.on_decision                                       # fn(request, decision); @on_outcome fn(outcome);
def log_it(request, decision): ...                     # @on_error fn(request, error)

@osr.signal(name="urgent")                             # a scalar lands in signals.extra["urgent"]
def urgent(request, signals): return "asap" in request.text.lower()

d = osr.route("Refund order #123")                     # the registry builds + caches its router (live())
osr.feedback(d, success=True, quality=0.9)             # Outcome from the decision, learned
res = osr.run("Summarise ..."); osr.feedback(res, quality=0.7)   # route + execute + learn; later judgement = correction
await osr.aroute("..."); await osr.arun("...")        # async forms (alive() = AsyncRouter over live())

Rule conditions are the when keys (RULE_CONDITIONS: domains, actions, all_actions, language, min_complexity, max_complexity, contains_pii, needs_tools, event, workflow, context) - a typo raises ConfigurationError. Declared rules become one rules strategy in front of whichever ensemble is used (custom strategies or the defaults). live() is a Router, or an EnterpriseRouter once a guard, middleware or hook is registered; it is rebuilt when the registry changes after first use.

With the CLI: osr --components my_components serve / route (repeatable, module:setup calls setup(registry); OSR_SERVER_COMPONENTS for the container) imports the module and applies its declarations to the router built from the catalogue - registry.apply(router) in code; guards, middleware and hooks make serve harden the router so they run.

Assembling

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() / rules() / rules_strategy() / extractors() / policy() / middlewares() / telemetry_sinks() / targets() / target_registry(base) materialise fresh instances (handlers bound).
  • add(*instances, weight=) registers ready-made objects (targets, strategies, rules, ...); 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)

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).

Something wrong or out of date on this page? Tell us - the page is named for you, so a one-line note is enough.