libsy-llm-client
An HTTP client that speaks Switchyard's neutral IR directly. You hand it a
switchyard_protocol::Request and a model name; it looks up the configured backend,
encodes the request to that backend's wire format, adds auth and forwards your
headers, makes the call with a shared reqwest::Client, and decodes the reply
back into a switchyard_protocol::Response — buffered or streamed.
It depends only on switchyard-protocol and switchyard-translation; no server, no
provider SDK.
Concepts
- Model configs. A client is built from
ModelConfigvalues. Each model has a defaultBackendand can have additional backends for other wire formats.TranslatingLlmClient::call_rewrite_modeluses the request's metadata wire format when set, otherwise the model's default backend. - Backends. A
Backendis one ofOpenAiChat,OpenAiResponses, orAnthropic, each wrapping anHttpBackendConfig(base_url,api_key, staticextra_headers, defaultextra_bodyfields, andmax_retries). The variant fixes the URL path and auth scheme (Bearer vsx-api-key+anthropic-version). - Model rewrite. The resolved model name is both the map key and the model id
sent upstream — it overwrites whatever
modelthe request arrived with. - Streaming is chosen by the request. If the encoded body has
stream: true(i.e.request.llm_request.stream), you getLlmResponse::Stream; otherwiseLlmResponse::Agg. OpenAI Chat streaming requests defaultstream_options.include_usagetotrue; an explicit caller value is preserved.
Add the dependency
Within this workspace:
[]
= { = "../libsy-llm-client" }
= { = "../libsy-protocol" }
= { = "../switchyard-translation" } # for WireFormat
Quickstart
Build a client
use BTreeMap;
use ;
Buffered call
use ;
use ;
async
Streaming call
Set stream on the IR request and drive the returned chunk stream:
use StreamExt;
use TranslatingLlmClient;
use ;
async
Cross-format translation
The request/response are translated through the neutral IR, so the inbound shape
you build and the backend's wire format are independent. Pointing a
WireFormat::AnthropicMessages backend at an Anthropic endpoint while building
requests with the same helpers works the same way. Set request.metadata.wire_format
to select a non-default backend before calling call_rewrite_model. Register several
formats under one model to serve it over more than one upstream API:
use ;
Headers & auth
- The
Backendvariant sets auth: OpenAI formats sendAuthorization: Bearer <key>; Anthropic sendsx-api-key: <key>plusanthropic-version. request.metadata.http_headersare forwarded upstream, except reserved ones:host,content-length,connection, and the backend-ownedauthorization/x-api-key/anthropic-version/content-type. So a caller's placeholder credential never overrides the backend's real key.- Per-backend static headers go in
HttpBackendConfig::extra_headers. - Per-target top-level request defaults go in
HttpBackendConfig::extra_body. The merge is shallow and fields already present in the request take precedence. HttpBackendConfig::max_retriescontrols additional attempts after retryable transport failures, timeouts, HTTP 408/429, and 5xx responses. Buffered body transport failures are retried; streaming body failures are not replayed after the response has been returned.
Retries replay the same upstream request. A transport failure can therefore
duplicate a request that the provider processed but did not finish returning,
and the retry budget plus capped Retry-After delays determines total latency.
Errors
call_rewrite_model returns LlmClientError:
| Variant | When |
|---|---|
InvalidRequest { message } |
the request does not identify a model |
Configuration { message } |
the model or requested wire format has no configured backend |
RequestTranslation(msg) |
decoding the inbound request failed in the translation engine |
RequestEncoding(msg) |
re-encoding an already-decoded request to the wire format failed (internal fault) |
ResponseTranslation(msg) |
response decoding or encoding failed in the translation engine |
Timeout { source } |
request or response body read exceeded its timeout |
Transport { source } |
non-timeout connection or transport failure |
ContextWindowExceeded { model, message } |
upstream 400 detected as a context overflow (checked before UpstreamHttp, so callers can evict-and-retry) |
UpstreamHttp { status, body } |
any other non-2xx upstream response |
InvalidResponse { source } |
the upstream response could not be decoded |
Other(source) |
a client-specific failure outside the shared categories |