API Reference¶
Auto-generated from docstrings. For narrative guides, see Guides.
Runtime¶
byoai.Runtime ¶
Runtime(*, llm: dict[str, Any] | None = None, providers: list[LLMProvider | Callable[..., Any]] | None = None, cache: dict[str, Any] | CacheStore | None = None, vector_store: dict[str, Any] | VectorStore | Callable[..., Any] | None = None, semantic_cache: dict[str, Any] | SemanticCacheStore | None = None, embedder: dict[str, Any] | Embedder | None = None, retry_policy: RetryPolicy | None = None, selection: SelectionName | SelectionFn = 'ordered', system_prompt: str | None = None, telemetry: Any | None = None)
The execution engine: owns middleware, pipelines, events, provider
routing, caching and usage accounting. Configure it declaratively
(llm={...}, cache={...}) or with pre-built adapter instances;
both styles compose.
Source code in src/byoai/runtime.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
aclose
async
¶
aclose() -> None
Alias for :meth:close, matching async-native naming (httpx, anyio).
Source code in src/byoai/runtime.py
415 416 417 | |
close
async
¶
close() -> None
Close every adapter the runtime owns (providers, caches, vector
stores, embedder, telemetry). Prefer async with Runtime(...).
Source code in src/byoai/runtime.py
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 | |
on ¶
on(event: str, handler: EventHandler) -> Any
Subscribe to lifecycle events (supports * wildcards).
Source code in src/byoai/runtime.py
177 178 179 | |
stream
async
¶
stream(input: Any, *, pipeline: str | Pipeline | None = None, session_id: str | None = None, user_id: str | None = None, model: str | None = None, system_prompt: str | None = None, metadata: dict[str, Any] | None = None, provider_metadata: dict[str, Any] | None = None, filters: dict[str, Any] | None = None, **provider_options: Any) -> AsyncGenerator[StreamChunk, None]
Run the pipeline in streaming mode and yield token chunks.
The pipeline prepares the context (history, cache policy, retrieval, prompt); the terminal provider call streams. A middleware/stage that short-circuits with a response yields a single chunk.
Source code in src/byoai/runtime.py
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 | |
use ¶
use(middleware: MiddlewareLike) -> Runtime
Add a middleware wrapping every execution. Chainable.
Source code in src/byoai/runtime.py
172 173 174 175 | |
Pipeline¶
byoai.Pipeline ¶
Pipeline(name: str = 'default')
An ordered list of stages executed against one :class:RequestContext.
Stages run sequentially; a stage calling ctx.short_circuit() stops the
rest. Mutate with :meth:add, :meth:remove, :meth:replace.
Source code in src/byoai/pipeline.py
49 50 51 | |
add ¶
add(stage: PipelineStage | Callable[[RequestContext], Awaitable[None]]) -> Pipeline
Append a stage. Accepts stage objects or bare async functions. Chainable.
Source code in src/byoai/pipeline.py
57 58 59 60 61 62 | |
remove ¶
remove(stage_type: type | None = None, *, name: str | None = None) -> Pipeline
Remove every stage matching stage_type and/or name (both, if
both given). stage_type alone matches every stage of that type —
for two or more bare-function stages (every one of them is a
FunctionStage once add() wraps it), pass name= too (a bare
function's stage name defaults to fn.__name__) to target one
specifically instead of removing all of them at once.
Source code in src/byoai/pipeline.py
71 72 73 74 75 76 77 78 79 80 81 | |
replace ¶
replace(stage_type: type | None = None, replacement: PipelineStage | None = None, *, name: str | None = None) -> Pipeline
Replace every stage matching stage_type and/or name with
replacement. Same multiple-bare-function-stages caveat as
:meth:remove — pass name= to target one specifically.
Source code in src/byoai/pipeline.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
byoai.PipelineStage ¶
Bases: Protocol
byoai.FunctionStage ¶
FunctionStage(fn: Callable[[RequestContext], Awaitable[None]], name: str | None = None)
Adapts a bare async function into a stage.
Source code in src/byoai/pipeline.py
32 33 34 35 36 | |
Middleware¶
byoai.Middleware ¶
Base class. Subclass and override __call__; plain async callables with
the same (ctx, call_next) signature are also accepted by the chain.
Request context & results¶
byoai.RequestContext
dataclass
¶
RequestContext(input: Any, pipeline_name: str | None = None, request_id: str = (lambda: uuid.uuid4().hex)(), session_id: str | None = None, user_id: str | None = None, system_prompt: str | None = None, messages: list[Message] = list(), documents: list[Document] = list(), response: str | None = None, model: str | None = None, provider: str | None = None, finish_reason: str | None = None, raw_response: Any = None, usage: Usage = Usage(), cached: bool = False, short_circuited: bool = False, started_at: float = time.monotonic(), state: dict[str, Any] = dict(), metadata: dict[str, Any] = dict())
Carries one request through the runtime.
input is the caller's raw payload, untouched. Stages append their
products to the typed slots below (messages, documents, response)
or to the free-form state dict for anything stage-specific.
short_circuit ¶
short_circuit(response: str, *, cached: bool = False) -> None
Set a final response and stop remaining pipeline stages from running.
Used by cache middleware/stages on a hit, or by guardrail middleware rejecting a request.
Source code in src/byoai/context.py
62 63 64 65 66 67 68 69 70 | |
byoai.ExecutionResult
dataclass
¶
ExecutionResult(content: str, context: RequestContext, usage: Usage = Usage(), cached: bool = False, model: str | None = None, provider: str | None = None, metadata: dict[str, Any] = dict(), finish_reason: str | None = None, raw: Any = None)
Final result of runtime.execute().
Porting from the raw anthropic/openai SDKs: content here is
always a flattened str, unlike anthropic.Message.content (a list
of typed content blocks) — a pure tool_use turn comes back as "",
not a list to iterate. Block-level detail (tool_use, response id,
prompt-cache token counts) lives on raw instead; see the
docs/guides/providers.md#anthropic-tool-use-and-content-blocks guide
for the per-adapter shape of raw and a worked tool-use example.
byoai.Message
dataclass
¶
Message(role: Role, content: str | list[dict[str, Any]] | None, name: str | None = None, metadata: dict[str, Any] = dict(), tool_call_id: str | None = None, tool_calls: list[dict[str, Any]] | None = None)
content is plain text, a list of provider content blocks (e.g.
Anthropic tool_use/tool_result/image blocks — only the
Anthropic adapters, providers/anthropic.py and
providers/anthropic_cloud.py, are safe to use those with; Gemini/
OpenAI-compat expect plain strings and will error or mishandle a list),
or None on an OpenAI-shaped assistant turn whose only content is a
tool call (tool_calls set, mirroring OpenAI's own wire format).
tool_call_id/tool_calls are the OpenAI-compatible-family
counterpart to Anthropic's content-block tool use: to send a tool result
back, append the assistant's own tool-call message (role="assistant",
content=None, tool_calls=[{"id": ..., "type": "function",
"function": {"name": ..., "arguments": "..."}}] — the shape
ExecutionResult.raw/a streamed turn's assembled tool_calls are
already in) followed by one role="tool" message per call
(tool_call_id=<call id>, content=<the tool's output as a string>).
Anthropic instead round-trips through list-valued content blocks —
see docs/guides/providers.md for both worked examples. Unused by
Anthropic/Bedrock/Vertex/Gemini.
byoai.Usage
dataclass
¶
Usage(input_tokens: int = 0, output_tokens: int = 0, cost_usd: float = 0.0, cache_read_tokens: int = 0, cache_creation_tokens: int = 0)
Token/cost accounting for one or more provider calls. Additive.
byoai.StreamChunk
dataclass
¶
StreamChunk(delta: str = '', done: bool = False, model: str | None = None, provider: str | None = None, usage: Usage | None = None, raw: Any = None, cached: bool = False, request_id: str | None = None, finish_reason: str | None = None, tool_call: ToolCallDelta | None = None)
One streamed increment. delta is the new text; done marks the end.
tool_call is set instead of delta when the increment is a
streamed tool-use argument fragment rather than text — the two are
mutually exclusive per chunk.
cached/request_id are only ever set on the final done chunk
Runtime.stream() yields (mirroring ExecutionResult), never on individual
provider-level deltas.
byoai.Document
dataclass
¶
Document(id: str, content: str, metadata: dict[str, Any] = dict(), score: float | None = None, embedding: list[float] | None = None)
A retrieved vector-store document, normalized across providers.
byoai.ProviderResponse
dataclass
¶
ProviderResponse(content: str, model: str, provider: str, usage: Usage = Usage(), finish_reason: str | None = None, raw: Any = None)
Normalized non-streaming completion from any provider adapter.
Errors¶
byoai.ByoAIError ¶
Bases: Exception
Base class for all runtime errors.
byoai.ConfigurationError ¶
Bases: ByoAIError
Invalid or missing runtime configuration.
byoai.ProviderError ¶
ProviderError(message: str, *, provider: str, status_code: int | None = None, retryable: bool = False, retry_after: float | None = None)
Bases: ByoAIError
Normalized provider failure.
Attributes:
| Name | Type | Description |
|---|---|---|
provider |
name of the provider adapter that raised. |
|
status_code |
HTTP status from the provider, if any. |
|
retryable |
whether the router may retry this call. |
|
retry_after |
server-suggested delay in seconds, if the provider sent one. |
Source code in src/byoai/errors.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
byoai.AllProvidersFailedError ¶
AllProvidersFailedError(message: str, errors: list[ProviderError])
Bases: ByoAIError
Every provider in the fallback chain failed.
Source code in src/byoai/errors.py
74 75 76 | |
byoai.RateLimitError ¶
RateLimitError(message: str, *, provider: str, retry_after: float | None = None)
Bases: ProviderError
Source code in src/byoai/errors.py
61 62 63 64 65 66 67 68 | |
byoai.CacheError ¶
Bases: ByoAIError
A cache adapter failed. Cache failures should generally be non-fatal.
byoai.VectorStoreError ¶
Bases: ByoAIError
A vector store adapter failed.
byoai.FilterError ¶
Bases: ByoAIError
An AST filter expression is malformed or unsupported by the target dialect.
byoai.MiddlewareError ¶
Bases: ByoAIError
A middleware failed outside of normal short-circuiting.
byoai.PipelineError ¶
PipelineError(message: str, *, stage: str | None = None)
Bases: ByoAIError
A pipeline stage failed while executing.
Source code in src/byoai/errors.py
21 22 23 | |
byoai.PipelineNotFoundError ¶
Bases: ByoAIError, LookupError
A named pipeline was requested but never registered.
Cache adapters¶
byoai.cache.base.CacheStore ¶
Bases: Protocol
read_session
async
¶
read_session(**params: str) -> Any | None
Read existing application state through the configured pattern.
Params fill the pattern's placeholders, e.g.
read_session(user_id="usr_1") with pattern
app:users:{user_id}:chat_history.
Source code in src/byoai/cache/base.py
27 28 29 30 31 32 33 34 | |
byoai.cache.memory.MemoryCache ¶
MemoryCache(*, namespace: str = 'byoai:', default_ttl: int | None = 3600, session_reader: dict[str, str] | None = None, session_data: dict[str, Any] | None = None, max_size: int | None = None)
Source code in src/byoai/cache/memory.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
byoai.cache.redis.RedisCache ¶
RedisCache(*, url: str = 'redis://localhost:6379', namespace: str = 'byoai:', session_reader: dict[str, str] | None = None, client: Any | None = None, default_ttl: int | None = 3600, mode: str = 'standalone', sentinels: list | None = None, service_name: str | None = None, **client_kwargs: Any)
**client_kwargs (e.g. socket_timeout, socket_connect_timeout,
retry_on_timeout, health_check_interval, ssl, ssl_ca_certs)
are forwarded to the underlying redis-py client when client= isn't
supplied directly — see redis-py's connection docs for the full set.
Source code in src/byoai/cache/redis.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
read_session
async
¶
read_session(**params: str) -> Any | None
Read existing app state via the configured key pattern. Never writes.
Two sequential round trips (TYPE then LRANGE/GET) — the
key's shape isn't known ahead of time, and Redis has no single
command that fetches "whatever this key is." ContextResolver
calls this on every request that has both cache= and history
reading enabled (max_history_messages > 0, the default), so this
is real per-request Redis latency on top of the LLM call itself, not
a one-time cost.
Source code in src/byoai/cache/redis.py
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
byoai.cache.redis.make_redis_client ¶
make_redis_client(*, url: str = 'redis://localhost:6379', mode: str = 'standalone', sentinels: list[tuple[str, int]] | list[list] | None = None, service_name: str | None = None, **client_kwargs: Any) -> Any
Build an async Redis client for the deployment you already run.
standalone(default) — single node or Valkey, viaurl.cluster— Redis Cluster, viaurlpointing at any node.sentinel— Sentinel-managed master: passsentinelsas[(host, port), ...]and theservice_name.
Requires the redis extra: pip install byoai-runtime[redis].
Source code in src/byoai/cache/redis.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
Semantic (intent) cache¶
byoai.cache.semantic.SemanticCacheStore ¶
Bases: Protocol
find
async
¶
find(embedding: list[float], *, threshold: float) -> tuple[str, float] | None
Best cached response scoring >= threshold under the store's
configured metric (cosine similarity by default), as
(response, score); None on miss.
Source code in src/byoai/cache/semantic.py
79 80 81 82 83 84 85 | |
byoai.cache.semantic.MemorySemanticCache ¶
MemorySemanticCache(*, capacity: int = 10000, ttl: int | None = 3600, metric: MetricName | SimilarityFn = 'cosine')
Ring-buffer semantic cache: fixed capacity, oldest entries evicted.
TTL is wall-clock seconds per entry (None = no expiry) — note the
constructor arg here is ttl, not default_ttl like
:class:~byoai.cache.memory.MemoryCache/
:class:~byoai.cache.redis.RedisCache: this store has no per-call
ttl= override to be a default for (:meth:add takes no ttl=
param), so there's only ever the one constructor-level value. metric
selects
how a query vector is scored against stored ones — a preset name or a
bare callable:
"cosine"(default) — cosine similarity, range [-1, 1]. Vectors are L2-normalized at insert and query time so a hit is a single matrix-vector product. The module docstring'sthresholdguidance (0.85-0.95+) assumes this metric."dot"— raw inner product, no normalization. Useful when an embedding model's vector magnitude is itself meaningful."euclidean"— negative squared Euclidean distance (higher = closer, unbounded range), no normalization.- a bare callable
(matrix, vector) -> scores, one score per stored row, higher = more similar — receives raw (non-normalized) vectors. Whatever range your callable produces is the rangethresholdgets compared against. Keep it cheap: :meth:findonly offloads scoring to a worker thread past_OFFLOAD_MIN_ROWSentries, a threshold sized for the built-in presets' numpy matrix-vector product — a custom callable doing real per-row work in Python runs inline (and can stall the event loop) below that row count the same as it would above it if it's expensive, since there's no way to know a callable's cost ahead of time.
Source code in src/byoai/cache/semantic.py
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | |
add
async
¶
add(embedding: list[float], response: str, *, expires_at: float | None = None) -> None
Store an entry. expires_at (monotonic clock) overrides the
store's TTL — used by shared/persistent backends replaying entries
whose remaining lifetime differs from a fresh one.
Source code in src/byoai/cache/semantic.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | |
byoai.cache.semantic.RedisSemanticCache ¶
RedisSemanticCache(*, url: str = 'redis://localhost:6379', stream: str = 'byoai:semcache', capacity: int = 10000, ttl: int | None = 3600, metric: MetricName | SimilarityFn = 'cosine', client: Any | None = None, mode: str = 'standalone', sentinels: list | None = None, service_name: str | None = None, approximate_trim: bool = True, **client_kwargs: Any)
Shared, persistent semantic cache on an existing Redis/Valkey.
Entries live in one Redis Stream under the isolated byoai: namespace
(embedding packed as base64 float32 + response + wall-clock expiry).
Every worker keeps a local numpy mirror and catches up incrementally
(XRANGE from its last-seen id) before each lookup — usually an empty
round-trip. So intent hits are shared across processes/replicas and
survive restarts, while similarity math stays local and fast.
XTRIM MAXLEN ~capacity bounds the stream; expiry is enforced at
lookup time via each entry's wall-clock deadline. metric selects the
similarity scoring — same presets/callable escape hatch as
:class:MemorySemanticCache, which does the actual scoring here.
Requires the redis and semantic extras.
Source code in src/byoai/cache/semantic.py
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | |
Vector store adapters¶
byoai.vector.base.VectorStore ¶
Bases: Protocol
byoai.vector.base.FunctionVectorStore ¶
FunctionVectorStore(fn: Callable[..., Awaitable[list[Document]]], *, name: str | None = None)
Adapts a bare async search function into a :class:VectorStore — for
a custom retrieval backend that doesn't fit a declarative
vector_store={...} config. search() is the only operation this
protocol has, so one function is the whole adapter — no class needed:
async def my_search(embedding: list[float], *, top_k=5, filters=None) -> list[Document]:
...
Runtime(vector_store=my_search) # auto-wrapped
Source code in src/byoai/vector/base.py
56 57 58 59 60 61 62 63 | |
byoai.vector.pgvector.PgVectorStore ¶
PgVectorStore(*, dsn: str | None = None, table: str, schema_map: dict[str, str] | None = None, metric: PgMetric = 'cosine', pool: Any | None = None, min_pool_size: int = 1, max_pool_size: int = 5, command_timeout: float | None = None, **pool_kwargs: Any)
metric must match the vector_*_ops operator class the table's index
was actually built with: "cosine" (default, vector_cosine_ops), "l2"
(vector_l2_ops), or "inner_product" (vector_ip_ops).
**pool_kwargs (e.g. server_settings={"statement_timeout": "..."},
ssl=..., max_inactive_connection_lifetime=...) are forwarded
to asyncpg.create_pool when pool= isn't supplied directly.
Source code in src/byoai/vector/pgvector.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
byoai.vector.qdrant.QdrantVectorStore ¶
QdrantVectorStore(*, url: str = 'http://localhost:6333', collection: str, api_key: str | None = None, schema_map: dict[str, Any] | None = None, timeout: float = 30.0, client: AsyncClient | None = None, with_vectors: bool = False, score_threshold: float | None = None, search_params: dict[str, Any] | None = None)
Source code in src/byoai/vector/qdrant.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
byoai.vector.pinecone.PineconeVectorStore ¶
PineconeVectorStore(*, host: str | None = None, api_key: str | None = None, namespace: str = '', schema_map: dict[str, str] | None = None, timeout: float = 30.0, client: AsyncClient | None = None, include_values: bool = False, sparse_vector: dict[str, Any] | None = None)
Source code in src/byoai/vector/pinecone.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
Provider adapters¶
byoai.providers.base.LLMProvider ¶
Bases: Protocol
byoai.providers.base.FunctionProvider ¶
FunctionProvider(fn: Callable[..., Awaitable[str | ProviderResponse]], *, name: str | None = None, model: str = '', stream_fn: Callable[..., AsyncIterator[str | StreamChunk]] | None = None)
Adapts a bare async function into an :class:LLMProvider — for a
custom or gateway-wrapped backend that doesn't fit a declarative
llm={...} config. Mirrors Pipeline's FunctionStage and the
embedder= callable pattern: give the runtime one function, not a class.
async def my_gateway(messages: list[Message], **options) -> str:
response = await my_existing_client.create(...)
return response.text
Runtime(providers=[my_gateway]) # auto-wrapped — no class needed
fn may return a plain str (model/usage/finish_reason default to
empty/zero) or a full :class:ProviderResponse when you want those
tracked. providers=[...] passed to :class:ProviderRouter auto-wraps
any bare callable this way; you rarely need to construct this directly.
Source code in src/byoai/providers/base.py
298 299 300 301 302 303 304 305 306 307 308 309 | |
byoai.providers.router.ProviderRouter ¶
ProviderRouter(providers: Sequence[LLMProvider | Callable[..., Any]], *, retry_policy: RetryPolicy | None = None, selection: SelectionName | SelectionFn = 'ordered', event_bus: EventBus | None = None)
Tries providers in the order selection picks for each call:
retryable failures back off and retry up to retry_policy.max_retries,
then routing falls through to the next provider in that order; when every
provider fails, :class:AllProvidersFailedError carries the accumulated
errors.
selection — a preset name or a bare callable:
"ordered"(default) — always tryprovidersin the order given; identical to the router's original fixed-primary/fallback behavior."round_robin"— rotates which provider goes first each call, so load spreads across providers instead of always preferring the first; a failure still falls through the rest in ring order, so nothing is skipped, only reprioritized.- a bare callable
(providers) -> providers, returning the providers to try, in order, for this call — e.g. weighted selection. A callable that also filters (e.g. dropping providers it considers unhealthy) excludes them entirely for that call, by its own choice; it is not a pure reprioritization like the two presets above.
providers accepts LLMProvider instances or bare async
functions — a callable without a complete attribute is
auto-wrapped in :class:FunctionProvider, same as Pipeline.add()
auto-wraps a bare function into a FunctionStage.
Source code in src/byoai/providers/router.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
stream
async
¶
stream(messages: list[Message], **options: Any) -> AsyncIterator[StreamChunk]
Stream from the first provider that starts successfully.
Fallback happens only if a provider fails before yielding any content; once tokens have been emitted downstream, a mid-stream failure is raised as-is (the transport already sent partial output).
Source code in src/byoai/providers/router.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
byoai.providers.router.RetryPolicy
dataclass
¶
RetryPolicy(max_retries: int = 2, base_delay: float = 0.5, max_delay: float = 10.0, jitter: float = 0.25)
Retry/backoff knobs for :class:ProviderRouter: exponential backoff
from base_delay capped at max_delay, with proportional jitter;
a server-provided Retry-After wins (still capped at max_delay).
byoai.providers.openai_compat.OpenAICompatProvider ¶
OpenAICompatProvider(*, model: str, api_key: str | None = None, base_url: str = 'https://api.openai.com/v1', name: str = 'openai', timeout: float = 60.0, default_headers: dict[str, str] | None = None, default_params: dict[str, str] | None = None, client: AsyncClient | None = None, chat_path: str = '/chat/completions', retryable_status: frozenset[int] | set[int] | None = None)
Source code in src/byoai/providers/openai_compat.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
byoai.providers.anthropic.AnthropicProvider ¶
AnthropicProvider(*, model: str, api_key: str | None = None, base_url: str = 'https://api.anthropic.com', name: str = 'anthropic', timeout: float = 60.0, max_tokens: int = 4096, client: AsyncClient | None = None, api_version: str = DEFAULT_API_VERSION, default_headers: dict[str, str] | None = None, retryable_status: frozenset[int] | set[int] | None = None, messages_path: str = '/v1/messages', cache_system: bool = False)
Source code in src/byoai/providers/anthropic.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
byoai.providers.anthropic_cloud.AnthropicBedrockProvider ¶
AnthropicBedrockProvider(*, model: str, name: str = 'bedrock', max_tokens: int = 4096, aws_region: str | None = None, aws_access_key: str | None = None, aws_secret_key: str | None = None, aws_session_token: str | None = None, aws_profile: str | None = None, default_headers: dict[str, str] | None = None, retryable_status: frozenset[int] | set[int] | None = None, client: Any | None = None, cache_system: bool = False)
Bases: _AnthropicSDKProviderBase
Anthropic models via AWS Bedrock.
Only aws_region is required here; credentials come from the standard
AWS chain (env vars, ~/.aws, or an instance/task role) unless passed
explicitly. Declarative: llm={"provider": "bedrock", "model": "...",
"aws_region": "us-east-1"}.
Source code in src/byoai/providers/anthropic_cloud.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |
byoai.providers.anthropic_cloud.AnthropicVertexProvider ¶
AnthropicVertexProvider(*, model: str, name: str = 'vertex', max_tokens: int = 4096, project_id: str | None = None, region: str | None = None, access_token: str | None = None, credentials: Any | None = None, default_headers: dict[str, str] | None = None, retryable_status: frozenset[int] | set[int] | None = None, client: Any | None = None, cache_system: bool = False)
Bases: _AnthropicSDKProviderBase
Anthropic models via Google Vertex AI.
project_id and region are required; credentials come from
Application Default Credentials unless access_token/credentials
is passed explicitly. Declarative: llm={"provider": "vertex",
"model": "...", "project_id": "...", "region": "us-east5"}.
Source code in src/byoai/providers/anthropic_cloud.py
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | |
byoai.providers.gemini.GeminiProvider ¶
GeminiProvider(*, model: str, api_key: str | None = None, base_url: str = 'https://generativelanguage.googleapis.com/v1beta', name: str = 'gemini', timeout: float = 60.0, client: AsyncClient | None = None, default_headers: dict[str, str] | None = None, retryable_status: frozenset[int] | set[int] | None = None)
Source code in src/byoai/providers/gemini.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
byoai.providers.embeddings.OpenAICompatEmbedder ¶
OpenAICompatEmbedder(*, model: str, api_key: str | None = None, base_url: str = 'https://api.openai.com/v1', name: str = 'openai', timeout: float = 30.0, default_headers: dict[str, str] | None = None, client: AsyncClient | None = None, embeddings_path: str = '/embeddings', retryable_status: frozenset[int] | set[int] | None = None, max_batch_size: int | None = None)
Source code in src/byoai/providers/embeddings.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
Pipeline stages¶
byoai.stages.ContextResolver ¶
ContextResolver(*, system_prompt: str | None = None, cache: CacheStore | None = None, session_params: Callable[[RequestContext], dict[str, str]] | None = None, max_history_messages: int = 20)
Normalize ctx.input into ctx.messages.
Accepts
str— becomes a single user message{"messages": [{"role": ..., "content": ...}, ...]}{"query"|"input"|"prompt": str, ...}list[Message]/ list of role-content dicts
Optionally prepends a system prompt and existing session history read (read-only) from the cache adapter's session reader.
Source code in src/byoai/stages.py
56 57 58 59 60 61 62 63 64 65 66 67 | |
byoai.stages.CacheLookup ¶
CacheLookup(cache: CacheStore, *, bus: EventBus | None = None, extra_fingerprint: Callable[[RequestContext], Any] | None = None)
Exact-match response cache. Short-circuits the pipeline on a hit.
The cache key fingerprints the normalized messages, model, pipeline,
provider options (temperature, top_p, ...) and retrieval filters, so
requests that differ only in those fields never collide on the same
entry. The runtime writes the response back (with the cache's own
default_ttl) after a successful non-streamed, non-cached execution.
Source code in src/byoai/stages.py
413 414 415 416 417 418 419 420 421 422 423 424 | |
byoai.stages.SemanticCacheLookup ¶
SemanticCacheLookup(store: Any, embedder: Embedder, *, threshold: float = DEFAULT_SEMANTIC_THRESHOLD, bus: EventBus | None = None)
Intent cache: short-circuit when a similar (not identical) query was already answered. Runs after the exact-match cache — exact hits are cheaper (no embedding call). On a miss, the query embedding is kept on the context so the runtime can store the eventual response for future intent hits.
Streaming requests participate too: a hit streams back as a single chunk.
Source code in src/byoai/stages.py
512 513 514 515 516 517 518 519 520 521 522 523 | |
byoai.stages.VectorRetrieve ¶
VectorRetrieve(store: VectorStore, embedder: Embedder, *, top_k: int = 5, filters: dict[str, Any] | None = None, bus: EventBus | None = None, format_document: Callable[[Document], str] | None = None, context_header: str = 'Relevant context retrieved for this request:', insert_at: Callable[[RequestContext], int] | None = None)
Retrieve documents from an existing vector store for the last user message.
embedder is supplied by the application (it owns embedding model choice);
ByoAI executes the retrieval. Filters come from ctx.state['filters'] or
the constructor default, in the unified AST dialect.
Source code in src/byoai/stages.py
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 | |
byoai.stages.ProviderCall ¶
ProviderCall(router: ProviderRouter, **default_options: Any)
Terminal stage: call the provider router with the built messages.
In streaming mode (ctx.state[STATE_STREAMING]) this stage is a no-op —
the runtime streams from the router itself after the pipeline finishes
preparing the context.
Source code in src/byoai/stages.py
618 619 620 | |
Telemetry (OpenTelemetry)¶
byoai.telemetry.otel.instrument ¶
instrument(runtime: Runtime, *, tracer_provider: Any | None = None) -> Runtime
Attach tracing to a runtime: request spans, per-stage child spans, and provider lifecycle span events.
Source code in src/byoai/telemetry/otel.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
byoai.telemetry.otel.configure_otlp ¶
configure_otlp(*, endpoint: str, service_name: str = 'byoai-runtime', headers: dict[str, str] | None = None, protocol: str = 'grpc', timeout: float | None = None, compression: str | None = None, resource_attributes: dict[str, Any] | None = None, max_queue_size: int | None = None, schedule_delay_millis: int | None = None, max_export_batch_size: int | None = None, export_timeout_millis: int | None = None) -> Any
Create an SDK TracerProvider exporting OTLP to an existing collector
(Grafana Tempo, Datadog agent, Honeycomb, Jaeger, ...). Returns the
provider — pass it to :func:instrument.
protocol is "grpc" (default, port 4317) or "http"/"http/protobuf"
(port 4318) — many collectors behind corporate ingress only allow the
latter. compression is "gzip" or None. The max_queue_size/
schedule_delay_millis/max_export_batch_size/export_timeout_millis
knobs tune the batch processor; unset ones use the OTel SDK's defaults.
resource_attributes adds to (not replaces) service.name — e.g.
{"service.version": "1.2.0", "deployment.environment": "prod"}.
Source code in src/byoai/telemetry/otel.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | |
byoai.telemetry.otel.OpenTelemetryMiddleware ¶
OpenTelemetryMiddleware(tracer_provider: Any | None = None)
Bases: Middleware
Wraps every execution in a byoai.execute span.
Source code in src/byoai/telemetry/otel.py
50 51 | |
Background workers¶
byoai.workers.RuntimeWorker ¶
RuntimeWorker(runtime: Runtime, queue: JobQueue, *, concurrency: int = 10, shutdown_timeout: float | None = None)
Consume jobs and execute them through the runtime, concurrency at a
time. Failed jobs get an {"error": ...} result and are still acked
(dead-lettering/retry policy belongs to the queue, not the worker).
Source code in src/byoai/workers.py
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | |
run
async
¶
run(*, until_idle: bool = False, poll_timeout: float = 0.5) -> None
Consume jobs; drains in-flight work on exit.
Default mode runs until :meth:stop is called. With until_idle=True
it returns once the queue stays empty and nothing is in flight
(batch/test runs).
Source code in src/byoai/workers.py
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | |
run_until_idle
async
¶
run_until_idle() -> None
Convenience for batch/test runs: consume until the queue stays empty.
Source code in src/byoai/workers.py
347 348 349 | |
byoai.workers.JobQueue ¶
Bases: Protocol
pop
async
¶
pop(timeout: float = 1.0) -> Job | None
Next job, or None if none arrived within timeout seconds.
Source code in src/byoai/workers.py
45 46 47 | |
byoai.workers.Job
dataclass
¶
Job(payload: dict[str, Any], id: str = (lambda: uuid.uuid4().hex)(), delivery_tag: Any = None)
byoai.workers.MemoryJobQueue ¶
MemoryJobQueue(*, maxsize: int = 0)
In-process queue for dev/tests. Same contract as RedisStreamQueue.
maxsize (default 0 = unbounded, matching asyncio.Queue) bounds
memory when publishers can outrun a slow worker fleet; publish()
then backpressures by awaiting free space instead of growing forever.
Source code in src/byoai/workers.py
66 67 68 69 | |
byoai.workers.RedisStreamQueue ¶
RedisStreamQueue(*, url: str = 'redis://localhost:6379', stream: str = 'byoai:jobs', group: str = 'byoai-workers', consumer: str | None = None, result_prefix: str = 'byoai:result:', result_ttl: int = 3600, prefetch: int = 16, client: Any | None = None, mode: str = 'standalone', sentinels: list | None = None, service_name: str | None = None, maxlen: int | None = None, approximate_trim: bool = True, start_id: str = '0', **client_kwargs: Any)
Jobs on a Redis Stream with a consumer group; results as byoai:-
namespaced keys with TTL. At-least-once delivery: entries are XACKed only
after the result is stored.
Requires the redis extra: pip install byoai-runtime[redis].
maxlen caps the jobs stream (unbounded by default) so an idle or
crashed worker fleet doesn't let publishers grow it forever.
start_id is the consumer group's initial read position — "0"
(default) replays the whole existing stream for a fresh group;
"$" starts from only new entries, for attaching a new worker
fleet to a pre-existing, already-large stream without a backlog
replay. **client_kwargs are forwarded to the redis-py client.
Source code in src/byoai/workers.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
FastAPI integration¶
byoai.integrations.fastapi.attach ¶
attach(app: FastAPI, runtime: Runtime) -> Runtime
Bind the runtime to an app: stores it on app.state and registers a
shutdown hook that closes provider/cache/vector connections.
Works alongside an app's existing lifespan/startup handlers — it only adds
an on_shutdown callback rather than replacing the lifespan.
Source code in src/byoai/integrations/fastapi.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
byoai.integrations.fastapi.get_runtime ¶
get_runtime(request: Request) -> Runtime
FastAPI dependency: runtime: Runtime = Depends(get_runtime).
Also works with a WebSocket passed directly (not through Depends)
since both expose .app.state — useful for fetching the runtime inside
a websocket route before calling :func:serve_websocket. Typed as
Request rather than Request | WebSocket because FastAPI's
dependency-injection machinery cannot build a response field for a Union
here; callers using the websocket form should pass it through as-is (it
works at runtime) or cast it for their own type-checking.
Source code in src/byoai/integrations/fastapi.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |
byoai.integrations.fastapi.stream_response ¶
stream_response(runtime: Runtime, input: Any, *, media_type: str = 'text/event-stream', headers: dict[str, str] | None = None, **execute_kwargs: Any) -> StreamingResponse
SSE response streaming runtime.stream() chunks.
Emits data: {"delta": "..."} events per token batch and a final
data: {"done": true, "usage": {...}} event. headers defaults to
disabling proxy buffering (Cache-Control: no-cache,
X-Accel-Buffering: no) — pass {} to omit them entirely.
Source code in src/byoai/integrations/fastapi.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | |
byoai.integrations.fastapi.serve_websocket
async
¶
serve_websocket(runtime: Runtime, websocket: WebSocket) -> None
Serve the shared WebSocket dialect on an accepted-or-new connection.
Each client message is one JSON payload (see byoai.transport); the
response is a stream of JSON frames — {"delta": ...} per token batch,
then {"done": true, "usage": {...}}. Use inside your own route::
@app.websocket("/ws")
async def ws(websocket: WebSocket, rt: Runtime = Depends(get_runtime)):
await serve_websocket(rt, websocket)
Source code in src/byoai/integrations/fastapi.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
Flask integration¶
byoai.integrations.flask.attach ¶
attach(app: Flask, runtime: Runtime) -> Runtime
Bind runtime to app via app.extensions (Flask's own
extension-state slot) and start its background bridge thread.
Idempotent: calling this again on an already-attached app (a duplicate
call, or Werkzeug's debug-mode reloader re-executing module code) returns
the existing runtime instead of starting a second bridge thread — the
runtime passed on that second call is unused and gets closed
immediately (its provider(s) already opened real connections in
__init__, so leaving it open would leak them for the rest of the
process). Registers atexit cleanup — WSGI has no lifespan-shutdown
protocol like ASGI's, so this is the best available hook; it won't fire
on SIGKILL, but does on gunicorn's graceful SIGTERM path.
Source code in src/byoai/integrations/flask.py
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 | |
byoai.integrations.flask.get_runtime ¶
get_runtime(app: Flask | None = None) -> Runtime
The attached Runtime — defaults to flask.current_app; pass
app= explicitly when calling outside a request/app context (e.g. a
background job wrapped in with app.app_context(): ...).
Source code in src/byoai/integrations/flask.py
574 575 576 577 578 | |
byoai.integrations.flask.execute ¶
execute(input: Any, *, app: Flask | None = None, **kwargs: Any) -> ExecutionResult
Sync wrapper for runtime.execute() — blocks the calling (Flask
request) thread until the result is ready.
Source code in src/byoai/integrations/flask.py
581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 | |
byoai.integrations.flask.stream_response ¶
stream_response(input: Any, *, app: Flask | None = None, media_type: str = 'text/event-stream', headers: dict[str, str] | None = None, **execute_kwargs: Any) -> Response
SSE Response streaming runtime.stream() chunks — same frame
shape as the FastAPI/Robyn integrations (via transport.chunk_to_dict).
headers defaults to disabling proxy buffering; pass {} to omit.
Source code in src/byoai/integrations/flask.py
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 | |
Robyn integration¶
byoai.integrations.robyn.attach ¶
attach(app: Robyn, runtime: Runtime, *, prefix: str = '/byoai', stream_media_type: str = 'text/event-stream', stream_headers: dict[str, str] | None = None) -> Runtime
Register ByoAI routes on an existing Robyn app and bind lifecycle.
Adds POST {prefix}/execute, POST {prefix}/stream (SSE) and a
{prefix}/ws WebSocket, plus a shutdown handler closing the runtime's
provider/cache/vector connections. stream_headers defaults to
disabling proxy buffering (Cache-Control: no-cache,
X-Accel-Buffering: no) — pass {} to omit them entirely.
Source code in src/byoai/integrations/robyn.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
byoai.integrations.robyn.create_app ¶
create_app(runtime: Runtime, *, prefix: str = '/byoai', healthz_path: str | None = '/healthz', **attach_kwargs: Any) -> Robyn
A standalone Robyn app serving the runtime.
Registers GET {healthz_path} by default; pass healthz_path=None
to skip it (e.g. if your host app already defines that route).
Source code in src/byoai/integrations/robyn.py
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
MCP integration¶
byoai.integrations.mcp.create_server ¶
create_server(runtime: Runtime, *, name: str = 'byoai-runtime', tool_name: str = 'execute', description: str | None = None, stream_tool_name: str | None = 'execute_stream', stream_description: str | None = None, **server_kwargs: Any) -> Any
Build an MCP server exposing runtime as one or two tools.
Both tools accept the same payload shape as every other transport
(input, optional pipeline, session_id, user_id, model,
filters) and return the same result dict as POST /execute elsewhere.
tool_name(default"execute") — one request, one response.stream_tool_name(default"execute_stream"; passNoneto disable) — streams token deltas as MCP progress notifications (visible to clients that render them live) while still returning the same full result dict at the end, so non-streaming-aware clients work unchanged. Progress notifications require a live client session; if the tool is invoked without one (e.g. a localcall_tool()in a test), reporting is skipped and the full result is still returned correctly.
**server_kwargs are forwarded to the underlying MCP server
constructor (instructions, version, debug, auth, ...) for
anything this wrapper doesn't already surface.
Source code in src/byoai/integrations/mcp.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | |
byoai.integrations.mcp.attach ¶
attach(app: Starlette, runtime: Runtime, *, path: str = '/mcp', name: str = 'byoai-runtime', **create_kwargs: Any) -> Any
Mount an MCP server into an existing Starlette/FastAPI app at path
(streamable HTTP transport).
Two things app.mount() alone doesn't give you, both handled here:
- The MCP sub-app has its own ASGI lifespan (it starts the streamable-HTTP session manager's task group); Starlette never cascades a parent app's lifespan into a mounted sub-app, so without this the session manager never starts and every request 500s. We enter/exit that lifespan manually via the host app's startup/shutdown hooks.
server.streamable_http_app()registers its own internal route atstreamable_http_path— mounting that app again atpathwould double the prefix (path+path). We register the sub-app's internal route at"/"sopathis applied exactly once, byapp.mount().
Requires the host app to support add_event_handler (FastAPI's
router, or Starlette apps not yet on the lifespan-only contract) so we
have somewhere to hook the sub-app's startup/shutdown — raises
:class:ConfigurationError immediately if it doesn't, rather than
mounting an endpoint that would 500 on every request. Plain modern
Starlette apps (lifespan= only) aren't supported by this helper;
wire the MCP server's lifespan into your own instead.
Source code in src/byoai/integrations/mcp.py
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | |
byoai.integrations.mcp.create_app ¶
create_app(runtime: Runtime, *, name: str = 'byoai-runtime', **create_kwargs: Any)
A standalone ASGI app serving the MCP tool over streamable HTTP.
Source code in src/byoai/integrations/mcp.py
270 271 272 273 | |