Skip to content

SDKs and frameworks

Install telem-sdk, and your Python code can search the web and read pages through Telem.

Using a coding agent? Find the one you use on Integrations.

telem-sdk is a typed Python client (httpx + pydantic v2) for the Telem search orchestration API. It searches the web, reads pages, groups calls into sessions, runs monitors and lists the providers. It reads back the server’s normalized response, which has one result shape for every provider. The synchronous client (Telem) and the asynchronous client (AsyncTelem) mirror each other and share every behavior.

Terminal window
pip install telem-sdk

It requires Python 3.10+. Two optional extras add integrations. A plain install does not pull in their dependencies:

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. The model can then call Telem search as a tool.
  • [langchain]: adds telem.integrations.langchain.create_telem_search_tool(), a native LangChain/LangGraph tool.

Create an API key under API keys in the Telem console. The guided installer writes it to ~/.telem/credentials.json. You can also set it as an environment variable, or pass it in code.

api_key and base_url resolve the same way, in this order. The first match wins:

  1. An explicit constructor argument.
  2. TELEM_API_KEY / TELEM_BASE_URL.
  3. ~/.telem/credentials.json, the machine-written file that the guided installer produces.
  4. The defaults: no key, and the hosted deployment for base_url.

See API keys for the full rules.

from telem import Telem
client = Telem() # environment, then ~/.telem/credentials.json
# 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. Every other Telem tool reads .telem/telem.json for search options. Here, that file is opt-in. An application asks for it with telem.resolve_search_options(project_root=...), and for fetch options with telem.resolve_fetch_options(project_root=...). The reason: a library must not let a checked-out repository control the spend of an arbitrary program.

Per-search defaults resolve in the same way: a constructor argument, then a TELEM_* variable, then the server’s default. The constructor arguments are default_tier, default_fields, default_providers_include, default_providers_exclude, default_include_full_content and default_auto_routing. timeout (in seconds, default 60) and user_agent set up the HTTP client. A call argument to search() overrides the default.

client = Telem(default_tier="extended", default_providers_include=["exa", "brave"], timeout=30)

client.providers() lists the search providers the deployment runs, as ProviderInfo objects with name, type, description, active_by_default and auto_routed. auto_routed is True when auto-routing can choose the provider without a provider list, and None when the router does not say.

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. If you do not use with, Telem also needs an explicit client.close().

Use Telem from Python

resp = client.search(
"climate policy 2026",
tier="extended", # minimalist | default | extended | max
providers_include=["exa"], # omit to use the default provider set
num_results=10, # rows per provider (server default 5)
)
for result in resp.results:
print(result.title, result.url)

resp.results is one list across all providers, and resp.by_provider keeps each provider’s run, failed ones included. Every option is on Search parameters.

Batches, the response, and every option

A batch of queries. query also accepts a sequence of strings. They batch into one interaction, and the backend runs them concurrently. 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.

The response.

resp.results # flattened list[SearchResult]: providers in run order, rows in envelope order
resp.by_provider # list[ProviderRun]: one per provider that ran, failed ones included
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"

A provider that failed still has a ProviderRun, with status and error set and results empty. Each run has provider, status, results, error, latency_ms, preprocessor_run_id, tier, fields, query and batch_index. It also has provider-native extras: answer, entities, related, verticals, usage, warnings and raw.

Each SearchResult has url, title, summary, excerpt, full_content, publish_date, rank, thumbnail, favicon, source, enrichments, fetch_meta, provider and raw. raw is the row exactly as the server sent it. result.content is a shortcut: summary, else full_content["content"], else "".

Every option. search() takes tier, fields, providers_include, providers_exclude, provider_overrides, num_results, include_raw, include_full_content, auto_routing, published_after, published_before, include_domains, goal, context, session and metadata.

  • None means unset. The value then falls through to the client default, and then to the server’s default.
  • published_after and published_before take an ISO 8601 string, a date or a datetime. A datetime with no time zone is local time.
  • tier and fields do not go together. The one set at the more specific level wins. Only on a tie at the same level does fields replace tier.
  • When both provider lists resolve, the excluded names are removed from the allow-list. If that leaves no provider, search() raises ValueError before it sends anything.
  • Nothing else is checked in the client. An unknown tier, field or provider name comes back as a BadRequestError. See Errors.
  • num_results and include_raw are call arguments only. In a config file, they do nothing.

Every error that the SDK raises derives from TelemError. Each error has .message, .status_code and .body:

Status Exception
400 BadRequestError
401 / 403 AuthError
404 NotFoundError
other non-2xx APIStatusError

The Python SDK does not retry. On a 429 or a 5xx, wait and send the call again in your own code: see Rate limits.

Two failures have no status code. A request that got no HTTP response at all raises TelemConnectionError. 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 Errors for every status code and what to do.

Was this page helpful?