11# libsy — Switchyard-Lib
22
3- A lightweight library for multi-LLM agent optimization, with ** routing** as the first case. An
4- algorithm decides — statefully, using more than the request — which model(s) to call and how, and
5- never performs the call itself ("ask, don't call"), so it owns no HTTP/provider SDK and embeds cleanly
6- in a proxy, gateway, or agent runtime.
3+ A lightweight library for multi-LLM agent optimization, with ** routing** as the first case.
4+ The library is provider agnostic and gives the user control of the llm api calls.
75
8- Two audiences:
6+ ## Example
97
10- - ** Integrators** — build a target set, pick an algorithm, run requests through a
11- [ ` MultiLlmOrchestrator ` ] ( #the-orchestrator--multillmorchestrator ) .
12- - ** Algorithm developers** — add strategies by [ implementing ` OrchAlgo ` ] ( #the-orchalgo-trait ) .
8+ Build a target set, pick an algorithm, run a request:
139
14- Narrative design: [ ` DESIGN.md ` ] ( DESIGN.md ) .
15-
16- ## Concepts
10+ ``` rust
11+ use libsy :: llm_class :: LlmClassifierOrchAlgo ;
12+ use libsy :: {LlmRequest , LlmTarget , LlmTargetSet , MultiLlmOrchestrator , OrchestratorRequest };
13+ use std :: sync :: Arc ;
1714
18- | Type | Role |
19- | ------| ------|
20- | ` OrchAlgo ` | The ** algorithm** : runs once per request, makes model calls, returns a decision trace + response. |
21- | ` LlmTarget ` | A ** target** : a ` name ` the algorithm routes by, the provider ` model ` id, and an optional ` LlmClient ` . |
22- | ` LlmTargetSet ` | The set of targets an algorithm routes among. |
23- | ` LlmClient ` | The one piece of I/O libsy doesn't own — a target's real model call over * your* transport. |
24- | ` MultiLlmOrchestrator ` | Drives one algorithm; the integrator's entry point. |
25- | ` DecisionTrace ` | Why the algorithm chose as it did — read uniformly, downcast when the algo is known. |
15+ // Targets the algorithm routes among, each backed by your LlmClient (see below).
16+ let client = Arc :: new (MyClient { /* .. */ }) as Arc <dyn LlmClient >;
17+ let t = | name : & str , model : & str | LlmTarget {
18+ name : name . into (), model : model . into (), llm_client : Some (client . clone ()),
19+ };
2620
27- Per request: ` MultiLlmOrchestrator ` → ` OrchAlgo::process_request ` → one or more ` LlmTarget::call `
28- (each ** served** by an ` LlmClient ` , or ** offloaded** to you) → ` (trace, response) ` .
21+ let algo = Arc :: new (LlmClassifierOrchAlgo :: new (
22+ " classifier" , " strong" , " weak" , 0.5 ,
23+ LlmTargetSet :: new (vec! [
24+ t (" classifier" , " openai/gpt-4o-mini" ),
25+ t (" strong" , " openai/gpt-4o" ),
26+ t (" weak" , " openai/gpt-4o-mini" ),
27+ ]),
28+ ));
29+ let orch = MultiLlmOrchestrator :: new (algo );
2930
30- ---
31+ let req = OrchestratorRequest {
32+ llm_request : LlmRequest { model_name : " auto" . into (), prompt : " explain tail latency" . into () },
33+ raw_request : None , metadata : None ,
34+ };
35+ let (trace , response ) = orch . orchestrate_direct (req ). await ? ; // one call in, trace + response out
36+ println! (" routed to {}" , trace . last (). unwrap (). model_decision ());
37+ ```
3138
32- # For integrators
39+ Runnable: [ ` examples/research_agent.rs ` ] ( examples/research_agent.rs ) .
3340
34- ## The orchestrator — ` MultiLlmOrchestrator `
41+ ## Requests & responses
3542
3643``` rust
37- impl MultiLlmOrchestrator {
38- pub fn new (algo : Arc <dyn OrchAlgo >) -> Self ; // the algorithm owns its target set
39-
40- // libsy holds every client: makes the calls, returns (trace, response).
41- // Errors up front if any target is client-less.
42- pub async fn orchestrate_direct (& self , request : OrchestratorRequest )
43- -> Result <(Vec <Arc <dyn DecisionTrace >>, OrchestratorResponse ), Box <dyn Error + Send + Sync >>;
44-
45- // "Ask, don't call": client-less targets stream back as `CallLlm` promises you
46- // fulfill, then a final `ReturnToAgent`.
47- pub fn orchestrate (& self , request : OrchestratorRequest )
48- -> impl Stream <Item = OrchestratorStepResult >;
44+ pub struct OrchestratorRequest {
45+ pub llm_request : LlmRequest , // normalized: { model_name, prompt }
46+ pub raw_request : Option <serde_json :: Value >, // original provider body, forwarded verbatim if present
47+ pub metadata : Option <Metadata >, // correlation: session / agent / task / correlation_id / extra
48+ }
4949
50- pub async fn process_signals (& self , signals : AgentSysSignals ) // out-of-band events
51- -> Result <(), Box <dyn Error + Send + Sync >>;
50+ pub struct OrchestratorResponse {
51+ pub llm_response : LlmResponse , // normalized: { completion, raw_response? }
52+ pub metadata : Option <Metadata >,
5253}
5354```
5455
55- One instance is cheap to share (` Arc<dyn OrchAlgo> ` inside, no lock) and serves requests from many
56- threads in parallel.
56+ ## Building a target set / using a client
5757
58- ## Building a target set
59-
60- Implement ` LlmClient ` over your transport, wrap each model as an ` LlmTarget ` , collect into an
61- ` LlmTargetSet ` :
58+ An ` LlmTarget ` pairs a routing ` name ` with a provider ` model ` id and an optional ` LlmClient ` .
59+ Although ` libsy ` provides a ` SwitchyardClient ` and a reference implementation, ` LlmClient `
60+ is designed to be implemented by the user.
6261
6362``` rust
6463struct MyClient { /* http client, base url, key */ }
@@ -72,40 +71,42 @@ impl LlmClient for MyClient {
7271 Ok (OrchestratorResponse { llm_response : LlmResponse { completion , raw_response : None }, metadata : None })
7372 }
7473}
75-
76- // `name` is the routing label; `model` is the provider id the client calls. They can differ.
77- let client = Arc :: new (MyClient { /* .. */ }) as Arc <dyn LlmClient >;
78- let target = | name : & str , model : & str | LlmTarget {
79- name : name . into (), model : model . into (),
80- llm_client : Some (client . clone ()), // `None` -> offloaded (streaming mode)
81- };
82- let targets = LlmTargetSet :: new (vec! [
83- target (" classifier" , " openai/gpt-4o-mini" ),
84- target (" strong" , " openai/gpt-4o" ),
85- target (" weak" , " openai/gpt-4o-mini" ),
86- ]);
8774```
8875
89- ## Running a request
76+ ` name ` is the label an algorithm routes by; ` model ` is the id the client calls — they can differ
77+ (` "strong" ` -> ` "openai/gpt-4o" ` ) or coincide. A target with ` llm_client: None ` is ** offloaded**
78+ instead of served (streaming, below). The planned optional ` SwitchyardClient ` will provide an
79+ ` LlmClient ` that maps in Switchyard's OpenAI / Anthropic / Responses translations, so you needn't
80+ hand-roll one.
81+
82+ ## The orchestrator — ` MultiLlmOrchestrator `
9083
91- ` orchestrate_direct ` when libsy holds the clients — one call in, trace + response out. Runnable:
92- [ ` examples/research_agent.rs ` ] ( examples/research_agent.rs ) .
84+ The entry point. Cheap to share ( ` Arc<dyn OrchAlgo> ` inside, no lock) and safe to drive from many
85+ threads at once:
9386
9487``` rust
95- use libsy :: llm_class :: LlmClassifierOrchAlgo ;
88+ impl MultiLlmOrchestrator {
89+ pub fn new (algo : Arc <dyn OrchAlgo >) -> Self ; // the algorithm owns its target set
9690
97- let algo = Arc :: new (LlmClassifierOrchAlgo :: new (" classifier" , " strong" , " weak" , 0.5 , targets ));
98- let orch = MultiLlmOrchestrator :: new (algo );
91+ // libsy holds every client: makes the calls, returns (trace, response).
92+ // Errors up front if any target is client-less.
93+ pub async fn orchestrate_direct (& self , request : OrchestratorRequest )
94+ -> Result <(Vec <Arc <dyn DecisionTrace >>, OrchestratorResponse ), Box <dyn Error + Send + Sync >>;
9995
100- let request = OrchestratorRequest {
101- llm_request : LlmRequest { model_name : " auto" . into (), prompt : " explain tail latency" . into () },
102- raw_request : None , metadata : None ,
103- };
104- let (trace , response ) = orch . orchestrate_direct (request ). await ? ;
105- println! (" routed to {}" , trace . last (). unwrap (). model_decision ());
96+ // "Ask, don't call": client-less targets stream back as `CallLlm` promises you
97+ // fulfill, then a final `ReturnToAgent`.
98+ pub fn orchestrate (& self , request : OrchestratorRequest )
99+ -> impl Stream <Item = OrchestratorStepResult >;
100+
101+ pub async fn process_signals (& self , signals : AgentSysSignals ) // out-of-band events
102+ -> Result <(), Box <dyn Error + Send + Sync >>;
103+ }
106104```
107105
108- ` orchestrate ` when your targets are client-less (` llm_client: None ` ) and * you* own the calls. Runnable:
106+ ## Streaming — you own the model calls (` orchestrate ` )
107+
108+ Build the targets client-less (` llm_client: None ` ) and every ` target.call ` is offloaded: ` orchestrate `
109+ yields ` CallLlm ` promises you fulfill with your own transport, then a final ` ReturnToAgent ` . Runnable:
109110[ ` examples/research_agent_core.rs ` ] ( examples/research_agent_core.rs ) .
110111
111112``` rust
@@ -122,21 +123,10 @@ while let Some(step) = stream.next().await {
122123}
123124```
124125
125- ## ` SwitchyardClient ` (planned, optional feature)
126-
127- An optional-feature ` LlmClient ` that maps in Switchyard's provider translations (OpenAI / Anthropic /
128- Responses) and calls an upstream endpoint for you — so integrators needn't hand-roll one while the
129- default build ships no transport. The [ ` demo/libsy-proxy ` ] ( ../../demo/libsy-proxy ) binary hand-rolls it
130- today.
131-
132- ---
133-
134- # For algorithm developers
135-
136- ## The ` OrchAlgo ` trait
126+ ## Building an algorithm (` OrchAlgo ` )
137127
138- ` process_request ` runs once per request and makes as many ` LlmTarget::call ` s as it needs (a router one,
139- a classifier two, an ensemble many):
128+ Implement ` OrchAlgo ` to add a strategy. ` process_request ` runs once per request and makes as many
129+ ` LlmTarget::call ` s as it needs.
140130
141131``` rust
142132#[async_trait]
@@ -151,32 +141,21 @@ pub trait OrchAlgo: Send + Sync {
151141}
152142```
153143
154- Give it a ` new(config.., target_set) ` constructor; integrators ` Arc ` -wrap it for
155- ` MultiLlmOrchestrator::new ` . There is no builder.
156-
157- ## Targets and decisions
144+ Give it a ` new(config.., target_set) ` constructor and ` Arc ` -wrap it for ` MultiLlmOrchestrator::new ` —
145+ there is no builder. A ` target.call ` serves the call if the target has a client, else offloads it via
146+ ` ctx ` — invisible to the algorithm. Attach a ` DecisionTrace ` to each call so a consumer can see * why *
147+ (it's a trait object, read uniformly, downcast when the algo is known):
158148
159149``` rust
160- pub struct LlmTarget { pub name : String , pub model : String , pub llm_client : Option <Arc <dyn LlmClient >> }
161-
162150pub trait DecisionTrace : Send + Sync {
163151 fn model_decision (& self ) -> & str ; // the model chosen
164152 fn reasoning (& self ) -> Option <& str >; // human-readable "why"
165- fn as_any (& self ) -> & dyn std :: any :: Any ; // downcast when the algo is known
153+ fn as_any (& self ) -> & dyn std :: any :: Any ; // downcast to the concrete decision
166154}
167155```
168156
169- ` LlmTarget::call(ctx, request, decision) ` serves the call if the target has a client, else offloads it
170- via ` ctx ` 's channel — invisible to the algorithm. The target stamps its ` model ` onto
171- ` request.llm_request.model_name ` , so an algorithm routes by the logical ` name ` and the client still
172- hits the concrete model. ` DecisionTrace ` is a trait object (not a generic), so a consumer reads any
173- algorithm's decision uniformly.
174-
175- ## Implementing a router (LLM classifier, minimized)
176-
177- Two ` target.call ` s in one ` process_request ` : classify, then route. (Full version in
178- [ ` src/llm_class.rs ` ] ( src/llm_class.rs ) ; random router in [ ` src/rand.rs ` ] ( src/rand.rs ) , ensemble in
179- [ ` src/ensemble.rs ` ] ( src/ensemble.rs ) .)
157+ Example — the LLM classifier (classify, then route; full version in
158+ [ ` src/llm_class.rs ` ] ( src/llm_class.rs ) ):
180159
181160``` rust
182161#[async_trait]
@@ -200,12 +179,26 @@ impl OrchAlgo for LlmClassifierOrchAlgo {
200179}
201180```
202181
203- ## Reference algorithms
182+ ## Explore
183+
184+ ** Reference algorithms** — implementations to read and route with:
185+
186+ - [ ` src/rand.rs ` ] ( src/rand.rs ) — ` RandomOrchAlgo ` : uniform random over the set (one call).
187+ - [ ` src/llm_class.rs ` ] ( src/llm_class.rs ) — ` LlmClassifierOrchAlgo ` : classify, then route strong/weak;
188+ fail open to strong.
189+ - [ ` src/ensemble.rs ` ] ( src/ensemble.rs ) — ` EnsembleOrchAlgo ` : stateful — fan out to candidates, judge
190+ the best, commit to the winner after N exploration turns
191+
192+ ** Runnable examples** (` cargo run -p libsy --example <name> ` ):
193+
194+ - [ ` examples/research_agent.rs ` ] ( examples/research_agent.rs ) — client-backed targets, ` orchestrate_direct `
195+ (libsy makes the calls).
196+ - [ ` examples/research_agent_core.rs ` ] ( examples/research_agent_core.rs ) — client-less targets,
197+ ` orchestrate ` stream (the agent makes the calls).
204198
205- - ** ` RandomOrchAlgo ` ** — uniform random over the set (one call).
206- - ** ` LlmClassifierOrchAlgo ` ** — classify, then route strong/weak; fail open to strong.
207- - ** ` EnsembleOrchAlgo ` ** — stateful: fan out to candidates, judge the best, commit after N exploration
208- turns. State sits behind a ` Mutex ` over just its own fields.
199+ ** Demo proxy** — [ ` demo/libsy-proxy ` ] ( ../../demo/libsy-proxy ) : a real HTTP proxy where switchyard's
200+ crates serve the OpenAI / Anthropic / Responses APIs and translate formats, while * all* routing is
201+ libsy's classifier calling upstream through switchyard's backend.
209202
210203## Not yet built
211204
0 commit comments