<!-- OpenSmartRoute: osr-integrations. https://osr-web.kindrock-caaab1a2.centralindia.azurecontainerapps.io/docs/skills/osr-integrations -->

# OpenSmartRoute integrations

Everything below produces `RouteTarget`s or handlers; the router itself has no I/O.

## LLM providers (`adapters.openai_compat`)

```python
from opensmartroute.adapters import OpenAICompatClient, chat_handler, embedder, judge_fn
openai = OpenAICompatClient()                                   # OPENAI_API_KEY (or *_FILE) from env
ollama = OpenAICompatClient("http://localhost:11434/v1", api_key="ollama")
registry.get("llm-frontier").handler = chat_handler(openai, model="gpt-4o", system_prompt=None)
registry.get("llm-small").handler   = chat_handler(ollama, model="llama3.1")
router = Router(registry,
    strategies=[CapabilityStrategy(), SimilarityStrategy(embedder=embedder(openai, "text-embedding-3-small"))],
    llm_judge=LLMJudgeStrategy(judge_fn(openai, "gpt-4o-mini")), escalate_llm_judge_below=0.5)
```
`chat_handler` returns `ChatResult(text, model, prompt_tokens, completion_tokens, latency_ms, raw)`;
execution derives cost from tokens x `usd_per_1k_tokens`. Local embeddings:
`sentence_transformers_embedder(model)` (`embeddings` extra).

## MCP tools (`adapters.mcp`)

```python
from opensmartroute.adapters import tools_from_mcp, connect_mcp, connect_mcp_http, tools_from_manifest
targets = tools_from_mcp(tools_list_json, server="github", call=lambda name, args: client.call(name, args),
                         cost_per_call_usd=0.0, latency_ms=300, quality_prior=0.6, max_description_risk=0.6)
client, targets = connect_mcp(["npx", "-y", "@modelcontextprotocol/server-filesystem", "."], server="fs")
remote, more = connect_mcp_http("https://mcp.example.com/mcp", api_key=load_secret("MCP_TOKEN"),
                                server="crm", resources=True, prompts=True)   # Streamable HTTP, no MCP SDK
```
Ids are `server:tool`; `inputSchema` is kept in `metadata`; risky descriptions are dropped. Prefer
signed manifests in production (`sign_manifest` / `verify_manifest` / `tools_from_manifest`).
`connect_mcp(..., resources=True, prompts=True)` and `connect_mcp_http` also import `resources/list` as
read-only `tool` targets (`resources_from_mcp`) and `prompts/list` as instructions-only `skill` targets
(`prompts_from_mcp`, `primary=False`). `HttpMCPClient(url, api_key=...)` is the underlying stdlib client
(https, or http for localhost; JSON and `text/event-stream` replies; echoes `Mcp-Session-Id`) - it reaches the
hosted platform's `/mcp`, an `osr serve` and third-party servers alike. `mcp_server_handler(url, api_key=None,
tool=None)` is the handler for a target that stands for a *whole* remote server (an OCM manifest's `mcp` endpoint,
a marketplace tool listing): it connects lazily, lists the tools once and runs `context["tool"]`, the only tool, or
the closest lexical match with `context["tool_arguments"]`; `ocm.bind_endpoint(target)` uses it for `mcp`
endpoints next to `http_handler` / `a2a_handler` for `http` / `a2a`.

Imported tools are routable targets *and* plan slots: a model / agent plan offers up to `tool_slots` matching tools
to the model as function-calling specs (`execution.tool_spec` builds them from `metadata["input_schema"]`, or a
single `input` string for handler-only tools) and `run()` executes the calls the model makes, passing the parsed
arguments as `context["tool_arguments"]`. Give tools 2-5 `examples` and `domains`: the slot is filled from the
ensemble's quality estimate (`RoutingSettings.tool_quality_floor`, 0.5).

## Marketplace (`adapters.marketplace`)

```python
from opensmartroute.adapters import MarketplaceClient, MarketplaceSync
mk = MarketplaceClient()                                   # branding.MARKETPLACE_URL; https only
router.registry.upsert(mk.target("acme-skills-sql-reporting"))   # one listing as a target
mk.sync(router.registry, kind="skill", limit=50)          # upsert matching listings (SyncReport)
MarketplaceSync(router.registry, refresh_s=86400).start()  # follow on a schedule (MarketplaceSettings)
server = MCPServer(router, marketplace=mk.mcp_hook(router.registry))  # local marketplace_search / marketplace_get
```
`osr marketplace search|pull|sync`, `osr serve --marketplace-refresh`, `osr mcp` (marketplace tools on by default;
`marketplace_get {install: true}` loads the listing into the running router). Loaded listings are bound to their
`http` / `a2a` / `mcp` endpoint (`MarketplaceClient(bind=False)` opts out), so an agent card or MCP server pulled
from the marketplace runs when the router picks it; on the hosted platform an installed listing joins the
workspace's pool automatically (`Registry.targets_for` -> `Router.route(extra_targets=)`) and an installed
model card runs once mapped to one of the workspace's own providers.

Which *server* to connect in the first place (`adapters.mcp_servers`):

```python
from opensmartroute.adapters import ServerCard, recommend_servers
cards = [ServerCard.from_dict(d) for d in registry_json]            # name, description, tools, tags, auth,
for rec in recommend_servers("open a pull request", cards, k=2,    #   latency_ms, region, data_boundary
                             constraints=req.constraints, allowed_auth=["none", "token"]):
    rec.server.name, rec.score, rec.rationale, rec.matched_tools
