Strategies API Reference
CallableStrategy
Adapter for an existing asynchronous SDK, gateway client, agent, or application service. See Use Your Existing Async Client for callback and retry-state semantics.
llm_minder.CallableStrategy
CallableStrategy(invoke: InvokeCallback[TOutput], *, identity: ArtifactIdentity | None = None, error_classifier: ErrorClassifier | None = None, prepare: LifecycleCallback | None = None, cleanup: LifecycleCallback | None = None, on_error: ErrorCallback | None = None, dry_run: DryRunCallback[TOutput] | None = None, max_concurrency: int | None = None, concurrency_scope: object | None = None, quota_scope: object | None = None, token_estimator: TokenEstimator | None = None, request_concurrency: RequestConcurrencyCallback | None = None)
Bases: LLMCallStrategy[TOutput]
Adapt an existing async client or application operation to ABL.
The invocation callback has this canonical shape::
async def invoke(
prompt: str,
*,
attempt: int,
timeout: float,
state: RetryState | None,
) -> CallOutcome[TOutput]: ...
attempt is ABL's logical attempt number, timeout is the effective
executor-owned attempt budget, and state belongs exclusively to the
current work item. This class adapts callbacks to LLMCallStrategy; the
existing ItemExecutor remains responsible for retries, timeouts,
cooldowns, accounting, and cancellation.
Source code in src/llm_minder/callable_strategy.py
artifact_identity
property
Stable artifact identity, or None when the caller omitted one.
CallOutcome
llm_minder.CallOutcome
dataclass
CallOutcome(output: TOutput, token_usage: Mapping[str, int] = dict(), metadata: Mapping[str, Any] | None = None)
Bases: Generic[TOutput]
The output, reported token usage, and metadata from an async operation.
Token usage and metadata are copied and validated when the outcome crosses the strategy boundary. An empty usage mapping means the upstream operation did not report usage; ABL does not estimate missing values.
LLMCallStrategy
llm_minder.LLMCallStrategy
Bases: ABC, Generic[TOutput]
Abstract base class for LLM call strategies.
A strategy encapsulates how LLM calls are made, including: - Resource initialization (caches, clients) - Call execution with retries - Resource cleanup
The framework calls: 1. prepare() once per unique strategy instance before its first execution 2. execute() for each attempt (including retries) 3. cleanup() when the processor exits or shuts down; a later explicit close retries an unsuccessful attempt
concurrency_scope
property
Identity whose capacity is shared by concurrent calls.
max_concurrency
property
Maximum safe concurrent calls advertised by this strategy.
None means the capacity is unknown. Model-backed strategies forward
capacity metadata from their model; custom strategies can override this
property when they own a bounded client or transport.
quota_scope
property
Identity whose RPM, TPM, and coordinated cooldown are shared.
The default follows :attr:concurrency_scope for backward-compatible
ownership. Override it when one provider/account quota spans multiple
clients or when one shared client serves independent quota budgets.
Object identity, not equality, defines sharing. Explicit None falls back
to strategy identity. An ordinary property failure raises QuotaScopeError
before admission; cancellation and process-control exceptions propagate.
cleanup
async
Clean up resources when the processor exits or shuts down.
Called per prepared strategy instance, not per work item. A successful call is never repeated. If cleanup raises or is interrupted, a later explicit close retries it. Implementations must therefore be idempotent and safe to call after a partially completed attempt.
Use this for: - Closing connections/sessions - Releasing locks - Logging final metrics - Deleting temporary files
Do NOT use this for: - Deleting caches intended for reuse across runs - Destructive cleanup that prevents resource reuse
Note on Caches (v0.2.0):
For reusable resources like Gemini caches with TTLs, consider letting
them expire naturally to enable cost savings across multiple pipeline
runs. See GeminiCachedModel for an example.
Default: no-op
Source code in src/llm_minder/llm_strategies.py
dry_run
async
Return mock output for dry-run mode (testing without API calls).
Override this method to provide realistic mock data for testing. Default implementation returns placeholder values that may not match your output type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
The prompt that would have been sent to the LLM |
required |
Returns:
| Type | Description |
|---|---|
tuple[TOutput, TokenUsage]
|
Tuple of (mock_output, mock_token_usage) |
Default behavior: - Returns string "[DRY-RUN] Mock output" as output - Returns mock token usage: 100 input, 50 output, 150 total
Source code in src/llm_minder/llm_strategies.py
estimate_tokens
estimate_tokens(prompt: str, attempt: int, state: RetryState | None) -> TokenEstimate | Awaitable[TokenEstimate] | None
Return a local token estimate for TPM admission, if supported.
The default supplies no estimate. Configure ProcessorConfig with a
run-level estimator or override this hook. No provider call should be
made solely to estimate tokens.
Source code in src/llm_minder/llm_strategies.py
execute
abstractmethod
async
execute(prompt: str, attempt: int, timeout: float, state: RetryState | None = None) -> tuple[TOutput, TokenUsage, dict[str, Any] | None]
Execute an LLM call for the given attempt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
The prompt to send to the LLM |
required |
attempt
|
int
|
Which logical retry attempt this is (1, 2, 3, ...).
Guarantee: rate-limit errors do NOT advance this number. If a
call is throttled (429 / coordinated cooldown), the framework
retries the same |
required |
timeout
|
float
|
Maximum time to wait for response (seconds) |
required |
state
|
RetryState | None
|
Optional retry state that persists across attempts (v0.3.0) |
None
|
Returns:
| Type | Description |
|---|---|
TOutput
|
A 3-tuple |
TokenUsage
|
TokenUsage dict with optional keys |
dict[str, Any] | None
|
|
tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
pass |
tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
(category |
Raises:
| Type | Description |
|---|---|
Exception
|
Any exception propagated to trigger a retry (if retryable) or a permanent failure. |
Note (v0.3.0): The state parameter allows strategies to maintain state across retry attempts for multi-stage retry patterns. See RetryState documentation for examples.
Source code in src/llm_minder/llm_strategies.py
on_error
async
Handle errors that occur during execute().
Called by the framework when execute() raises an exception, before deciding whether to retry. This allows strategies to: - Inspect the error type to adjust retry behavior - Store error information for use in next attempt - Modify prompts based on validation errors - Track error patterns across attempts
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exception
|
Exception
|
The exception that was raised during execute() |
required |
attempt
|
int
|
Which attempt number failed (1, 2, 3, ...) |
required |
state
|
RetryState | None
|
Optional retry state that persists across attempts (v0.3.0) |
None
|
Default: no-op
Example
async def on_error( self, exception: Exception, attempt: int, state: RetryState | None = None ) -> None: if state is not None: # Track validation errors separately from other errors if isinstance(exception, ValidationError): count = state.get('validation_failures', 0) + 1 state.set('validation_failures', count) # Save partial results for recovery if hasattr(exception, 'partial_data'): state.set('partial_data', exception.partial_data)
Source code in src/llm_minder/llm_strategies.py
prepare
async
Initialize resources before making any LLM calls.
Called once per unique strategy instance before the first work item using that instance executes. Use this to set up shared caches, clients, etc. Per-item retry state belongs in execute()/on_error() via RetryState.
Default: no-op
Source code in src/llm_minder/llm_strategies.py
recommended_error_classifier
Return the error classifier best suited to this strategy's provider.
The execution host calls this once per strategy identity when the caller
didn't pass error_classifier explicitly.
Returns None by default ("no preference"), which lets the framework
fall back to :class:DefaultErrorClassifier. Provider strategies
(GeminiStrategy, OpenAIStrategy, …) override this to return their
matching classifier. An explicit error_classifier= on the processor
always wins over this recommendation.
Source code in src/llm_minder/llm_strategies.py
The hooks below are the ones a custom strategy overrides.
estimate_tokens(prompt, attempt, state)
Optional per-strategy TPM estimator. It runs after middleware and coordinated
cooldown but before the atomic quota reservation and provider-capacity wait.
It may be synchronous or asynchronous and receives retry state so model
escalation can change the expected output. ProcessorConfig.token_estimator
takes precedence. The default returns None.
quota_scope identifies strategies sharing RPM, TPM, and cooldown by object
identity. It defaults to concurrency_scope, which identifies shared provider
capacity. Override them independently when account quota and client capacity
have different ownership. Explicit None uses strategy identity; unhashable
objects are supported. A raising property fails closed with non-retryable
QuotaScopeError, without copying the property exception message. Initial
configuration raises this error directly (including call_result and pool
construction); a middleware-selected strategy failure becomes a per-item
quota_scope_error result. No provider request or quota debit follows it.
async def prepare() -> None
Initialize resources before making LLM calls (e.g., create caches, initialize clients).
Default: No-op
async def execute(prompt, attempt, timeout, state=None)
async def execute(
prompt: str, attempt: int, timeout: float, state: RetryState | None = None
) -> tuple[TOutput, TokenUsage, dict[str, Any] | None]: ...
Execute an LLM call.
Parameters:
prompt(str): The prompt to send to the LLMattempt(int): Which retry attempt this is (1, 2, 3, ...)timeout(float): Maximum time to wait for response (seconds)- Note: Timeout enforcement is handled by the framework wrapping this call in
asyncio.wait_for() state(RetryState | None): Mutable per-work-item state provided by the framework so strategies can track partial progress across retries
Returns: Tuple of (output, token_usage, metadata)
output(TOutput): The LLM responsetoken_usage(TokenUsage): Token usage dict with optional keys:input_tokens,output_tokens,total_tokens,cached_input_tokens,cache_write_input_tokensmetadata(dict[str, Any] | None): Provider metadata forwarded toWorkItemResult.metadata;Nonewhen there is none
Since 1.0, returning the 2-tuple (output, token_usage) fails the item with a
ValueError (category logic_error, not retried) that names your strategy. Its token
usage is still recorded.
Raises: Any exception to trigger retry (if retryable) or failure
async def dry_run(prompt: str) -> tuple[TOutput, TokenUsage]
Return mock output for dry-run mode (testing without API calls).
Called when ProcessorConfig(dry_run=True) is set. Override this method to provide realistic mock data for testing.
Parameters:
prompt(str): The prompt that would have been sent to the LLM
Returns: Tuple of (mock_output, mock_token_usage)
Default behavior:
- Returns string
"[DRY-RUN] Mock output for prompt: {prompt[:50]}..."as output - Returns mock token usage: 100 input, 50 output, 150 total tokens
Example override:
class MyStrategy(LLMCallStrategy[Output]):
async def dry_run(self, prompt: str) -> tuple[Output, TokenUsage]:
# Return realistic mock data
mock_output = Output(result="Test result")
mock_tokens: TokenUsage = {
"input_tokens": len(prompt.split()),
"output_tokens": 50,
"total_tokens": len(prompt.split()) + 50,
}
return mock_output, mock_tokens
async def on_error(exception: Exception, attempt: int, state: RetryState | None = None) -> None
Handle errors that occur during execute().
Called by the framework when execute() raises an exception, before deciding whether to retry. This allows strategies to:
- Inspect the error type to adjust retry behavior
- Store error information for use in the next attempt
- Modify prompts based on validation errors
- Track error patterns across attempts
- Make intelligent decisions (e.g., escalate to smarter model only on validation errors)
Parameters:
exception(Exception): The exception that was raised duringexecute()attempt(int): Which attempt number failed (1, 2, 3, ...)state(RetryState | None): Retry state that persists across attempts (v0.3.0)
Default: No-op
Use Cases:
- Smart Model Escalation - Only escalate to expensive models on validation errors, not network errors:
class SmartModelEscalationStrategy(LLMCallStrategy[Output]):
async def on_error(self, exception: Exception, attempt: int, state=None) -> None:
if state is not None and isinstance(exception, ValidationError):
state.set("validation_failures", state.get("validation_failures", 0) + 1)
async def execute(self, prompt: str, attempt: int, timeout: float, state=None):
# Only escalate model on validation errors
failures = state.get("validation_failures", 0) if state is not None else 0
model_index = min(failures, len(MODELS) - 1)
model = MODELS[model_index]
# Make call with appropriate model...
- Smart Retry with Partial Parsing - Build better retry prompts based on what failed:
class SmartRetryStrategy(LLMCallStrategy[Output]):
async def on_error(self, exception: Exception, attempt: int, state=None) -> None:
if state is not None and isinstance(exception, ValidationError):
state.set("last_validation_error", exception)
# last_response set in execute() before raising
async def execute(self, prompt: str, attempt: int, timeout: float, state=None):
if attempt > 1 and state and state.get("last_validation_error"):
# Build smart retry prompt with partial parsing feedback
prompt = self._create_retry_prompt_with_partial_data(prompt, state)
# Make call with improved prompt...
- Error Type Tracking - Distinguish between different error types:
class ErrorTrackingStrategy(LLMCallStrategy[Output]):
async def on_error(self, exception: Exception, attempt: int, state=None) -> None:
if state is None:
return
if isinstance(exception, ValidationError):
key = "validation_errors"
elif isinstance(exception, ConnectionError):
key = "network_errors"
elif "429" in str(exception):
key = "rate_limit_errors"
else:
key = "other_errors"
state.set(key, state.get(key, 0) + 1)
Important Notes:
- Exceptions in
on_error()are caught and logged by the framework - they won't crash processing on_error()is only called whenexecute()raises an exception, not on success- The error is still propagated to the framework's retry logic after
on_error()returns - Share strategy/client instances across work items, and keep all item-specific mutation in
the supplied
RetryState. Use concurrency-safe metrics or observers for batch-wide counters.
See Also:
- examples/example_smart_model_escalation.py - Complete smart model escalation example
- examples/example_gemini_smart_retry.py - Smart retry with partial parsing
async def cleanup() -> None
Clean up resources after all attempts complete (e.g., delete caches, close clients).
Default: No-op
Custom Strategy Example:
from llm_minder import LLMCallStrategy, TokenUsage
class MyCustomStrategy(LLMCallStrategy[str]):
async def execute(
self, prompt: str, attempt: int, timeout: float, state=None
) -> tuple[str, TokenUsage, None]:
# Your custom LLM API call
response = await my_llm_api.generate(prompt)
tokens: TokenUsage = {
"input_tokens": response.input_tokens,
"output_tokens": response.output_tokens,
"total_tokens": response.total_tokens,
}
return response.text, tokens, None
ModelStrategy
Shared base for the provider-named strategies below; delegates to an
LLMModel. Use directly for a custom model you don't want a dedicated
subclass for.
llm_minder.ModelStrategy
ModelStrategy(model: LLMModel, response_parser: Callable[[LLMResponse], TOutput] | None = None, *, temperature: float | None = None, generation_config: dict[str, Any] | None = None, quota_scope: object | None = None, token_estimator: TokenEstimator | None = None)
Bases: LLMCallStrategy[TOutput]
Base strategy for any provider exposed as an :class:LLMModel.
Holds the machinery shared by all model-backed strategies: the model
reference, an optional response parser, lifecycle delegation to
:class:ManagedLLMModel, and an execute() that calls
model.generate(), parses the response, and forwards
LLMResponse.metadata as the third tuple element.
The provider-named subclasses (:class:GeminiStrategy,
:class:OpenAIStrategy, :class:OpenRouterStrategy) are thin shells over
this base — they exist so users can pick the strategy named after the
provider they're using. Use this base directly for a custom
:class:LLMModel you don't want to name a dedicated subclass for.
Added in v0.10.0 (extracted from the formerly-duplicated provider strategy classes).
Initialize strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
LLMModel
|
An LLMModel instance (e.g., GeminiModel, OpenAIModel). |
required |
response_parser
|
Callable[[LLMResponse], TOutput] | None
|
Function to parse LLMResponse into TOutput. Defaults to
returning |
None
|
temperature
|
float | None
|
Default sampling temperature. Defaults to |
None
|
generation_config
|
dict[str, Any] | None
|
Provider-specific config forwarded to
|
None
|
quota_scope
|
object | None
|
Optional identity shared by strategies consuming the same provider/account quota. Defaults to the model, matching the existing concurrency scope. |
None
|
token_estimator
|
TokenEstimator | None
|
Optional strategy-owned local estimator used when TPM admission is enabled and the processor has no override. |
None
|
Source code in src/llm_minder/llm_strategies.py
concurrency_scope
property
Strategies wrapping the same model share one admission limit.
max_concurrency
property
Forward optional client/transport capacity advertised by the model.
attempt_generation_config
Return config to merge over generation_config for this attempt.
Override it to change provider config per attempt without overriding
execute(), for example to raise an output-token bound after a
truncated response. The returned keys replace the same top-level keys of
generation_config for this call only (a shallow merge); None, the
default, sends generation_config unchanged. Don't mutate
self.generation_config: the strategy may be shared by concurrent
items.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attempt
|
int
|
The logical attempt number, as passed to |
required |
state
|
RetryState | None
|
The item's retry state, as passed to |
required |
Source code in src/llm_minder/llm_strategies.py
cleanup
async
Delegate to model.cleanup() if the model has a managed lifecycle.
Source code in src/llm_minder/llm_strategies.py
execute
async
execute(prompt: str, attempt: int, timeout: float, state: RetryState | None = None) -> tuple[TOutput, TokenUsage, dict[str, Any] | None]
Execute the LLM call via the model and parse the response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
The prompt to send to the LLM. |
required |
attempt
|
int
|
Which retry attempt this is (1, 2, 3, ...). |
required |
timeout
|
float
|
Maximum time for response (enforced by the framework). |
required |
state
|
RetryState | None
|
Optional retry state for cross-attempt persistence. |
None
|
Returns:
| Type | Description |
|---|---|
TOutput
|
3-tuple |
TokenUsage
|
is forwarded from |
dict[str, Any] | None
|
model, safety_ratings, etc.). Added the metadata slot in v0.10.0. |
Source code in src/llm_minder/llm_strategies.py
prepare
async
Delegate to model.prepare() if the model has a managed lifecycle.
Source code in src/llm_minder/llm_strategies.py
request_concurrency
async
Forward a concurrency request to the model, when supported.
Execution surfaces call this when ProcessorConfig.concurrency is
set (v0.20.0) so built-in models can right-size their connection
pools. Models without the hook (custom LLMModel implementations,
Gemini) simply return False and are left untouched.
Source code in src/llm_minder/llm_strategies.py
Per-attempt config
To change provider config from one attempt to the next, override
attempt_generation_config() rather than execute(). Its keys replace the same
top-level keys of generation_config for that call. Timing, token accounting on
parse failures and later fixes to execute() still apply.
from typing import Any
from llm_minder import DeepSeekStrategy, RetryState
class GrowingOutputBound(DeepSeekStrategy):
"""Double the output-token bound on each retry: 1024, 2048, 4096, ..."""
def attempt_generation_config(
self, attempt: int, state: RetryState | None
) -> dict[str, Any] | None:
return {"max_tokens": 1024 * 2 ** (attempt - 1)}
A truncated response raises TruncatedResponseError, which the built-in
classifiers treat as non-retryable response_truncated, so the item would fail on
its first attempt. To retry a truncation with the larger bound, and nothing else, mark
it retryable in a classifier (new in 1.0 and provisional):
from llm_minder import (
DefaultErrorClassifier,
ErrorInfo,
ParallelBatchProcessor,
TruncatedResponseError,
)
class RetryTruncations(DefaultErrorClassifier):
def classify(self, exception: Exception) -> ErrorInfo:
if isinstance(exception, TruncatedResponseError):
return ErrorInfo(
is_retryable=True,
is_rate_limit=False,
is_timeout=False,
error_category="response_truncated",
)
return super().classify(exception)
processor = ParallelBatchProcessor(error_classifier=RetryTruncations())
Subclass the classifier your provider uses (for DeepSeek, OpenAIErrorClassifier).
Safety blocks and refusals keep their own reason and stay final.
Helpers for custom execute() overrides
A strategy that makes its own provider call can report what the built-in
strategies report. provider_call(state) times the call and records it as the
attempt's provider time, even when the call raises. attach_token_usage(exception,
tokens) keeps a billed response's tokens in the item's totals when handling the
response fails afterwards, and keeps the exception's type, so classification and
on_error() see the original error. Both are new in 0.32 and provisional.
import json
from typing import Any
from llm_minder import (
LLMCallStrategy,
RetryState,
TokenUsage,
attach_token_usage,
provider_call,
)
class MyStrategy(LLMCallStrategy[dict]):
def __init__(self, client: Any) -> None:
self.client = client
async def execute(
self, prompt: str, attempt: int, timeout: float, state: RetryState | None = None
) -> tuple[dict, TokenUsage, dict[str, Any] | None]:
async with provider_call(state):
response = await self.client.generate(prompt)
tokens: TokenUsage = {"total_tokens": response.total_tokens}
try:
output = json.loads(response.text)
except ValueError as e:
attach_token_usage(e, tokens)
raise
return output, tokens, None
Setting the _failed_token_usage attribute on an exception yourself, which is what
attach_token_usage() does, is also supported.
llm_minder.provider_call
Time a provider call and record it as this attempt's provider time.
Use it around the call itself, inside execute(), with the state
passed to execute()::
async def execute(self, prompt, attempt, timeout, state=None):
async with provider_call(state):
response = await self.model.generate(prompt, config=config)
...
The time is recorded even when the call raises. It fills
AttemptTiming.provider_seconds and the provider side of the timing
statistics. Plain with works too. state=None records nothing.
Leave response parsing outside the block, so it isn't counted as provider
time. If the block runs more than once in an attempt, the last one wins.
Source code in src/llm_minder/provider_calls.py
llm_minder.attach_token_usage
Attach billed token usage to an exception before re-raising it.
Use it when a response the provider already billed fails afterwards, for example in parsing, so the failed attempt's tokens still count::
try:
output = parse(response)
except ValueError as e:
attach_token_usage(e, response.token_usage)
raise
The exception keeps its type, so classification and on_error() see the
original error. The usage is copied onto the exception's
_failed_token_usage attribute, which the framework reads first; setting
that attribute yourself is also supported.
Source code in src/llm_minder/provider_calls.py
PydanticAIStrategy
llm_minder.PydanticAIStrategy
Bases: LLMCallStrategy[TOutput]
Strategy for using PydanticAI agents.
This strategy wraps a PydanticAI agent, providing a clean interface for batch processing. The agent handles all model interaction, validation, and parsing.
Best for: Structured output with Pydantic models, using PydanticAI's features.
Initialize PydanticAI strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent
|
Agent[None, TOutput]
|
Configured PydanticAI agent |
required |
Source code in src/llm_minder/llm_strategies.py
dry_run
async
Return mock output based on agent's result_type for dry-run mode.
Source code in src/llm_minder/llm_strategies.py
execute
async
execute(prompt: str, attempt: int, timeout: float, state: RetryState | None = None) -> tuple[TOutput, TokenUsage, dict[str, Any] | None]
Execute PydanticAI agent call.
Note: timeout parameter is provided for information but timeout enforcement is handled by the framework wrapping this call in asyncio.wait_for().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str
|
The prompt to send to the LLM |
required |
attempt
|
int
|
Which retry attempt this is (1, 2, 3, ...) |
required |
timeout
|
float
|
Maximum time to wait for response (seconds) |
required |
state
|
RetryState | None
|
Optional retry state (v0.3.0, unused by this strategy) |
None
|
Returns:
| Type | Description |
|---|---|
TOutput
|
3-tuple |
TokenUsage
|
object doesn't expose provider-side metadata uniformly, so |
dict[str, Any] | None
|
|
Source code in src/llm_minder/llm_strategies.py
Structured JSON Parsing
llm_minder.pydantic_json_parser
pydantic_json_parser(model_cls: type[TModel], *, recover_trailing_markdown: bool = False) -> Callable[[LLMResponse], TModel]
Build a response_parser that fence-strips then validates with Pydantic.
Returns a function suitable for the response_parser argument of any
:class:~llm_minder.ModelStrategy subclass (OpenAIStrategy,
DeepSeekStrategy, GeminiStrategy, etc.). It runs
:func:strip_code_fences over LLMResponse.text before calling
model_cls.model_validate_json, so markdown-fenced JSON validates
cleanly instead of raising. Set recover_trailing_markdown=True to opt
into a conservative fallback for one complete top-level JSON object/array
followed only by a recognized closing-fence artifact. It never repairs
malformed JSON or discards arbitrary prose/multiple values.
Example
from pydantic import BaseModel from llm_minder import DeepSeekModel, DeepSeekStrategy from llm_minder.parsing import pydantic_json_parser
class Classification(BaseModel): ... label: str ... confidence: float
model = DeepSeekModel.from_api_key("deepseek-v4-flash", json_mode=True) parser = pydantic_json_parser(Classification, recover_trailing_markdown=True) strategy = DeepSeekStrategy(model, parser)
Validation failures raise pydantic.ValidationError, which the built-in
error classifiers treat as retryable (the model may produce valid output on
a retry).
Source code in src/llm_minder/parsing.py
llm_minder.strip_code_fences
Strip a leading/trailing markdown code fence from text, if present.
Handles the common shapes models emit around JSON:
```json\n{...}\n``````\n{...}\n```- bare text with no fences (returned stripped, unchanged otherwise)
Only an outer fence is removed; fences in the interior of the payload are left alone. Returns the stripped inner content.
Source code in src/llm_minder/parsing.py
pydantic_json_parser(Model) strips a normal outer Markdown fence and performs
strict Pydantic JSON/schema validation. Recovery is disabled by default.
parser = pydantic_json_parser(
Classification,
recover_trailing_markdown=True,
)
strategy = OpenAIStrategy(model, parser)
With recovery enabled, strict parsing still runs first. On failure, the parser uses a real JSON decoder to read exactly one complete top-level object or array, accepts only the explicitly supported trailing closing-fence artifacts (three backticks, with or without the observed trailing underscore), and then runs normal Pydantic schema validation. It does not repair malformed JSON or accept scalars, multiple JSON values, arbitrary prose, or schema-invalid data. Those failures continue through the configured retry policy.
A recovered WorkItemResult exposes typed recovery properties backed by
metadata. Processor stats and MetricsObserver include
structured_output_recoveries, structured_output_retries_avoided, and counts
by structured_output_recovery_reasons.
GeminiStrategy
llm_minder.GeminiStrategy
GeminiStrategy(model: LLMModel, response_parser: Callable[[LLMResponse], TOutput] | None = None, *, temperature: float | None = None, generation_config: dict[str, Any] | None = None, quota_scope: object | None = None, token_estimator: TokenEstimator | None = None)
Bases: ModelStrategy[TOutput]
Strategy for calling a Gemini model and parsing the response.
Accepts an LLMModel (e.g., GeminiModel or GeminiCachedModel) and a response parser. The model handles the API call and token extraction; the strategy handles response parsing and lifecycle delegation.
For caching, use GeminiStrategy(model=GeminiCachedModel(...)).
v0.6.0: Accepts LLMModel instead of raw client + model string.
Example
model = GeminiModel("gemini-3.5-flash", client) strategy = GeminiStrategy(model, response_parser=lambda r: r.text)
With caching:
cached_model = GeminiCachedModel("gemini-3.5-flash", client, cached_content=[...]) strategy = GeminiStrategy(cached_model, response_parser=lambda r: r.text)
Source code in src/llm_minder/llm_strategies.py
OpenAIStrategy
llm_minder.OpenAIStrategy
OpenAIStrategy(model: LLMModel, response_parser: Callable[[LLMResponse], TOutput] | None = None, *, temperature: float | None = None, generation_config: dict[str, Any] | None = None, quota_scope: object | None = None, token_estimator: TokenEstimator | None = None)
Bases: ModelStrategy[TOutput]
Strategy for calling an OpenAI-compatible model and parsing the response.
Accepts an LLMModel (typically OpenAIModel) and an optional response parser. The model handles the API call and token extraction; the strategy handles response parsing and lifecycle delegation.
Added in v0.9.0.
Example
model = OpenAIModel.from_api_key("gpt-6-luna", api_key="sk-...") strategy = OpenAIStrategy(model)
Structured output via response_parser:
strategy = OpenAIStrategy( ... model, ... response_parser=lambda r: MyModel.model_validate_json(r.text), ... )
Source code in src/llm_minder/llm_strategies.py
OpenRouterStrategy
llm_minder.OpenRouterStrategy
OpenRouterStrategy(model: LLMModel, response_parser: Callable[[LLMResponse], TOutput] | None = None, *, temperature: float | None = None, generation_config: dict[str, Any] | None = None, quota_scope: object | None = None, token_estimator: TokenEstimator | None = None)
Bases: ModelStrategy[TOutput]
Strategy for calling an OpenRouter-backed model and parsing the response.
Functionally identical to :class:OpenAIStrategy (both delegate to an
LLMModel via :class:ModelStrategy); the separate class exists for
provider-named symmetry so users can pick the strategy named after the
provider they're using. For OpenRouter, LLMResponse.metadata typically
includes provider (the upstream that served the request), model
(the actually-routed model), and finish_reason.
Added in v0.9.0.
Example
model = OpenRouterModel.from_api_key( ... "anthropic/claude-haiku-4-5", api_key="sk-or-...", ... ) strategy = OpenRouterStrategy(model)
Source code in src/llm_minder/llm_strategies.py
DeepSeekStrategy
llm_minder.DeepSeekStrategy
DeepSeekStrategy(model: LLMModel, response_parser: Callable[[LLMResponse], TOutput] | None = None, *, temperature: float | None = None, generation_config: dict[str, Any] | None = None, quota_scope: object | None = None, token_estimator: TokenEstimator | None = None)
Bases: ModelStrategy[TOutput]
Strategy for calling a DeepSeek model and parsing the response.
Functionally identical to :class:OpenAIStrategy (both delegate to an
LLMModel via :class:ModelStrategy); the separate class exists for
provider-named symmetry. Pair it with :class:DeepSeekModel, which
surfaces DeepSeek's native cache-hit token counts.
Added in v0.10.0.
Example
model = DeepSeekModel.from_api_key("deepseek-v4-flash", api_key="sk-...") strategy = DeepSeekStrategy(model)
Use the model's strict-schema parser when one is configured.
Source code in src/llm_minder/llm_strategies.py
Models
GeminiModel
llm_minder.GeminiModel
GeminiModel(model: str, client: Client, *, safety_settings: list[dict[str, Any]] | None = None, system_instruction: str | None = None, metadata_extractors: list[MetadataExtractor] | None = None, cache_group: str | None = None)
LLM model backed by the Google Gemini API.
Wraps a genai.Client and model name, handling API calls, token extraction, and response normalization. Implements the LLMModel protocol.
Example
client = genai.Client(api_key="...") model = GeminiModel("gemini-3.5-flash", client) response = await model.generate("Hello!") print(response.text, response.input_tokens)
Added in v0.6.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
Model name (e.g., "gemini-3.1-flash-lite-preview"). |
required |
client
|
Client
|
Initialized genai.Client. |
required |
safety_settings
|
list[dict[str, Any]] | None
|
Default safety settings for all calls. |
None
|
system_instruction
|
str | None
|
Default system instruction (overridable per-call). |
None
|
metadata_extractors
|
list[MetadataExtractor] | None
|
Optional hooks that contribute extra keys to
|
None
|
cache_group
|
str | None
|
Label for this model's calls in the per-group cache
statistics, in place of the system-instruction hash. A per-call
|
None
|
Source code in src/llm_minder/models.py
cleanup
async
Close both transports of a factory-owned Gemini client.
Source code in src/llm_minder/models.py
generate
async
generate(prompt: str | list[Any], *, temperature: float | None = None, system_instruction: str | None = None, config: dict[str, Any] | None = None) -> LLMResponse
Generate a response from Gemini.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str | list[Any]
|
Text prompt or list of content parts (multimodal). |
required |
temperature
|
float | None
|
Sampling temperature. Pass |
None
|
system_instruction
|
str | None
|
Override default system instruction. |
None
|
config
|
dict[str, Any] | None
|
Additional provider-specific config entries. |
None
|
Returns:
| Type | Description |
|---|---|
LLMResponse
|
Normalized LLMResponse. |
Source code in src/llm_minder/models.py
594 595 596 597 598 599 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 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 | |
prepare
async
Reopen a closed factory-owned client; caller clients stay untouched.
Source code in src/llm_minder/models.py
GeminiCachedModel
llm_minder.GeminiCachedModel
GeminiCachedModel(model: str, client: Client, cached_content: list[Content], *, cache_ttl_seconds: int = 3600, cache_renewal_buffer_seconds: int = 300, auto_renew: bool = True, cache_tags: dict[str, str] | None = None, safety_settings: list[dict[str, Any]] | None = None, metadata_extractors: list[MetadataExtractor] | None = None, cache_group: str | None = None)
LLM model backed by Google Gemini with context caching.
Wraps a genai.Client with cache lifecycle management. Implements the ManagedLLMModel protocol: call prepare() before first use, cleanup() when done.
IMPORTANT — share one instance across work items. Create ONE GeminiCachedModel and reuse it across every LLMWorkItem that should share the cached context. Constructing a new instance per item defeats caching entirely and can cost 10× more. The framework calls prepare() exactly once per unique instance, so sharing is the intended lifecycle. See examples/example_llm_strategies.py for the pattern.
This provides 70-90% cost savings when shared correctly.
Example
model = GeminiCachedModel( ... "gemini-3.5-flash", client, ... cached_content=[system_instruction, context_docs], ... ) await model.prepare() # finds or creates cache response = await model.generate("Process this") await model.cleanup() # preserves cache for reuse
Added in v0.6.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
Model name (e.g., "gemini-3.5-flash"). |
required |
client
|
Client
|
Initialized genai.Client. |
required |
cached_content
|
list[Content]
|
Content to cache (system instructions, documents). |
required |
cache_ttl_seconds
|
int
|
Cache TTL in seconds (default: 3600 = 1 hour). |
3600
|
cache_renewal_buffer_seconds
|
int
|
Renew this many seconds before expiry (default: 300 = 5 minutes). |
300
|
auto_renew
|
bool
|
Auto-renew expired caches in generate() (default: True). |
True
|
cache_tags
|
dict[str, str] | None
|
Tags for precise cache matching. Encoded into the cache's
|
None
|
safety_settings
|
list[dict[str, Any]] | None
|
Default safety settings for all calls. |
None
|
metadata_extractors
|
list[MetadataExtractor] | None
|
Optional hooks that contribute extra keys to
|
None
|
cache_group
|
str | None
|
Label for this model's calls in the per-group cache
statistics, in place of the cached-content hash. A per-call
|
None
|
Source code in src/llm_minder/models.py
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 | |
cleanup
async
Preserve cache for reuse (does not delete). Idempotent.
Source code in src/llm_minder/models.py
delete_cache
async
Explicitly delete the cache.
Safe to call concurrently: the cache lock serializes delete attempts so the provider API fires at most once, and late callers that arrive after the cache is cleared return silently.
Source code in src/llm_minder/models.py
generate
async
generate(prompt: str | list[Any], *, temperature: float | None = None, system_instruction: str | None = None, config: dict[str, Any] | None = None) -> LLMResponse
Generate a response using the cached context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str | list[Any]
|
Text prompt or multimodal content parts. |
required |
temperature
|
float | None
|
Sampling temperature. Pass |
None
|
system_instruction
|
str | None
|
Not supported with caching — raises ValueError. |
None
|
config
|
dict[str, Any] | None
|
Additional provider-specific config entries. |
None
|
Returns:
| Type | Description |
|---|---|
LLMResponse
|
Normalized LLMResponse. |
Source code in src/llm_minder/models.py
893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 | |
prepare
async
Find or create the Gemini cache. Idempotent.
Source code in src/llm_minder/models.py
OpenAICompatibleModel
llm_minder.OpenAICompatibleModel
OpenAICompatibleModel(model: str, client: AsyncOpenAI, *, system_instruction: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, metadata_extractors: list[MetadataExtractor] | None = None, prompt_cache_key: str | None = None, cache_group: str | None = None)
Base class for OpenAI chat-completions-compatible providers.
Wraps an AsyncOpenAI client pointed at any chat-completions endpoint
(OpenAI itself, OpenRouter, DeepSeek, HuggingFace Inference Providers,
Together, Fireworks, local vLLM, etc.). Subclasses customize the default
base URL, the install-extras hint, the env var read by
:meth:from_api_key, and optionally the token/metadata extractors.
Implements the ManagedLLMModel protocol — :meth:cleanup closes the
underlying AsyncOpenAI client when this model owns it (i.e. it was
constructed via :meth:from_api_key). User-provided clients are left
alone.
Models built with :meth:from_api_key and max_connections=N expose
max_concurrency=N so execution surfaces can diagnose worker/pool
mismatches. It remains None for caller-supplied clients because their
effective transport capacity cannot be inspected reliably.
Added in v0.9.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
Provider model id (e.g. "gpt-6-luna" or "anthropic/claude-haiku-4-5"). |
required |
client
|
AsyncOpenAI
|
Initialized AsyncOpenAI (point |
required |
system_instruction
|
str | None
|
Default system message prepended to each call.
Per-call |
None
|
extra_headers
|
dict[str, str] | None
|
Default headers forwarded on every call (e.g.
OpenRouter's |
None
|
extra_body
|
dict[str, Any] | None
|
Default extra body fields forwarded on every call
(e.g. OpenRouter |
None
|
metadata_extractors
|
list[MetadataExtractor] | None
|
Optional hooks that contribute extra keys to
|
None
|
prompt_cache_key
|
str | None
|
Sent as |
None
|
cache_group
|
str | None
|
Label for this model's calls in the per-group cache
statistics, ahead of the key and the system-instruction hash. A
per-call |
None
|
Source code in src/llm_minder/models.py
1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 | |
cleanup
async
Close the underlying AsyncOpenAI client if this model owns it.
Models constructed directly with OpenAIModel(model, client=...)
do NOT own the client — the caller is expected to close it. Models
constructed via :meth:from_api_key own the client and close it here,
unless an HTTP transport was supplied by the caller. Closing that SDK
client would also close the caller's transport, so both remain open.
A later prepare reopens an owned client with its configured pool size.
Source code in src/llm_minder/models.py
from_api_key
classmethod
from_api_key(model: str, api_key: str | None = None, *, base_url: str | None = None, system_instruction: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, json_mode: bool = False, max_connections: int | None = None, metadata_extractors: list[MetadataExtractor] | None = None, prompt_cache_key: str | None = None, cache_group: str | None = None, _instance_kwargs: dict[str, Any] | None = None, **client_kwargs: Any) -> TM
Build the model with a freshly-constructed AsyncOpenAI client.
The returned model owns the client — its connections are released
when the framework calls :meth:cleanup (typically when the
ParallelBatchProcessor exits).
Uses base_url (if provided) or the class's _default_base_url.
Pass client_kwargs to forward additional kwargs (timeout,
max_retries, http_client, etc.) to the SDK constructor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
Provider model id. |
required |
api_key
|
str | None
|
API key. If
|
None
|
json_mode
|
bool
|
When |
False
|
max_connections
|
int | None
|
Size of the underlying httpx connection pool
(both |
None
|
Source code in src/llm_minder/models.py
1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 | |
generate
async
generate(prompt: str | list[Any], *, temperature: float | None = None, system_instruction: str | None = None, config: dict[str, Any] | None = None) -> LLMResponse
Call client.chat.completions.create and normalize the response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str | list[Any]
|
A string (becomes a single user message) or a list of
OpenAI-shaped message dicts (passed through unchanged — used
for multimodal content and Anthropic-via-OpenRouter
|
required |
temperature
|
float | None
|
Sampling temperature. Pass |
None
|
system_instruction
|
str | None
|
Per-call override for the system message. |
None
|
config
|
dict[str, Any] | None
|
Per-call extra kwargs forwarded to the SDK call (merged
over the instance's |
None
|
Returns:
| Type | Description |
|---|---|
LLMResponse
|
Normalized LLMResponse. |
Source code in src/llm_minder/models.py
1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 | |
prepare
async
Reopen an owned client after cleanup, preserving its configured pool.
Source code in src/llm_minder/models.py
request_concurrency
async
Ask the model to support concurrency parallel requests.
Called by execution surfaces when ProcessorConfig.concurrency is
set (v0.20.0, issue #97), before the first request. When this model
owns its client (built via :meth:from_api_key) and no explicit
max_connections was given, the AsyncOpenAI client is rebuilt
with an httpx pool sized to concurrency (the SDK's default pool
allows 1000 connections but keeps only 100 alive between requests, and
ABL cannot report an unknown capacity) and max_concurrency starts
advertising the new size.
Returns True when the pool was resized. Returns False — leaving the
model untouched — for caller-supplied clients and for models built
with an explicit max_connections (an explicit value always wins;
a genuine contradiction is surfaced by the existing capacity warning
instead).
Source code in src/llm_minder/models.py
OpenAIModel
llm_minder.OpenAIModel
OpenAIModel(model: str, client: AsyncOpenAI, *, system_instruction: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, metadata_extractors: list[MetadataExtractor] | None = None, api_surface: str = 'responses', prompt_cache_key: str | None = None, cache_group: str | None = None)
Bases: _ResponsesSurface, OpenAICompatibleModel
LLM model backed by OpenAI, on the Responses API by default.
Uses the OpenAI SDK's default base URL (https://api.openai.com/v1).
OpenAI's automatic prompt cache surfaces in cached_input_tokens for
prompts longer than ~1024 tokens.
API surface. Since v0.27 requests use the Responses API
(api_surface="responses") with store=False unless the request sets
store. Chat-style request fields are translated: max_tokens →
max_output_tokens, response_format → text.format,
reasoning_effort → reasoning.effort, logprobs → include,
Chat tools/tool_choice → Responses function tools, and Chat message
lists (including assistant tool_calls and tool replies) → Responses
input items. Fields with no Responses equivalent (n, stop,
seed, penalties, logit_bias, legacy functions) and stream /
background raise ValueError before any request. Pass
api_surface="chat_completions" for the previous Chat Completions
behavior. For other OpenAI-compatible servers (vLLM, proxies, Azure-style
endpoints) use :class:OpenAICompatibleModel instead.
A completed Responses call that returns only function calls succeeds with
text="", finish_reason="tool_calls" and the calls in
metadata["tool_calls"]. An incomplete call with text succeeds with
finish_reason="length" (token limit) or "content_filter"; without
text it raises :class:EmptyResponseError, as does a refusal.
Example
model = OpenAIModel.from_api_key("gpt-6-luna", api_key="sk-...") response = await model.generate("Hello!") print(response.text, response.cached_input_tokens)
Added in v0.9.0.
See :class:OpenAICompatibleModel; adds api_surface
("responses", the default, or "chat_completions").
Source code in src/llm_minder/models.py
artifact_identity_extra
property
Replay identity: empty on Chat Completions, so v0.26 checkpoints match.
from_api_key
classmethod
from_api_key(model: str, api_key: str | None = None, *, base_url: str | None = None, system_instruction: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, json_mode: bool = False, max_connections: int | None = None, metadata_extractors: list[MetadataExtractor] | None = None, api_surface: str = 'responses', prompt_cache_key: str | None = None, cache_group: str | None = None, **client_kwargs: Any) -> OpenAIModel
Build an OpenAIModel with an owned client; adds api_surface.
json_mode adds response_format={"type": "json_object"}, which the
Responses surface translates to text.format.
Source code in src/llm_minder/models.py
generate
async
generate(prompt: str | list[Any], *, temperature: float | None = None, system_instruction: str | None = None, config: dict[str, Any] | None = None) -> LLMResponse
Call the selected OpenAI API surface and normalize the response.
Source code in src/llm_minder/models.py
OpenRouterModel
llm_minder.OpenRouterModel
OpenRouterModel(model: str, client: AsyncOpenAI, *, system_instruction: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, metadata_extractors: list[MetadataExtractor] | None = None, prompt_cache_key: str | None = None, cache_group: str | None = None)
Bases: OpenAICompatibleModel
LLM model backed by OpenRouter (https://openrouter.ai).
OpenRouter exposes a unified OpenAI-compatible API for many upstream
providers (Anthropic, OpenAI, Google, Mistral, DeepSeek, etc.). Model
ids are prefixed with the provider, e.g. "anthropic/claude-haiku-4-5".
Caching is provider-dependent:
- OpenAI / Gemini (implicit) / DeepSeek — automatic;
cached_input_tokensis populated when the upstream cache hits. - Anthropic — opt-in. Pass
promptas a list of message dicts withcache_control: {"type": "ephemeral"}markers on the blocks you want cached.
Example
model = OpenRouterModel.from_api_key( ... "anthropic/claude-haiku-4-5", ... api_key="sk-or-...", ... referer="https://my-app.example.com", ... title="My App", ... ) response = await model.generate("Hello!")
Added in v0.9.0.
Source code in src/llm_minder/models.py
1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 | |
from_api_key
classmethod
from_api_key(model: str, api_key: str | None = None, *, base_url: str | None = None, system_instruction: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, json_mode: bool = False, max_connections: int | None = None, referer: str | None = None, title: str | None = None, metadata_extractors: list[MetadataExtractor] | None = None, prompt_cache_key: str | None = None, cache_group: str | None = None, **client_kwargs: Any) -> OpenRouterModel
Build an OpenRouterModel.
If api_key is None, reads OPENROUTER_API_KEY from the
environment and raises ValueError if neither is set. (The
OpenAI SDK doesn't know about OPENROUTER_API_KEY, so we have
to read it ourselves rather than relying on the SDK's default.)
referer and title map to OpenRouter's optional
HTTP-Referer and X-Title headers (used for app attribution
on openrouter.ai's leaderboard).
Source code in src/llm_minder/models.py
DeepSeekModel
llm_minder.DeepSeekModel
DeepSeekModel(model: str, client: AsyncOpenAI, *, system_instruction: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, thinking: bool | None = None, api_surface: str = 'chat_completions', response_schema: type[BaseModel] | Mapping[str, Any] | None = None, schema_name: str | None = None, json_mode: bool = False, metadata_extractors: list[MetadataExtractor] | None = None, cache_group: str | None = None)
Bases: _ResponsesSurface, OpenAICompatibleModel
LLM model backed by DeepSeek's OpenAI-compatible API.
Points at https://api.deepseek.com and reads
DEEPSEEK_API_KEY in :meth:from_api_key. Model ids are bare DeepSeek
names, e.g. "deepseek-v4-flash" or "deepseek-v4-pro".
DeepSeek's automatic context cache reports hits at the top level of the
usage object (prompt_cache_hit_tokens / prompt_cache_miss_tokens)
rather than under OpenAI's nested prompt_tokens_details.cached_tokens —
so this subclass overrides :meth:_extract_tokens to surface them in
cached_input_tokens. Use :attr:CachedTokenRates.DEEPSEEK (~2%) when
computing billable tokens.
(Calling DeepSeek through OpenRouter uses :class:OpenRouterModel
instead; the native cache fields aren't reliably forwarded there, which is
why direct access via this class gives better cache telemetry.)
Thinking mode. DeepSeek's V4 models (deepseek-v4-flash /
deepseek-v4-pro) default to thinking, which for a batch
classification job is a surprising, expensive default — thinking can emit
several times the output tokens (and cost, and latency) of non-thinking.
Pass thinking=False to force non-thinking mode explicitly rather than
relying on the deepseek-chat (non-thinking) / deepseek-reasoner
(thinking) aliases, which DeepSeek is deprecating. Under the hood this sends
extra_body={"thinking": {"type": "disabled"}} for Chat Completions,
or reasoning={"effort": "none"} on the Responses API.
Strict structured output. Set api_surface="responses" and pass a
Pydantic model class or JSON Schema mapping as response_schema. The
schema is sent through text.format.type="json_schema" and
:class:DeepSeekStrategy parses successful output automatically. DeepSeek
validates the requested surface and schema locally; model IDs are passed
through for provider validation without an implicit surface fallback.
Example
model = DeepSeekModel.from_api_key( ... "deepseek-v4-flash", api_key="sk-...", thinking=False ... ) response = await model.generate("Hello!") print(response.text, response.cached_input_tokens)
Added in v0.10.0.
See :class:OpenAICompatibleModel; adds the DeepSeek thinking
toggle and opt-in Responses API structured output. DeepSeek's prefix
cache takes no key, so there is no prompt_cache_key; cache_group
labels calls for the per-group cache statistics.
Source code in src/llm_minder/models.py
2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 | |
artifact_identity_extra
property
Replay identity fields that distinguish transport/output contracts.
from_api_key
classmethod
from_api_key(model: str, api_key: str | None = None, *, base_url: str | None = None, system_instruction: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, json_mode: bool = False, max_connections: int | None = None, thinking: bool | None = None, api_surface: str = 'chat_completions', response_schema: type[BaseModel] | Mapping[str, Any] | None = None, schema_name: str | None = None, metadata_extractors: list[MetadataExtractor] | None = None, cache_group: str | None = None, **client_kwargs: Any) -> DeepSeekModel
Build a DeepSeekModel; reads DEEPSEEK_API_KEY when api_key is
None. Adds the thinking toggle and Responses API structured-output
options (see the class docstring) on top of the shared constructor.
Source code in src/llm_minder/models.py
generate
async
generate(prompt: str | list[Any], *, temperature: float | None = None, system_instruction: str | None = None, config: dict[str, Any] | None = None) -> LLMResponse
Call the selected DeepSeek API surface and normalize its response.
Source code in src/llm_minder/models.py
parse_structured_response
Parse provider-enforced output into its Pydantic type or JSON value.
Source code in src/llm_minder/models.py
Protocols
LLMModel
llm_minder.LLMModel
Bases: Protocol
Protocol for LLM model instances that can generate responses.
Implementations wrap a specific provider's client and model configuration, handling API calls and response normalization. Strategies call generate() without needing to know about provider-specific details.
Added in v0.6.0.
generate
async
generate(prompt: str | list[Any], *, temperature: float | None = None, system_instruction: str | None = None, config: dict[str, Any] | None = None) -> LLMResponse
Generate a response from the LLM.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompt
|
str | list[Any]
|
Text prompt, or list of content parts for multimodal input. |
required |
temperature
|
float | None
|
Sampling temperature. The default |
None
|
system_instruction
|
str | None
|
System instruction override (None = use default). |
None
|
config
|
dict[str, Any] | None
|
Provider-specific configuration (e.g., response_mime_type). |
None
|
Returns:
| Type | Description |
|---|---|
LLMResponse
|
Normalized LLMResponse with text, token counts, and metadata. |
Source code in src/llm_minder/core/protocols.py
ManagedLLMModel
llm_minder.ManagedLLMModel
Bases: LLMModel, Protocol
LLMModel with lifecycle management (e.g., caching).
Models that need one-time setup (creating a cache) or cleanup implement this protocol. The strategy delegates prepare/cleanup calls to the model. Preparation must support reuse after cleanup; cleanup must tolerate a partially completed previous cleanup. Overlapping hosts share a strategy lifecycle within one event loop. Built-in OpenAI-compatible models also share ownership across distinct ModelStrategy instances.
Added in v0.6.0.
cleanup
async
LLMResponse
llm_minder.LLMResponse
dataclass
LLMResponse(text: str, input_tokens: int, output_tokens: int, total_tokens: int, cached_input_tokens: int = 0, metadata: dict[str, Any] | None = None, raw: Any = None, cache_write_input_tokens: int = 0)
Bases: ProviderOutputViews
Normalized response from any LLM provider.
Returned by LLMModel.generate(). Provides a provider-agnostic interface so strategies don't need to know about Gemini, OpenAI, etc. response formats.
Attributes:
| Name | Type | Description |
|---|---|---|
text |
str
|
The response text content. |
input_tokens |
int
|
Number of input/prompt tokens. |
output_tokens |
int
|
Number of output/completion tokens. |
total_tokens |
int
|
Total tokens used. |
cached_input_tokens |
int
|
Input tokens served from cache (0 if no caching). |
metadata |
dict[str, Any] | None
|
Provider-specific metadata (safety ratings, finish reason, etc.).
The keys |
raw |
Any
|
The raw provider response object, for edge cases. |
cache_write_input_tokens |
int
|
Input tokens written to the prompt cache (0 if none were reported). |
Added in v0.6.0.
MetadataExtractor
A callable that takes the raw provider response and returns extra keys for
LLMResponse.metadata, or None. Pass a list of them as a built-in model's
metadata_extractors argument; their keys are merged over the built-in metadata.
llm_minder.MetadataExtractor
module-attribute
Provider notes
DeepSeek strict JSON Schema output
DeepSeek's Responses API can enforce a JSON Schema before ABL parses the result. Pass a Pydantic model class to get that model back directly:
from pydantic import BaseModel
from llm_minder import DeepSeekModel, DeepSeekStrategy
class Verdict(BaseModel):
valid: bool
reason: str
model = DeepSeekModel.from_api_key(
"deepseek-v4-flash",
api_surface="responses",
response_schema=Verdict,
thinking=False,
max_connections=64,
)
strategy = DeepSeekStrategy(
model,
generation_config={"max_tokens": 256}, # mapped to max_output_tokens
)
A JSON Schema mapping is also accepted; the default strategy output is then
the decoded JSON value. schema_name= overrides the Pydantic class name or
schema title used in the provider request.
This mode runs through the ordinary LLMCallPool and batch strategy path, so
capacity admission, proactive quotas, retries, timing, token/cache accounting,
and cost calculation remain active. Result metadata includes api_surface,
provider_request_id, and response_schema (name, Python/schema identity,
and canonical SHA-256). The same API/schema fields participate in inferred
JSONL/SQLite artifact identity, preventing Chat JSON-mode or different-schema
results from being replayed as compatible strict outputs.
ABL validates the surface and schema locally and passes model IDs through to
DeepSeek for validation. See the provider Responses reference
for available models. The deepseek extra requires OpenAI SDK 1.66.2 or newer;
caller-supplied clients must expose callable responses.create. This does not
add multimodal work-item support or an automatic surface fallback. The explicit fallback is Chat Completions
with json_mode=True plus pydantic_json_parser(...); it requests valid JSON
but does not enforce the schema and therefore cannot safely repair malformed
JSON inside the object. Provider/schema rejection is non-retryable under
structured_output_schema_rejected; an accepted response that fails local
validation is retryable under structured_output_validation_error.
Provider classifier categories
PydanticAIStrategy recommends the exported PydanticAIErrorClassifier.
It classifies ModelHTTPError by HTTP status (429 rate limit, 401 authentication,
403 permission denied, other deterministic 4xx client errors, transient 5xx
server errors). UsageLimitExceeded is non-retryable with category
usage_limit_exceeded. Exact UnexpectedModelBehavior remains a retryable
validation_error; its content-filter and incomplete-tool-call subclasses
retain ordinary retry backoff.
All built-in classifiers handle framework deadlines, aborts and framework
timeouts before provider matching, along with named middleware-contract and
quota-scope errors. Terminal rate-limit retry exhaustion uses
rate_limit_retries_exceeded with is_rate_limit=False. Empty provider
responses use non-retryable empty_response, and truncated ones
(TruncatedResponseError) non-retryable response_truncated; both exceptions
remain ValueError subclasses. Structured-output schema rejection is non-retryable
structured_output_schema_rejected; local output validation is retryable
structured_output_validation_error.
Gemini provider timeouts use timeout, including bare TimeoutError().
Default, OpenAI and OpenRouter provider timeouts use api_timeout.
Gemini daily quota exhaustion uses non-retryable quota_exhausted only when
every reported quota violation has an explicit per-day quota ID; ambiguous and
mixed daily/minute violations remain rate limits. Gemini RPC retry-delay hints
are parsed alongside HTTP headers.
Category changes affect per-category metrics and abort_on_error_categories.
The string factory rejects unknown model kwargs with a list of accepted names,
and unresolved OpenAI credentials raise ValueError.