Single Call & Shared Call Pool
Convenience surfaces for running individual calls through the full resilience
pipeline — error-type-aware retries, the coordinated rate-limit cooldown, and
token accounting — without constructing a
ParallelBatchProcessor.
call/call_result— one prompt, one result. No queue, workers, or result stream are created.LLMCallPool— a long-lived, shared entry point for the request path: many concurrent callers, one coordinated cooldown, concurrency bounded by a semaphore.
For large bulk jobs, keep using ParallelBatchProcessor /
process_prompts; these surfaces are for single calls, not batches.
Example
from llm_minder import LLMCallPool, OpenAIModel, OpenAIStrategy, ProcessorConfig, call, call_result
strategy = OpenAIStrategy(OpenAIModel.from_api_key("gpt-6-luna"))
# One prompt through the full pipeline — no queue, workers, or result stream.
summary = await call(strategy, "Summarize: ...") # output, or raises
# A long-lived, shared entry point for a web service's request path.
async with LLMCallPool(
strategy,
config=ProcessorConfig(max_workers=5),
max_pending=100, # admission cap: reject instantly when saturated
submit_timeout=30, # per-caller latency budget (seconds)
) as pool:
reply = await pool.submit("Answer this one request")
Failure semantics
call()/LLMCallPool.submit()re-raise the provider's own exception on failure, preserving its type.LLMCallErroris raised only when no provider exception was preserved — the pool'smax_pending/submit_timeoutrejections.call_result()/LLMCallPool.submit_result()never raise for a request failure; they return the fullWorkItemResult— inspectsuccess,error,token_usage,metadata, and the originatingexception.
result = await call_result(strategy, "Summarize: ...")
if not result.success:
print(result.error, result.exception) # exception preserves the provider type
The pool drains already-admitted requests on aclose() (the async with
exit) before cleaning up the shared strategy, so in-flight calls aren't cut off
by shutdown. Set submit_timeout to bound how long shutdown waits for that
drain — otherwise it waits as long as the admitted work takes.
With GuardrailConfig(abort_on_error_categories=...), the first request that
finishes failed in one of those categories stops the pool. Later submits return
batch_aborted results (submit() raises BatchAbortedError) without calling the
provider, and pool.termination (provisional) names the triggering item, category
and classifier hint. The pool stays stopped until pool.resume() (provisional),
which returns the cleared termination. See
Configurable fail-fast.
Timeout and capacity semantics
The pool semaphore is acquired before item execution, so semaphore wait does
not consume attempt_timeout. submit_timeout is the end-to-end caller budget:
it includes semaphore wait, provider-capacity admission, coordinated cooldown,
retry backoff, and every
attempt. If a model built with from_api_key(max_connections=N) advertises less
capacity than config.max_workers, pool construction emits a UserWarning
and the shared executor gates attempts at N. Use
ProcessorConfig(max_provider_concurrency=N) for a custom client whose capacity
cannot be inspected.
Avoid wrapping an unbounded input in asyncio.gather(gw.submit(...)): the
pool bounds active provider calls, but every gathered coroutine is still a
pending task. Bound the outer producer or use process_stream with a bounded
max_queue_size for large batch workloads. The
bounded-work guide includes both the
recommended batch path and a bounded shared-call task window.
pool.cache_stats() reports prompt-cache reads and writes per cache group, with a
rolling hit rate, and pool.reset_cache_stats() clears them. The pool reports these
statistics but has no hit-rate floor. pool.cache_tracker is the
CacheGroupTracker behind them, for saving, restoring (cache_tracker=) or checking a
floor yourself. See Cache hit-rate floor.
LLMCallPool is an in-process, queue-less object. Each caller directly uses the
shared ItemExecutor under a semaphore; there is no background dispatcher,
network service, or provider-routing layer. The strategy beneath it may use a
direct SDK, CallableStrategy, or a real third-party gateway client.
LLMGateway, the pre-v0.20 name, was removed in 1.0; use LLMCallPool, the same class.
call
llm_minder.call
async
call(strategy: LLMCallStrategy[TOutput], prompt: str, *, config: ProcessorConfig | None = None, error_classifier: Any = None) -> TOutput
Run one prompt and return its output, raising on failure.
Set a timeout or retry policy via config (e.g.
ProcessorConfig(attempt_timeout=20)).
Source code in src/llm_minder/single.py
call_result
llm_minder.call_result
async
call_result(strategy: LLMCallStrategy[TOutput], prompt: str, *, config: ProcessorConfig | None = None, error_classifier: Any = None) -> WorkItemResult[TOutput, Any]
Run one prompt through the full pipeline and return the WorkItemResult.
Inspect .success / .output / .error / .token_usage. Use this
when you want token accounting or to branch on failure without exceptions;
for the happy-path one-liner use :func:call.
Source code in src/llm_minder/single.py
LLMCallPool
llm_minder.LLMCallPool
LLMCallPool(strategy: LLMCallStrategy[TOutput], *, config: ProcessorConfig | None = None, error_classifier: Any = None, max_pending: int | None = None, submit_timeout: float | None = None, cache_tracker: CacheGroupTracker | None = None)
Bases: Generic[TOutput]
An in-process shared call pool for single LLM calls from many callers.
Create one at startup and call :meth:submit from any number of concurrent
handlers. config.max_workers is the global concurrency budget. The
strategy is shared, so use a stateless strategy or one whose prepare()
yields a reusable resource.
A long rate_limit.cooldown_seconds stalls all callers during a 429 —
tune it via config to trade upstream protection against latency.
Two opt-in knobs bound the request path under load (both off by default, so
the default is pure backpressure — callers beyond max_workers wait):
max_pending— an admission cap. With it set, at mostmax_workers + max_pendingrequests may be in flight (running or waiting on the semaphore); further submits are rejected instantly with a failed result instead of growing an unbounded waiter list. Bounds memory.submit_timeout— a per-caller latency bound (seconds). An admitted request that hasn't completed within the budget (e.g. stuck behind a cooldown) is cancelled and returns a failed result. A firing timeout cancels an in-flight request that might have been about to succeed — the right trade for a web latency budget where the client is already gone.
cache_tracker (provisional) is the :class:~llm_minder.CacheGroupTracker
behind :meth:cache_stats. By default the pool creates one with
config.guardrails.cache_hit_window; pass one restored with
CacheGroupTracker.from_dict to continue a previous pool's statistics. Its
own window then applies.
config.guardrails.abort_on_error_categories stops the pool on the first
request that finishes failed in one of those categories, as it stops a
processor run. From then on every submit, including those waiting for a slot,
returns a batch_aborted result without calling the provider; calls already
running drain or are cancelled per config.guardrails.abort_mode. The pool
stays stopped, with :attr:termination saying why, until :meth:resume is
called (for example after re-authenticating); a caller that never resumes it
has aborted it.
Source code in src/llm_minder/call_pool.py
cache_tracker
property
The pool's live :class:~llm_minder.CacheGroupTracker (provisional).
Save it with pool.cache_tracker.to_dict() and pass
CacheGroupTracker.from_dict(saved) as cache_tracker= to a later
pool to carry the statistics across a restart, or call
pool.cache_tracker.below_floor(...) to apply a floor yourself.
termination
property
Why the pool stopped, or None while it is accepting work (provisional).
Set when a request fails in one of
config.guardrails.abort_on_error_categories; kind is
"fail_fast" and reason names the item, the category and any
classifier hint.
aclose
async
Stop accepting work, drain in-flight requests, then run cleanup().
Marks the pool closed (new submits raise immediately), then waits for
already-admitted requests — running or still waiting on the semaphore —
to drain before cleaning up the shared strategy, whose clients/caches may
still be in use. The drain waits indefinitely; set submit_timeout to
bound how long an admitted request can hold up shutdown.
Concurrent callers share one ordered close attempt, so every
await gw.aclose() returns only once cleanup has actually completed.
A successful step is never repeated; a failed one is retried by the
next call. Cancelling one waiter defers that cancellation until the
shared attempt finishes; cancelling it a second time force-aborts.
Source code in src/llm_minder/call_pool.py
cache_stats
Prompt-cache reads and writes per cache group since creation or the last reset.
Returns {"cache_groups": [...], "cache_groups_overflow": ...} in the
shape ParallelBatchProcessor.get_stats() uses. Each group also reports
window_hit_rate over its last cache_tracker.window measured calls
(None until that many), since a long-lived pool's
cumulative hit_rate changes slowly. Statistics only: the pool has no
cache hit-rate floor. Call it from the pool's event loop; it doesn't await.
Source code in src/llm_minder/call_pool.py
reset_cache_stats
Clear the per-group cache statistics, including the 64-group cap's overflow.
A call still running at the reset is counted when it returns.
resume
Accept work again after a fail-fast stop (provisional).
Returns the :attr:termination it clears, or None when the pool
wasn't stopped. Later submits call the provider again, and a later failure
in a configured category stops the pool again. Calls that started before
the resume finish as they would have, but their failures don't stop the
resumed pool. Call it from the pool's event loop; it doesn't await.
Source code in src/llm_minder/call_pool.py
submit
async
Submit one prompt and await its output, raising on failure.
Blocks on the semaphore when the pool is saturated (backpressure), unless
max_pending is set (then an over-cap submit raises immediately). A
rejected/timed-out submit raises :class:LLMCallError. timeout
overrides the pool's submit_timeout for this call.
Source code in src/llm_minder/call_pool.py
submit_result
async
Like :meth:submit but returns the WorkItemResult instead of raising.
Gives access to token_usage / metadata and lets callers branch
on .success without exception handling. A request rejected by the
admission cap or cut off by the timeout comes back as a failed result
(success=False) rather than raising.
timeout overrides the pool's submit_timeout for this call.
Source code in src/llm_minder/call_pool.py
LLMCallError
llm_minder.LLMCallError
Bases: RuntimeError, LLMMinderError
Raised by :func:call / LLMCallPool.submit when a request fails and no
originating provider exception was preserved.
When the failure carried a provider exception — the usual case for an
exhausted-retry or permanent error — that exact exception is re-raised
instead, preserving its type. LLMCallError is the fallback for failures
that produce no exception: the pool's admission-cap (max_pending) and
submit_timeout rejections.
Carries the originating :class:WorkItemResult on .result so callers
can still reach token usage, metadata, and the error string.