Skip to content
OpenSmartRoute
Documentation
Agent skills

osr-decorator-sdk

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.

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.

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() / 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)

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