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.
async_batch_llm.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/async_batch_llm/callable_strategy.py
artifact_identity
property
Stable artifact identity, or None when the caller omitted one.
CallOutcome
async_batch_llm.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
async_batch_llm.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/async_batch_llm/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/async_batch_llm/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/async_batch_llm/llm_strategies.py
execute
abstractmethod
async
execute(prompt: str, attempt: int, timeout: float, state: RetryState | None = None) -> tuple[TOutput, TokenUsage] | 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 |
|---|---|
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
Either a 2-tuple |
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
TokenUsage dict with optional keys |
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
|
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
pass |
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
deprecated since v0.27 ( |
tuple[TOutput, TokenUsage] | tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
rejected in 1.0; built-in strategies all return the 3-tuple shape. |
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/async_batch_llm/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/async_batch_llm/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/async_batch_llm/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/async_batch_llm/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_tokensmetadata(dict[str, Any] | None): Provider metadata forwarded toWorkItemResult.metadata;Nonewhen there is none
Returning the 2-tuple (output, token_usage) is deprecated since v0.27 and emits a
DeprecationWarning pointing at your execute(); it will be rejected in 1.0.
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 async_batch_llm 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.
async_batch_llm.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/async_batch_llm/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.
cleanup
async
Delegate to model.cleanup() if the model has a managed lifecycle.
Source code in src/async_batch_llm/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. |
tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
The framework still accepts the legacy 2-tuple shape from custom |
tuple[TOutput, TokenUsage, dict[str, Any] | None]
|
strategies, but that is deprecated since v0.27 and removed in 1.0. |
Source code in src/async_batch_llm/llm_strategies.py
prepare
async
Delegate to model.prepare() if the model has a managed lifecycle.
Source code in src/async_batch_llm/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/async_batch_llm/llm_strategies.py
PydanticAIStrategy
async_batch_llm.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/async_batch_llm/llm_strategies.py
dry_run
async
Return mock output based on agent's result_type for dry-run mode.
Source code in src/async_batch_llm/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/async_batch_llm/llm_strategies.py
Structured JSON Parsing
async_batch_llm.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:~async_batch_llm.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 async_batch_llm import DeepSeekModel, DeepSeekStrategy from async_batch_llm.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/async_batch_llm/parsing.py
async_batch_llm.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/async_batch_llm/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
async_batch_llm.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/async_batch_llm/llm_strategies.py
OpenAIStrategy
async_batch_llm.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/async_batch_llm/llm_strategies.py
OpenRouterStrategy
async_batch_llm.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/async_batch_llm/llm_strategies.py
DeepSeekStrategy
async_batch_llm.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/async_batch_llm/llm_strategies.py
Models
GeminiModel
async_batch_llm.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)
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
|
Source code in src/async_batch_llm/models.py
cleanup
async
Close both transports of a factory-owned Gemini client.
Source code in src/async_batch_llm/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/async_batch_llm/models.py
prepare
async
Reopen a closed factory-owned client; caller clients stay untouched.
Source code in src/async_batch_llm/models.py
GeminiCachedModel
async_batch_llm.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)
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
|
Source code in src/async_batch_llm/models.py
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 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 | |
cleanup
async
Preserve cache for reuse (does not delete). Idempotent.
Source code in src/async_batch_llm/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/async_batch_llm/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/async_batch_llm/models.py
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 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 | |
prepare
async
Find or create the Gemini cache. Idempotent.
Source code in src/async_batch_llm/models.py
OpenAICompatibleModel
async_batch_llm.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)
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
|
Source code in src/async_batch_llm/models.py
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 | |
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/async_batch_llm/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, _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/async_batch_llm/models.py
1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 | |
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/async_batch_llm/models.py
1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 | |
prepare
async
Reopen an owned client after cleanup, preserving its configured pool.
Source code in src/async_batch_llm/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/async_batch_llm/models.py
OpenAIModel
async_batch_llm.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')
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/async_batch_llm/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', **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/async_batch_llm/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/async_batch_llm/models.py
OpenRouterModel
async_batch_llm.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)
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/async_batch_llm/models.py
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 | |
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, **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/async_batch_llm/models.py
DeepSeekModel
async_batch_llm.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)
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.
Source code in src/async_batch_llm/models.py
2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 | |
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, **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/async_batch_llm/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/async_batch_llm/models.py
parse_structured_response
Parse provider-enforced output into its Pydantic type or JSON value.
Source code in src/async_batch_llm/models.py
Protocols
LLMModel
async_batch_llm.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/async_batch_llm/core/protocols.py
ManagedLLMModel
async_batch_llm.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
async_batch_llm.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)
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. |
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.
async_batch_llm.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 async_batch_llm 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; the exception remains a
ValueError subclass. 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.