Replies: 1 comment
-
|
Thank you for preparing this proposal. I will review it closely this week. |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Following the review on #852 (thanks @xintongsong for the thorough look) — opening this to align on the design before going deeper on implementation. PR #852 is a working prototype/reference; below is a refined design and the considerations behind it. Feedback welcome.
1 · What & why
Let an agent route each request to the most appropriate chat model instead of pinning one — send cheap/simple asks to a small model and hard ones to a strong model (cost + quality). The prototype models this as a drop-in
ChatModelRouter(aCHAT_MODELan agent points at by name), with a pluggable selection strategy, optional fallback, and per-conversation caching.2 · Core proposal: routing should select, not delegate
In the prototype the router is a
ChatModelSetupthat calls the chosen backend'schat()internally. That one fact causes the two issues raised in review: the backend call and the LLM-judge call happen inside one resource, so they're invisible to the EventLog and their metrics collapse onto the router.Proposal: make routing a framework capability (a dedicated router resource) that returns a model name; the framework then runs the normal chat path against that model.
ChatRequestEvent/ChatResponseEventin the EventLog; the decision is its ownModelRoutingEvent; an LLM judge's call goes through the standard chat path too. (This is also what keeps routing compatible with the tracing/evaluation direction in [Discussion] Agent observability: tracing & evaluation beyond metrics + event log #710 — the decision is data, not a black box.)One detail to settle under this model: how the LLM-judge call is represented — a full
ChatRequest/Responsein the loop (most transparent, bigger change) vs. a lighterModelRoutingEventcarrying its reply. I lean toward the lighter event first, with the full path as a follow-up.3 · Public API: function-first, with built-ins
Every well-regarded routing framework I looked at makes a function the front door and ships a few built-ins — none require authoring rules in a nested map as the primary API:
Predicate.matches(Exchange)in.choice().when(...)QueryRouter.route(Query)+ built-ins (LanguageModelQueryRouter)split().branch(Predicate)@Router/AbstractMessageRouterSo the single public extension point is
RoutingStrategy— a one-method interface returning a candidate name (ornullto abstain). Everything else is config:RoutingStrategyobjects handed in via small factories —Strategies.rules(...)(keyword/regex),Strategies.llm(...)(judge); later semantic/ML. Rule-based stays, but as just-a-built-in — no magic-string dispatch in code.fallback,cache/cache_size) — not interfaces users implement or wrap.RoutingStrategy, and only for custom/ML.4 · Framework vs. user — the target shape
Push everything mechanical into the framework; the user writes at most one function. Example shapes:
A · Common case — config only, no routing code
B · Custom — the only user code is one function (lambda)
C · LLM-as-router — a judge model decides (built-in)
D · ML / learned — a reusable strategy class (bring-your-own)
Regex is just a rule (or a lambda)
The agent side — identical in every case
Across all of these the agent's actions are unchanged — they just send
ChatRequestEvent("router", …). Only the strategy slot differs: rules → LLM → ML is a one-line change.Proposed API surface (names/semantics up for critique)
ModelRouter.of(String… candidates).strategy(RoutingStrategy s)Strategies.rules(...),Strategies.llm(...)); custom is a lambda or class instance. No name-string dispatch..defaultModel(String name)null) or names a non-candidate. Must be one of the candidates..fallback(boolean)false)..cache(boolean)/.cacheSize(int)RoutingStrategy#route(RoutingContext) → Stringnullto abstain. (@FunctionalInterface, so a lambda works.)RoutingContextPoints I'd like input on: (a) built-ins are factories that return a
RoutingStrategy(Strategies.rules(...)) — the open bit is only whether we prefer that or typed builder methods (.rules(...),.llm(...)); either way, no hardcoded strategy keywords in Java. (b) shouldcache/fallbackbe simple flags as above, or richer policy objects? (c) doesRoutingContextexpose enough (candidate descriptions/metadata, resource access) for ML/semantic strategies?5 · Design considerations — prior art & proposed direction
These are the decisions that shape the design. For each, I've laid out how established products already solve it and the direction I'm proposing, so we're converging on a concrete plan rather than starting from scratch. Refinements welcome.
Consideration 1 · Strategies are objects, not name strings — with built-in factories
Prior art — the split is about who dispatches on the name. In-code APIs pass objects: LangChain4j hands in a router instance (
new LanguageModelQueryRouter(...)), Camel/Kafka Streams take aPredicate. Only LiteLLM selects a built-in by string (latency-based/cost-based/…) — because it's a config-driven proxy, the declarative exception, and even its built-ins are operational, not content-rules.Proposal — no hardcoded strategy keywords in Java. One setter,
strategy(RoutingStrategy); built-ins are factories (Strategies.rules(...),Strategies.llm(...)) and custom is a lambda. A name string is unavoidable only in declarative config (YAML — cf. #662 — or SQL), and there we use the fully-qualified class name (open/extensible), with a short alias as optional sugar, not a baked-in framework keyword. Keep a thinrulesbuilt-in (handy for the declarative/SQL path); operational strategies (latency/cost) are a natural future built-in set — added as more factories, still no dispatch strings.Consideration 2 · ML/learned routing is bring-your-own, with embedding-similarity as the first built-in
Prior art — RouteLLM ships trained routers as in-process artifacts (BERT / causal-LLM /
sw_ranking); vLLM Semantic Router uses an in-process embedding classifier. Learned/content routing is typically an in-process artifact or embedding classifier, not an external service.Proposal — BYO for v1 via
RoutingStrategy; make embedding-similarity (a registered embedding-model resource) the first built-in ML path (no training pipeline needed); trained-classifier-as-artifact as a follow-up.Consideration 3 · Stay consistent with Flink SQL routing, but don't couple to it
Prior art — no LLM gateway is a SQL engine, so there's no direct analog; Flink's own
ML_PREDICT(FLINK-39961) is the closest.Proposal — keep semantics consistent with the SQL work, but don't couple or block the agents PR on it.
Cross-cutting — LiteLLM treats fallback / retries / timeout / caching as plain Router config, validating "fallback/cache are flags, not interfaces." (It also selects strategy by a name string, but only because it's config-driven — our in-code API passes strategy objects, matching LangChain4j/Camel.) The proposed API is squarely in line with the most-used LLM router in the wild.
Beta Was this translation helpful? Give feedback.
All reactions