Integrate the Python SDK¶
Use the SDK when AnyInfer runs inside a Python application. Quickstart is the fastest path to a first result; this page is the reference for embedding the SDK properly: the client lifecycle and the error handling a long-lived application needs.
Configure the Client¶
For deployed applications, keep provider identity and routing in the shared configuration file:
import anyinfer as ai
config = ai.load_config("anyinfer.json")
with ai.Client(config.providers, route=config.route) as client:
result = client.generate("Give me a two-sentence status summary.")
print(result.text)
For a small script, construct the same settings directly:
providers = [
ai.ProviderSettings.of(
"anthropic",
api_key="env://ANTHROPIC_API_KEY",
)
]
with ai.Client(providers) as client:
result = client.generate("Hello", target="anthropic:claude-sonnet-4-5")
Credential references are resolved when an adapter is first used and registered for
redaction. Prefer env:// or credential:// references to literals in source code and
configuration files.
One Client, Reused, Then Closed¶
AsyncClient is the native implementation. Client is its thread-safe synchronous
facade; both accept the same arguments and return the same domain types.
async with ai.AsyncClient(config.providers, route=config.route) as client:
result = await client.generate("Explain the result.")
with ai.Client(config.providers, route=config.route) as client:
result = client.generate("Explain the result.")
Choose AsyncClient inside an async application and Client in a synchronous one.
Create one client and reuse it; do not create one per request. Since a client owns
connection pools and any supervised local servers, close it with a context manager or an
explicit close()/aclose() call. One client serves many conversations; continuity
across turns is a session concern, not a client-lifecycle
one.
generate() returns the finished result; to consume events as they arrive, see
streaming.
Shape the Sampling¶
Sampling carries every knob that changes how the model chooses tokens. Every field
defaults to unset, and unset means the provider's own default — AnyInfer never invents a
temperature, and an unset field is omitted from the wire request entirely.
result = client.generate(
"Summarize this changelog.",
sampling=ai.Sampling(
temperature=0.2,
max_output_tokens=400,
seed=1234,
frequency_penalty=0.3,
),
)
seed asks the provider to make a repeated identical request more likely to produce
identical output. Treat it as best-effort: every provider that ships the field documents
it that way, and none promise reproducibility across model revisions.
Not every target has every knob. A provider that cannot honor one emits a
ParameterDropped event naming the parameter and what the
target did instead — the point being that a request accepted and quietly ignored looks
exactly like one that worked.
Ask for Token Probabilities¶
logprobs asks for the model's confidence in what it produced: 0 for each chosen
token's own log-probability, a positive count for that many alternatives beside it.
result = client.generate("Classify: positive or negative?", logprobs=3)
for token in result.logprobs:
alternatives = ", ".join(f"{alt.token}={alt.probability:.2f}" for alt in token.top)
print(f"{token.token!r} p={token.probability:.2f} ({alternatives})")
Each TokenLogprob carries the natural-log value the provider reported, with
probability available when a linear scale reads better. Targets that do not report
probabilities are not silently answered without them: the parameter is reported dropped,
the same as any other unhonored request field.
Ask for Attributions¶
When a request supplies documents, cite_documents=True asks the target to say which of
them each part of its answer came from:
result = client.generate(
grounded_messages,
cite_documents=True,
)
for citation in result.citations:
supported = citation.span_of(result.text)
print(f"{supported!r} ← {citation.title or citation.uri}")
It is off by default and never inferred from the mere presence of a document: every dialect that can do this treats it as a request-side opt-in — a model does not volunteer citations — and several bill a cited answer differently.
The dialects agree on almost nothing, so Citation carries only what a person rendering
an attribution needs, and every field is optional. An absent offset means the provider did
not say where in the answer the citation applies; it does not mean position zero. Use
span_of() rather than slicing by hand — it returns "" for a citation with no offsets,
which is the honest answer, and clamps a provider's off-by-one to a short span rather than
raising mid-render.
Streaming callers get each attribution as it lands, via a CitationDelta, without waiting
for the final result.
Let the Provider Run Its Own Tools¶
Several providers can search the web or execute code inside one request, folding the result into their own answer. Nothing comes back for you to run:
result = client.generate(
"What shipped in Python 3.14?",
target="anthropic:claude-sonnet-4-5",
server_tools=(ai.ServerToolSpec(kind="web_search", max_uses=3),),
)
for use in result.server_tool_uses:
print(f"{use.kind} ran {use.uses} time(s)")
Off by default and never inferred, because each invocation is billed. max_uses bounds
that where the provider can express it — a search tool with no ceiling is an unbounded
line item on a request you thought was fixed-price. Providers that take no ceiling report
it dropped rather than accepting it silently.
Unlike every other unhonored request parameter, a server tool the target cannot run is
refused before dispatch rather than reported dropped. The distinction is what you get
back: a dropped temperature still answers your question, while an answer produced
without the search you asked for is a different answer built from stale training data —
and it looks exactly like a good one. Two things are checked: whether this library has a
wire form for that provider at all, and whether the model itself supports it.
ServerToolUse carries a count, because the question a result must answer here is how
many invocations you paid for. What the tool actually returned arrives on the stream:
ServerToolDelta fires when one starts and again when it finishes, carrying the sources a
search consulted and the output a code execution printed.
async for event in stream:
if isinstance(event, ai.ServerToolDelta) and event.status == "completed":
for source in event.sources:
print(f" {source.title} — {source.url}")
The start event is what distinguishes a pause for a slow search from a stalled connection; the sources on the finish event are what let you render a grounded answer with its grounding, rather than asking the reader to take it on faith. Neither is a content event, so a search that runs before any text is written does not start the first-token clock.
Run a Batch at Half Price¶
Providers sell a deferred tier at roughly half the per-token price, answered within a window rather than immediately. That is the shape of an eval, a backfill, or an offline enrichment run:
batch = ai.BatchGenerationRequest(
requests=tuple(
ai.GenerationRequest(messages=(ai.user(question),), schema=Answer)
for question in questions
),
custom_ids=tuple(row_ids),
)
handle = client.submit_batch(batch, target="anthropic:claude-sonnet-4-5")
save_somewhere(handle) # AnyInfer stores nothing
Each line is the same GenerationRequest a live call takes, translated through the same
wire builder — so a batched request carries the schema, tools, cache marks, and reasoning
effort its live twin would. That is the whole argument for batching through this library
rather than around it: the typed request model and cost accounting are more valuable on
your highest-volume traffic, not less.
Five providers are bound, across three genuinely different lifecycle shapes, and all of it stays behind the same four calls:
| Provider | How the job travels |
|---|---|
| Anthropic | The whole job as one JSON request |
| OpenAI, Groq | Uploaded as a file; results come back as two more, successes and rejections |
| Bedrock, Vertex | Staged as an object in your own S3 or GCS bucket, referenced by URI |
The third shape needs one piece of configuration the others do not, because those APIs never carry a batch over the wire at all:
ai.ProviderSettings.of(
"bedrock",
options={
"region": "us-east-1",
"batch_s3_uri": "s3://my-bucket/anyinfer",
"batch_role_arn": "arn:aws:iam::...:role/AnyInferBatch",
},
)
The bucket is yours. AnyInfer writes the input object, submits the job, and reads the
answers back; it never creates or manages storage on your account. Vertex takes a single
batch_gcs_uri the same way.
The handle is yours to persist. Run retention is a stated non-goal, and a job answered hours later in another process is exactly where it would be most tempting to break it — so there is no registry here to look it up in later.
report = client.batch_status(handle)
if report.finished:
result = client.fetch_batch(handle)
for line in result.lines:
...
Polling and fetching are separate calls because their costs are: a provider charges nothing to ask about a job and real bandwidth to download one, and a caller waiting on a 24-hour window asks many times and fetches once.
Lines come back in submission order even though providers return them in completion
order, so you can zip results against your own inputs without sorting. A batch is not
all-or-nothing: result.succeeded and result.failed split the lines, because providers
run and bill what worked even when one request was malformed.
Handle Failures¶
All public failures derive from AnyInferError and carry structured fields. Branch on
those fields when behavior matters; show hint to the operator:
try:
result = client.generate("Hello", target="medium")
except ai.AnyInferError as exc:
logger.error("generation failed during %s: %s", exc.phase, exc)
if exc.hint:
logger.info("next step: %s", exc.hint)
The error catalog lists every exception, when it is raised, and what the user will see.
Key Takeaways
AsyncClientis the native implementation;Clientis its thread-safe synchronous facade over the same surface.- Create one client, reuse it, and close it: it owns connection pools and any supervised local servers.
- Catch
AnyInferError, branch on its structured fields, and surfacehint.