```
BM25 + hashed-dense fusion over the card text; hard filters on latency, region, data boundary and
auth kind run before ranking, so a private request never gets a public-only server recommended.

## vLLM semantic-router configs (`adapters.semantic_router`)

```python
from opensmartroute.adapters import load_semantic_router_config
imported = load_semantic_router_config("config.yaml")            # or a dict
router = Router(imported.registry, strategies=[RulesStrategy(imported.rules), *default_strategies()])
imported.categories, imported.default_model, imported.warnings
```
Categories with `model_scores` become `RouteTarget`s (`quality_prior` = mean score, cost from
`model_config.pricing`, `effort: high` when every category uses reasoning) plus one `Rule` per
category; `system_prompt` lands in the top target's `instructions`.

## A2A agents (`adapters.a2a`)

```python
from opensmartroute.adapters import fetch_agent_card, agent_from_card, skills_from_card, a2a_handler
card = fetch_agent_card("https://agents.example.com/travel")          # https only, /.well-known/agent.json
agent = agent_from_card(card, call=a2a_handler(card["url"], token=load_secret("A2A_TOKEN")))
for s in skills_from_card(card): registry.add(s)                       # primary=False, id "<agent>/<skill>"
```
`a2a_handler` sends `message/send` and, when the agent answers with a `submitted` / `working` task, polls
`tasks/get` (`poll_interval_s`) until a terminal state within `timeout_s`; `failed` / `rejected` become a failed
outcome and `input-required` is handed back to the caller instead of being retried.

## Agent harnesses (`adapters.harness`)

`CallableHarness(fn)` (`fn(task, context, history) -> str | dict | HarnessResult`),
`HTTPHarness(url, api_key_env=..., timeout_s=300)` (POST `{"task","context","history"}`),
`SubprocessHarness(argv, json_output=True, system_flag="--system")`. Wrap with
`harness_handler(harness)` and assign to a `kind: agent` target. Return `HarnessResult(text, success,
cost_usd, prompt_tokens, completion_tokens, quality)` so outcomes carry real cost/quality.

## Frameworks (`adapters.frameworks`)

```python
node = langgraph_node(router, execute=False, plan=False)      # state["messages"] -> route_target, route
graph.add_conditional_edges("route", langgraph_condition(by="target", default="__end__"), {...})
executor, handoffs = maf_router_executor(router)               # Microsoft Agent Framework
spec, handle = openai_tool_spec("route_request"), openai_tool_handler(router)   # function calling
decision, result = route_and_execute(router, text, history=..., context=..., execute=True)
```

## Personas and skills

- Personas: `load_personas(path)` accepts a directory of `*.md`, `.json`, `.jsonl` or `.csv`;
  `persona_target(name, prompt, description, domains=..., primary=False)`. Their `instructions` are
  composed into `context["system"]` as `# Persona: <name>` when they fill a plan slot.
