SchemaRouter
Schema-aware planning and execution for LLM tool ecosystems.
SchemaRouter compiles a natural-language request plus a registered capability catalog into a small, typed, auditable execution plan.
It goes beyond Query -> Tool routing:
Query
-> Tool
-> Endpoint
-> Parameters
-> Response fields
-> Evidence / policy
-> Schema validation
-> Execute
SchemaRouter is intentionally narrower than LangChain or LangGraph. It is designed to sit at the tool-schema boundary between an agent and structured capability sources such as OpenAPI, MCP, OPTIMADE, Python callables, and third-party adapter protocols.
Status: 0.4.0 is the current public non-prerelease release. Install it from PyPI with
pip install schemarouter. SchemaRouter remains pre-1.0, so deliberate compatibility changes may still occur in later 0.x minor releases under the documented versioning policy.
Why
As an agent gains more tools, choosing the tool is only one part of the problem. The runtime also needs to know:
- which operation inside that tool is relevant;
- which parameters are declared and valid;
- which response fields should be retained;
- whether the operation is read-only, mutating, destructive, or unclassified;
- whether the schema changed after planning;
- whether the raw tool result actually satisfies the declared contract.
SchemaRouter makes those decisions explicit.
Quickstart
from pydantic import BaseModel
from schemarouter import PlanRequest, SchemaRouter, schema_tool
class Weather(BaseModel):
city: str
temperature: float
@schema_tool(read_only=True)
def current_weather(city: str) -> Weather:
return Weather(city=city, temperature=20.5)
router = SchemaRouter()
router.add_callable(current_weather)
results = router.invoke(
PlanRequest(
query="city temperature",
arguments={"city": "Seoul"},
)
)
print(results[0].data)
The same execution vocabulary works across capability sources:
router.invoke(request)
await router.ainvoke(request)
router.batch(requests)
await router.abatch(requests)
router.stream(request)
router.astream(request)
router.astream_events(request)
Bring your schema
OpenAPI
router = await SchemaRouter.from_url(
"https://api.example.com/openapi.json",
kind="openapi",
)
Cross-document OpenAPI $ref fetching stays off by default. Trusted callers can opt into bounded
same-origin resolution:
router = await SchemaRouter.from_url(
"https://api.example.com/openapi.json",
kind="openapi",
openapi_external_refs=True,
)
OPTIMADE
router = await SchemaRouter.from_url(
"https://www.crystallography.net/cod/optimade",
kind="optimade",
)
OPTIMADE entry schemas are discovered from /info/<entry_type>. Planned fields are translated
into the protocol's response_fields query parameter before execution.
MCP
pip install "schemarouter[mcp]"
router = await SchemaRouter.from_url(
"http://localhost:8000/mcp",
kind="mcp",
)
Python
router.add_callable(my_typed_function)
Human-readable API docs
proposal = await router.inspect_url(
"https://docs.example.com/api",
model=documentation_model,
)
router.approve_proposal(
proposal,
base_url="https://api.example.com",
)
Human-readable documentation never becomes executable automatically. It first becomes an evidence-grounded proposal and then requires explicit approval.
Core guarantees
- Schema-constrained planning — unknown tools, endpoints, parameters, and fields cannot become executable calls.
- Runtime JSON Schema validation — validate arguments before invocation and raw output before projection.
- Bounded nested projection — declared logical fields may map to explicit nested object paths without allowing model-produced JSONPath or undeclared field traversal.
- Schema and binding drift detection — stale plans and stale transports fail closed.
- Local execution authority — remote metadata and model output cannot grant mutation or destructive permissions.
- Credential separation — schema-fetch credentials and runtime credentials stay in different channels; authenticated MCP keeps secrets in the trusted transport boundary.
- Bounded OpenAPI external refs — cross-document
$refloading is explicit opt-in, confined to the entry-document origin, and bounded by redirect/depth/document/byte limits. - Read-only retries by default — contract violations are never retried.
- Per-call approval and execution budgets — trusted local callbacks and deterministic call, attempt, remote, time, quota, and cost-unit limits fail closed.
- Trusted execution hooks — ordered sync/async before/after hooks receive detached snapshots, cannot transform calls/results, and fail closed without turning hook failures into tool retries.
- OpenAPI compatibility reporting — partial/unsupported constructs are machine-readable instead of silently reinterpreted.
- Redacted runtime events by default — payload tracing is opt-in.
- Replayable persistent traces —
SQLiteRunTraceStorecan persist validated event streams and replay them later without re-running planners, network calls, or tools. - Persistent/pluggable registry — use the built-in transactional
SQLiteRegistryor inject a custom implementation of the publicToolRegistryprotocol. Persistent catalog state never serializes trusted invokers or credentials. - Pluggable source adapters —
AdapterRegistrylets structured protocols compile into the sameToolSpec/EndpointSpecexecution model; installed entry-point plugins require an explicit allowlist before import. - Optional OpenTelemetry export — redacted runtime events can become parented run/tool spans without exporting payload values.
With LangChain
pip install "schemarouter[langchain]"
from schemarouter.integrations import to_langchain_tools
tools = to_langchain_tools(router)
Execution still flows through SchemaRouter's policy, fingerprint, input, and output validation.
With LangGraph
pip install "schemarouter[langgraph]"
from schemarouter.integrations import to_langgraph_node
builder.add_node("schema_router", to_langgraph_node(router))
The node supports sync/async StateGraph execution and returns checkpoint-friendly partial state
updates while SchemaRouter retains planning, policy, and validated execution authority.
With LlamaIndex
Install the packaged LlamaIndex bridge:
pip install "schemarouter[llamaindex]"
from schemarouter.integrations import to_llamaindex_tools
tools = to_llamaindex_tools(router)
LlamaIndex remains the agent/workflow layer; SchemaRouter retains schema identity, validation, and endpoint execution.
Experimental bounded decisions
SchemaRouter includes an optional bounded DecisionBackend for tool/endpoint and output-field
selection. It is off by default and cannot invent executable schema members.
from schemarouter import DecisionPolicy, SchemaPlanner
from schemarouter.integrations import JevDecisionBackend
planner = SchemaPlanner(
registry,
decision_backend=JevDecisionBackend(min_confidence=0.65),
decision_policy=DecisionPolicy(
enabled=True,
endpoint_selection=True,
fallback="deterministic",
),
)
A provider-neutral local embedding backend is also available without adding an embedding library to SchemaRouter's dependencies:
from schemarouter import EmbeddingDecisionBackend
backend = EmbeddingDecisionBackend(
embed_batch,
min_similarity=0.35,
min_margin=0.05,
)
The callable can wrap a local SentenceTransformers/FastEmbed-style encoder or an application-owned embedding service. SchemaRouter computes cosine ranking locally and can abstain on weak or ambiguous matches.
Jev / TypeSafe System One is optional:
pip install "schemarouter[jev]"
export TYPESAFE_API_KEY="..."
The provider receives only bounded decision inputs. Unknown option IDs fail closed, low-confidence valid choices can abstain, and deterministic fallback remains available. Jev is never enabled just because the package or an API key exists.
A local Ollama model can also serve as a bounded decision backend without an additional Python SDK:
from schemarouter.integrations import OllamaDecisionBackend
backend = OllamaDecisionBackend("your-installed-model")
Ollama structured output constrains the finite option IDs, and SchemaRouter revalidates the result locally. No local model is enabled automatically.
Bounded field selection is independently opt-in:
policy = DecisionPolicy(
enabled=True,
field_selection=True,
fallback="deterministic",
)
Only declared non-identifier fields are offered to the backend. Identifier fields are always preserved locally, and invalid/abstaining provider output falls back to deterministic projection.
Evidence sufficiency is independently opt-in as a conservative gate:
policy = DecisionPolicy(
enabled=True,
evidence_sufficiency=True,
fallback="deterministic",
)
Requested provenance/license/unit/source-type requirements must first be satisfied by local schema
metadata. The backend then receives only evidence:sufficient / evidence:insufficient and can
veto a locally sufficient call, but it cannot upgrade missing evidence or grant execution authority.
Decision benchmark
Run the deterministic baseline:
python scripts/benchmark_decision_routing.py
Compare an embedding backend through a local callable:
python scripts/benchmark_decision_routing.py \
--corpus benchmarks/decision-routing-v1.json \
--embedding-callable my_embeddings:embed_batch
Compare Jev when credentials are available:
TYPESAFE_API_KEY="..." python scripts/benchmark_decision_routing.py --jev
Compare an installed local Ollama model:
python scripts/benchmark_decision_routing.py --ollama-model your-installed-model
The harness includes a checked-in 144-case multilingual/adversarial corpus and reports routing
accuracy, invalid-plan rate, abstentions/fallbacks, category accuracy, p50/p95 latency, token usage,
errors, and optional cost estimates. A provider-neutral ModelQueryAnalyzer callable can also be
supplied with --model-callable module:function; embedding encoders use
--embedding-callable module:function.
python scripts/benchmark_decision_routing.py \
--corpus benchmarks/decision-routing-v1.json \
--json-out artifacts/decision-benchmark.json \
--csv-out artifacts/decision-benchmark.csv
Documentation
Full documentation is organized as a framework manual rather than embedded in this README:
- Getting started
- Core concepts
- OpenAPI guide
- OpenAPI compatibility
- OPTIMADE guide
- MCP guide
- LangChain integration
- LlamaIndex integration
- Jev / TypeSafe integration
- Ollama decision backend
- OpenTelemetry integration
- Third-party adapter plugins
- Decision backends
- Decision benchmark
- Persistent run traces
- Trusted execution hooks
- Field projection
- Candidate indexing
- API reference
- Architecture
- Security
- Brand assets
Build the docs locally with:
pip install -e ".[docs]"
mkdocs serve
Development
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
ruff check .
pytest -q -m "not mcp_integration"
python examples/quickstart.py
python scripts/benchmark_decision_routing.py
# Type-check the complete packaged surface, including optional integrations.
pip install -e ".[dev,mcp,langchain,langgraph,llamaindex,jev,otel]"
pyright
pytest -q --cov=schemarouter --cov-branch --cov-report=term-missing
Optional integration suites are isolated from the core package:
pip install -e ".[dev,mcp]"
pytest -q tests/test_mcp_integration.py
pip install -e ".[dev,langchain]"
pytest -q tests/test_langchain_integration.py
pip install -e ".[dev,llamaindex]"
pytest -q tests/test_llamaindex_integration.py
pip install -e ".[dev,jev]"
pytest -q tests/test_jev_integration.py
pip install -e ".[dev,otel]"
pytest -q tests/test_opentelemetry_integration.py
Project scope
SchemaRouter does not implement another chat abstraction, graph runtime, model-provider layer, memory system, or checkpoint store. Those belong in surrounding agent frameworks.
Its scope is:
Natural-language request -> typed tool execution plan -> validated execution.
Research
SchemaRouter originated from SchemaRouter: Field-Aware Tool Routing for Efficient Heterogeneous Agentic RAG. The framework keeps the research idea while removing harness assumptions such as one endpoint per tool and fixture-only execution.
License
MIT © 2026 Yong-eun Cho
Release files for schemarouter 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| schemarouter-0.4.0.tar.gz | 205.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| schemarouter-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 293.4 kB
Release files / schemarouter-0.4.0.tar.gz
| Download URL | schemarouter-0.4.0.tar.gz |
|---|---|
| Size | 205.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cd00938ed74b0e319f709d9848272a2c687f2a1235ebf695c4a4aab1965395e5
|
|
BLAKE2b-256 checksum How to use checksums |
8f9a6d7dff430b2467120194479c68b683f1b251bab9ecad251ae6a8982db754
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / schemarouter-0.4.0-py3-none-any.whl
| Download URL | schemarouter-0.4.0-py3-none-any.whl |
|---|---|
| Size | 88.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9dcc5303e408e96f754dafd853850a7bbe3aef57d73a1626f3ea48def08d0bd2
|
|
BLAKE2b-256 checksum How to use checksums |
7ec1e927c35f1efdf6bb0ef8b85432cc2dfc34b7dbe53b47e7c98a623c499b34
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency log