Python SDK
Fastest install: the one-line curl — it detects the agent frameworks you already have and installs Telem into as many of them as you select in one pass. The steps below are the manual path.
telem-sdk is a typed Python client (httpx + pydantic v2) for the Telem search
orchestration API. It wraps the router’s two operations, Search and Fetch,
and reads back the server’s normalized envelope — one result shape for every
provider — with mirrored synchronous (Telem) and asynchronous (AsyncTelem)
clients that share every behavior.
Install
Section titled “Install”pip install telem-sdkRequires Python 3.10+. Two optional extras add integrations without pulling their dependencies into a plain install:
pip install "telem-sdk[openai]"pip install "telem-sdk[langchain]"[openai]— letsclient.wrap()patch a realopenai.OpenAI/AsyncOpenAIclient in place so the model can call Telem search as a tool.[langchain]— addstelem.integrations.langchain.create_telem_search_tool(), a native LangChain/LangGraph tool.
Authentication
Section titled “Authentication”api_key and base_url resolve the same way, first match wins: an explicit
constructor argument, then TELEM_API_KEY / TELEM_BASE_URL, then
~/.telem/credentials.json (the machine-written file the guided installer
produces), then the defaults — no key, and the hosted deployment for base_url.
See Authentication for the full rules.
from telem import Telem
client = Telem() # reads TELEM_API_KEY / TELEM_BASE_URL from the environment# or, explicitly:client = Telem(api_key="tlm_...", base_url="https://your-router.example.com")That credentials file is the only file the client opens on its own. The
.telem/telem.json that every other Telem tool reads for
search options is opt-in here — an application asks
for it with telem.resolve_search_options(project_root=...) — because a library
must not let a checked-out repository steer an arbitrary program’s spend.
Per-search defaults (tier, fields, provider allow/deny lists, full content)
resolve the same three-step way, through their own constructor arguments and the
TELEM_* variables — see search() below.
Sync and async clients
Section titled “Sync and async clients”Telem and AsyncTelem expose exactly the same surface — every method, argument,
and default.
from telem import Telem
client = Telem()response = client.search("best python http client")print(len(response.results))client.close()import asynciofrom telem import AsyncTelem
async def main(): async with AsyncTelem() as client: response = await client.search("best python http client") print(len(response.results))
asyncio.run(main())Both support the context-manager form; Telem also needs an explicit
client.close() if you don’t use with.
Search
Section titled “Search”search() performs one round trip — one Search operation — and returns the
server’s normalized envelope for every provider that ran:
resp = client.search( "climate policy 2026", tier="extended", # minimalist | default | extended | max providers_include=["exa"], # omit to use the deployment's default provider set num_results=10, # rows PER PROVIDER (server default 5) include_raw=True, # also attach each provider's own response body goal="brief the user", # merged into request metadata)The full option set is tier, fields, providers_include, providers_exclude,
provider_overrides, num_results, include_raw, include_full_content, plus
goal/context/session/metadata. None means unset — fall through to the
client default, then the server’s. tier and fields are mutually exclusive: the
more specifically-set one wins, and only on a same-level tie does fields replace
tier. When both provider halves resolve, the excluded names are subtracted from
the allow-list. Nothing is validated client-side — an unknown tier, field, or
provider name comes back as a BadRequestError (see Errors).
Full parameter reference → — every key, its env
fallback and how the levels compose. num_results and include_raw are call
arguments only: putting them in a config file does nothing.
Multi-query batching
Section titled “Multi-query batching”query also accepts a sequence of strings. They batch into one interaction —
the backend runs them concurrently — and each provider run in by_provider is
tagged with the query it served:
resp = client.search(["onsen towns near kyoto", "best ramen in kyoto"])
for run in resp.by_provider: print(run.batch_index, run.query, run.provider, len(run.results))resp.results stays the flattened list across every run. A one-element sequence
behaves exactly like a plain string.
SearchResponse shape
Section titled “SearchResponse shape”resp.results # flattened list[SearchResult]: providers in run order, rows in envelope orderresp.by_provider # list[ProviderRun] — the primary surface; keeps partial failuresresp.session_id # continue the conversation by passing session=resp.session_idresp.interaction_id # the interaction this call createdresp.status # "succeeded" | "partially_succeeded" | "failed"resp.normalized_schema_version # the contract the server answered with (>= 2 required)by_provider holds one ProviderRun per provider that ran, so a provider that
failed still shows up — status/error set, results empty — instead of silently
vanishing. Each run carries provider, status, results, error, latency_ms,
preprocessor_run_id, tier, fields, query, batch_index, plus
provider-native extras (answer, entities, related, verticals, usage,
warnings, raw).
Each SearchResult exposes url, title, summary, excerpt, full_content,
publish_date, rank, thumbnail, favicon, source, enrichments,
fetch_meta, plus provider and raw (the verbatim envelope row).
result.content is a convenience alias — summary, else
full_content["content"], else "" — computed as a property, not a stored field.
Errors
Section titled “Errors”Every error the SDK raises derives from TelemError and carries .message,
.status_code, and .body:
| Status | Exception |
|---|---|
| 400 | BadRequestError |
| 401 / 403 | AuthError |
| 404 | NotFoundError |
| other non-2xx | APIStatusError |
Two failures never reach a status code: a request that produced no HTTP
response at all raises TelemConnectionError, and a search answered without
the normalized response raises TelemServerVersionError.
from telem import Telem, BadRequestError
try: Telem().search("hi", providers_include=["does-not-exist"])except BadRequestError as exc: print(exc.status_code, exc.message)See Resources → Errors for how the OpenClaw/opencode/pi plugins surface a failed call.