- Skills: `load_skills(root)` globs `*/SKILL.md`; `load_skill(dir)` loads one.

### Authoring a SKILL.md the router can select

```markdown
---
name: sql-reporting                      # must equal the directory name; ^[a-z0-9]+(-[a-z0-9]+)*$, <= 64
description: Writes warehouse SQL for recurring reports. Use for SQL or analytics questions.   # <= 1024
license: Apache-2.0
metadata:
  osr-domains: "data_analysis coding"    # routing hints (space/comma separated)
  osr-languages: "en"
  osr-tags: "sql"
  osr-quality-prior: "0.82"
  osr-primary: "false"                   # slot-only: layered on a model in the plan
  osr-latency-ms: "0"
  osr-requires: "warehouse-schema"       # optional skill-graph edges, space/comma separated ids
  osr-conflicts: ""
  osr-composes: "chart-builder"
---
# Body = instructions, disclosed only when the skill is selected (progressive disclosure)
```
Only `name`/`description` are used for routing; write the description for the *request* the skill
should attract (it seeds `examples`, one per sentence). `osr-domains`/`osr-actions` must use the
signal ontology names (`signals.DOMAIN_LEXICON` / `ACTION_LEXICON`, e.g. `coding`, `data_analysis`,
`code_generation`, `reasoning`); an unmatched declared scope is penalised, so omit `osr-actions`
when the skill spans many request types. Validate with `osr skills <root>`; register with
`registry.add(skill)` or `--skills <root>` on the CLI; the router fills the plan's skill slot
(`router.route(req, plan=True)`) or scores skills directly via `candidates=[...]`.
`osr-requires` / `osr-conflicts` / `osr-composes` feed `discovery.SkillGraph(load_skills(root))`;
`graph.compose(["sql-reporting"])` returns the dependency-ordered skill set and the ids dropped for
conflicts, and `graph.observe(skill_ids, success)` learns co-usage companions.

## Client dialects the served router accepts

`osr -t targets.yaml serve` (and the hosted platform) expose the routed engine behind the dialects existing
clients already speak, so an application switches by changing one base URL: OpenAI `POST /v1/chat/completions`
(streaming, tool calling), `POST /v1/responses` (the Responses API the OpenAI Agents SDK uses), `/v1/embeddings`,
`/v1/moderations`, `/v1/audio/speech`, `/v1/audio/transcriptions`, `/v1/images/generations`, `/v1/models`, and the
Anthropic `POST /v1/messages` + `/v1/messages/count_tokens` (what the Anthropic SDKs and Claude Code speak).
`model: "auto"` (or `"opensmartroute"`, `"osr/auto:cheap|fast|private|agentic|quality"`) lets the router pick, a real target id pins it,
`models: [...]` scores only those candidates with fallbacks. Each call becomes one routed chat call
(`responses.responses_to_chat` / `anthropic` translate in and out); the decision rides in the `opensmartroute`
metadata and the `X-OSR-*` headers. See the deploy skill for tokens and hardening flags.

## TypeScript client (`@opensmartroute/sdk`)

`npm install @opensmartroute/sdk` (published with every release; zero runtime dependencies, global `fetch`):
`new OpenSmartRoute({ baseUrl, apiKey })` for the platform, `apiPrefix: ""` for an `osr serve`;
`route` / `feedback` / `estimate` / `targets`, `chat.completions.create` (SSE stream as an `AsyncIterable`),
`responses`, `messages`, `embeddings`, `audio`, `images`, and every platform endpoint typed from `openapi.json`
through `osr.rest.get|post|patch|del`. Guide: `platform/sdk-ts/README.md` (`/docs/sdk-typescript` on the site).
