feat(router): port Router Core into the Python SDK - #50
Merged
Conversation
…al tier tables
The Python SDK routed on its own hand-maintained tier tables and a 14-dimension
scorer while the TypeScript SDK and the gateway had both moved to
@blockrun/router-core. The same request could pick different models on each
SDK, and the Python tables drifted independently.
router-core ships as an npm tarball pinned to a commit, so parity requires a
port: blockrun_llm/router_core/ is that port, at upstream commit 18bf4ab (one
ahead of the TS pin, which predates the deepseek-v4-flash NVIDIA EOL).
blockrun_llm/router_adapter.py ports the TS SDK's src/router-adapter.ts, and
router.py becomes a back-compat shim.
What Python did not have before: portfolio (V3) ranking instead of tier lookup,
hard capability filtering (a model that cannot hold the conversation, emit the
requested max_tokens, call tools or read images is dropped before scoring),
task classification, and explainable decisions (candidates, candidate_scores,
task_type, reasoning). Adds client.route() for a dry-run decision.
Two live bugs fell out of the port:
- The free profile pointed at models NVIDIA has retired (deepseek-v4-flash,
410 on 2026-08-12; llama-4-maverick; qwen3-coder-480b), so free routing
depended entirely on the gateway's redirect. It now routes the live free
lineup, and the adapter drops any candidate the catalog does not price at $0.
- Catalog rows marked available: false no longer enter the pricing map; every
smart call to one would have failed with a non-transient error.
Behavior change: routing.method is now "portfolio" by default ("rules" for the
free profile and the config-only V2 rollback).
tests/unit/test_router_core.py ports all four upstream vitest suites (88 cases)
as the parity guard; test_router_adapter.py covers the host layer. Verified on
Python 3.9 (the CI floor) and end-to-end against the live gateway.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Python SDK routed on its own hand-maintained tier tables and a 14-dimension scorer, while the TypeScript SDK and the gateway had both moved to
@blockrun/router-core. The same request could pick different models depending on which SDK you called, and the Python tables drifted on their own schedule.router-core ships as an npm tarball pinned to a commit, so npm can't install it into a Python package — parity requires a port.
What
blockrun_llm/router_core/— the port, at upstream commit18bf4ab(one commit ahead of the TS SDK's pind430804, which predates the deepseek-v4-flash NVIDIA EOL).config.pyis machine-transpiled fromconfig.tswith comments preserved; the transpiled object was asserted equal to a JSON parse of the TS source, so no value was hand-copied._js.py:js_regex, which setsre.ASCIIso\b/\w/\dkeep JavaScript semantics — without it patterns like\b(?:fast)\bsilently stop matching inside CJK text._js.pyalso portsNumber.toFixedrounding andDate.parse, both of which show up in decision output.blockrun_llm/router_adapter.py— port of the TS SDK'ssrc/router-adapter.ts:free/*→nvidia/*catalog id resolution, x402 payment floors, capacity filtering against the full conversation.blockrun_llm/router.py— now a back-compat shim.route(),classify_by_rules()and the decision keys still work.client.route(prompt, ...)— returns the routing decision without making or paying for a call (TS SDK parity).Capabilities Python didn't have before
code_agent,tool_agent,reasoning_math,long_context,extraction, …reasoningstringcandidates,candidate_scores,task_type,profile,router_versionBugs this surfaced
freeprofile pointed at retired models. Its table led withnvidia/deepseek-v4-flash(HTTP 410 since 2026-08-12) and fell back tonvidia/llama-4-maverickandnvidia/qwen3-coder-480b(also EOL), so free routing worked only because the gateway redirects pinned callers. It now routes the live free lineup, and the adapter drops any candidate the catalog doesn't price at $0 — a paid model can't leak into a free-profile call.available: falseentered the pricing map, so routing could pick a model every call to which fails non-transiently. Now skipped (matches the TS client).Breaking-ish
routing.methodis now"portfolio"by default —"rules"only for the free profile and the config-only V2 rollback. Code assertingmethod == "rules"needs updating. Everything else onRoutingDecisionis additive.Verification
tests/unit/test_router_core.py— a 1:1 port of all four upstream vitest suites (portfolio,selector,strategy,tool-intent). These are the guard that Python and TypeScript keep choosing the same model; re-port them whenever upstream is re-synced.routing_profile="free"call served bynvidia/step-3.7-flashat $0.model_profiles.generated.jsonships.Version bumped to 1.11.0 (all three declarations), CHANGELOG / README / CLAUDE.md updated.