Skip to content

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.

Terminal window
pip install telem-sdk

Requires Python 3.10+. Two optional extras add integrations without pulling their dependencies into a plain install:

Terminal window
pip install "telem-sdk[openai]"
Terminal window
pip install "telem-sdk[langchain]"
  • [openai] — lets client.wrap() patch a real openai.OpenAI/AsyncOpenAI client in place so the model can call Telem search as a tool.
  • [langchain] — adds telem.integrations.langchain.create_telem_search_tool(), a native LangChain/LangGraph tool.

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.

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 asyncio
from 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() 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.

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.

resp.results # flattened list[SearchResult]: providers in run order, rows in envelope order
resp.by_provider # list[ProviderRun] — the primary surface; keeps partial failures
resp.session_id # continue the conversation by passing session=resp.session_id
resp.interaction_id # the interaction this call created
resp.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.

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.