Custom Strategies
Learn how to create custom strategies for any LLM provider.
Two shortcuts come first. OpenAI, Gemini, OpenRouter, and DeepSeek have built-in
strategies (llm("openai:gpt-6-luna"), or OpenAIStrategy(OpenAIModel(...))), so you
don't need a custom one for them. To wrap an async client you already have, a single
function passed to CallableStrategy is usually enough.
Subclass LLMCallStrategy, as below, when you need lifecycle hooks (prepare(),
cleanup(), on_error()) or per-attempt logic.
Basic Custom Strategy
This raw Chat Completions strategy shows the shape of execute(). It is named
RawChatStrategy so it doesn't shadow the built-in OpenAIStrategy, which uses the
Responses API by default.
from async_batch_llm import LLMCallStrategy
class RawChatStrategy(LLMCallStrategy[str]):
def __init__(self, client, model: str):
self.client = client
self.model = model
async def execute(self, prompt: str, attempt: int, timeout: float, state=None):
response = await self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}]
)
output = response.choices[0].message.content
tokens = {
"input_tokens": response.usage.prompt_tokens,
"output_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens
}
return output, tokens, None
Resource Management
Use prepare() and cleanup() for resource lifecycle:
class CachedStrategy(LLMCallStrategy[str]):
def __init__(self, client, system_instruction: str):
self.client = client
self.system_instruction = system_instruction
self.cache_name = None
async def prepare(self):
"""Create cache before processing."""
self.cache_name = await self.client.create_cache(
content=self.system_instruction
)
async def execute(self, prompt: str, attempt: int, timeout: float, state=None):
# Use the cached content
response = await self.client.generate(
prompt=prompt,
cache_name=self.cache_name
)
return response.text, response.usage, None
async def cleanup(self):
"""Delete cache after processing."""
if self.cache_name:
await self.client.delete_cache(self.cache_name)
Error Handling
Use on_error() to track failures and adjust behavior:
from pydantic import ValidationError
from async_batch_llm import RetryState
class SmartRetryStrategy(LLMCallStrategy[dict]):
def __init__(self, client):
self.client = client
async def on_error(
self, exception: Exception, attempt: int, state: RetryState | None = None
):
"""Track validation errors for smart escalation."""
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: RetryState | None = None
):
# Use cheaper model initially, escalate only on validation errors
failures = state.get("validation_failures", 0) if state is not None else 0
if failures == 0:
model = "cheap-model"
elif failures == 1:
model = "medium-model"
else:
model = "expensive-model"
response = await self.client.generate(prompt, model=model)
return response.output, response.tokens, None
Progressive Temperature
To raise the temperature only after validation failures, count them in RetryState
from on_error(), as in the example above. Keying off attempt would also raise it
after a transport error or timeout. See
Progressive Temperature on Retries
for the full pattern.
Anthropic Example
from anthropic import AsyncAnthropic
class AnthropicStrategy(LLMCallStrategy[str]):
def __init__(self, client: AsyncAnthropic, model: str = "claude-sonnet-5-5"):
self.client = client
self.model = model
async def execute(self, prompt: str, attempt: int, timeout: float, state=None):
response = await self.client.messages.create(
model=self.model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
output = response.content[0].text
tokens = {
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"total_tokens": response.usage.input_tokens + response.usage.output_tokens
}
return output, tokens, None
Usage
from async_batch_llm import ParallelBatchProcessor, LLMWorkItem, ProcessorConfig
async def main():
# Use your custom strategy
strategy = RawChatStrategy(client=openai_client, model="gpt-6-luna")
config = ProcessorConfig(concurrency=5)
async with ParallelBatchProcessor(config=config) as processor:
await processor.add_work(
LLMWorkItem(
item_id="test",
strategy=strategy,
prompt="Hello!"
)
)
result = await processor.process_all()