OpenAI Integration
First-class OpenAI support arrived in v0.9.0 via OpenAIModel,
OpenAIStrategy, and OpenAIErrorClassifier.
Installation
Authentication
Set the OPENAI_API_KEY environment variable, or pass api_key= directly to
OpenAIModel.from_api_key().
Quick start
import asyncio
from async_batch_llm import (
LLMWorkItem,
OpenAIErrorClassifier,
OpenAIModel,
OpenAIStrategy,
ParallelBatchProcessor,
ProcessorConfig,
)
async def main() -> None:
model = OpenAIModel.from_api_key("gpt-4o-mini", api_key="sk-...")
strategy = OpenAIStrategy(model)
config = ProcessorConfig(max_workers=5, timeout_per_item=30.0)
async with ParallelBatchProcessor[None, str, None](
config=config,
error_classifier=OpenAIErrorClassifier(),
) as processor:
await processor.add_work(
LLMWorkItem(item_id="hello", strategy=strategy, prompt="Hi!")
)
result = await processor.process_all()
print(result.results[0].output)
asyncio.run(main())
Choosing a model
OpenAIModel accepts any model id the OpenAI chat completions endpoint
serves: gpt-4o, gpt-4o-mini, o1, o3-mini, etc. Reasoning models
(o1, o3) work, but if you need reasoning summaries or server-side tools,
the Responses API
is a better fit; that's a future addition (OpenAIResponsesModel).
Reasoning models reject an explicit
temperature. Passtemperature=None(on the strategy or pergenerate()call) to omit the parameter entirely so the model uses its default — otherwise the call fails:
Structured output
Use the json_mode=True convenience to request JSON, and the built-in
pydantic_json_parser helper to parse it. The parser strips markdown code
fences before validating, so providers that wrap JSON in ```json ... ```
(DeepSeek does this even in JSON mode) validate cleanly instead of burning
retries on the fence characters:
from pydantic import BaseModel
from async_batch_llm import OpenAIModel, OpenAIStrategy, pydantic_json_parser
class Sentiment(BaseModel):
sentiment: str
confidence: float
model = OpenAIModel.from_api_key(
"gpt-4o-mini",
api_key="sk-...",
json_mode=True, # adds response_format={"type": "json_object"}
system_instruction='Respond with JSON: {"sentiment": ..., "confidence": ...}',
)
strategy = OpenAIStrategy(model, pydantic_json_parser(Sentiment))
json_mode=True is shorthand for
extra_body={"response_format": {"type": "json_object"}}; an explicit
response_format you pass in extra_body takes precedence. Most providers
still require the word "JSON" somewhere in the prompt for JSON mode to engage.
For OpenAI specifically, client.chat.completions.parse(response_format=...)
also works — wrap it in a custom strategy that calls parse() directly. Kept
out of OpenAIModel.generate() so the same class can serve every
OpenAI-compatible provider.
Prompt caching
OpenAI automatically caches prompt prefixes longer than ~1024 tokens. No
client action is required — cached_input_tokens is populated on hits, and
BatchResult.total_cached_tokens aggregates across the batch.
from async_batch_llm import CachedTokenRates
result = await processor.process_all()
print(f"input={result.total_input_tokens} cached={result.total_cached_tokens}")
print(f"cache hit rate: {result.cache_hit_rate():.1f}%")
# OpenAI charges 50% of normal for cached tokens — pass the matching rate.
print(f"billable tokens: {result.effective_input_tokens(CachedTokenRates.OPENAI)}")
effective_input_tokens() defaults to CachedTokenRates.GEMINI (10% rate)
for backward compatibility with pre-v0.9.0 versions — always pass an
explicit rate when working with OpenAI to get accurate numbers. As of
v0.10.0, calling it without an explicit rate while cached tokens are present
emits a UserWarning for exactly this reason; passing
CachedTokenRates.OPENAI silences it. Note that Anthropic charges a 25%
premium on cache writes over the normal input price; that write premium is
not modeled by this helper.
Reasoning traces, tool calls, and logprobs
The OpenAI-compatible models (OpenAIModel, OpenRouterModel,
DeepSeekModel) surface additional structured output under reserved
metadata keys, readable through typed views on each per-item
WorkItemResult (or LLMResponse) — not on the batch-level BatchResult — see
Typed auxiliary output
for the shapes and boundaries (experimental — shapes may change while
they stabilize):
reasoning— the model's reasoning/thinking trace, read frommessage.reasoning_content(DeepSeek reasoner models) with a fallback tomessage.reasoning(OpenRouter). Access viaitem_result.reasoning.tool_calls— tool/function calls the model requested, as[{"id", "name", "arguments"}]withargumentskept as the raw JSON string. Access viaitem_result.tool_calls(alist[ToolCall] | None). Visibility only — the framework never executes tools; note that a pure tool-call turn (content=null) raisesEmptyResponseError, so calls surface only alongside returned text.logprobs— the provider logprobs object (as a plain dict viamodel_dump()), when you requested it, e.g.OpenAIStrategy(model, generation_config={"logprobs": True}). Access viaitem_result.logprobs.
Each key is emitted only when present on the response, so default payloads are unchanged unless you asked the model for these features.
Error handling
OpenAIErrorClassifier understands the openai SDK's exception hierarchy:
RateLimitError→ retryable, rate-limit category. If the response carries aRetry-Afterheader, it's parsed intoErrorInfo.suggested_wait, which theRateLimitCoordinatorhonors as a floor on the cooldown (theRateLimitStrategystill owns the default duration when there's no header).APITimeoutError→ retryable, timeout.APIConnectionError→ retryable, network.APIStatusError→ branches onstatus_code:- 429 → rate limit.
- 402 → not retryable,
insufficient_balancecategory, with a remediation hint ("top up your prepaid DeepSeek balance"). Auth has passed, so this otherwise looks like a generic bug; the hint is logged at WARNING when the item gives up. Stops a dead balance from silently burning every retry. - 408/425/500/502/503/504 → retryable server error.
- 400/401/403/404/422 → not retryable (client error / auth / config).
- Pydantic
ValidationError→ retryable (LLM may produce valid output on retry). ValueError/TypeError/etc. → not retryable (logic bug).
Pass it to the processor:
Convenience constructor
OpenAIModel.from_api_key(
model="gpt-4o-mini",
api_key="sk-...",
base_url=None, # override SDK default if needed
system_instruction="...", # default system message
extra_headers={...}, # forwarded on every request
extra_body={"response_format": {...}}, # default per-request kwargs
max_connections=50, # size the httpx pool to match max_workers
timeout=30.0, # forwarded to AsyncOpenAI
)
Connection pool sizing (max_connections)
The openai SDK uses httpx's default connection pool (~100 connections). If you
raise ProcessorConfig(max_workers=...) above that, the extra workers just
block waiting for a connection — throughput plateaus with no warning. This
bites high-concurrency providers like DeepSeek hardest (it allows thousands of
concurrent connections, so the ~100 default — not the API — is your ceiling).
Pass max_connections to size the pool to your worker count:
# Match the pool to max_workers (a little headroom doesn't hurt).
model = OpenAIModel.from_api_key("gpt-4o-mini", max_connections=150)
config = ProcessorConfig(max_workers=150, timeout_per_item=60.0)
max_connections sets both max_connections and max_keepalive_connections
on the underlying httpx.AsyncClient. It's a convenience for the common case;
if you need finer control, build your own http_client=httpx.AsyncClient(...)
and pass that instead (the two are mutually exclusive).
Slow-start, too. Even with the pool raised, the default
RateLimitConfigslow-start ramp bounds time-to-full-throughput on the first ~50 items. If you're chasing peak throughput, tune that as well.
Subclassing for other OpenAI-compatible providers
OpenAICompatibleModel is exported so you can target Together, Fireworks,
local vLLM, etc. with a few lines:
from async_batch_llm import OpenAICompatibleModel
class TogetherModel(OpenAICompatibleModel):
_default_base_url = "https://api.together.xyz/v1"
_install_extras = "openai"
The built-in DeepSeekModel is exactly this pattern — it additionally
overrides _extract_tokens to read DeepSeek's native cache-hit field. Read
its source for a worked example of customizing token extraction.
See also
docs/OPENROUTER_INTEGRATION.md— the multi-provider sibling.DeepSeekModel/DeepSeekStrategy— direct DeepSeek access with native cache-hit tracking (install[deepseek]); seeexamples/example_deepseek.py.examples/example_openai.py— runnable example.