Official Python SDK for the LLMLayer Search & Answer API
Project description
LLMLayer Python SDK (API v2)
Search → Reason → Cite with one call.
Official Python client for the LLMLayer Web API v2 — typed models, streaming via SSE, and production‑ready ergonomics.
Table of Contents
Overview
LLMLayer unifies web search, context building, and LLM reasoning behind a clean API. The Python SDK provides:
- Answer (blocking & streaming via SSE)
- Vertical Web Search (general/news/images/videos/shopping/scholar)
- Scraping (markdown/html/pdf/screenshot)
- Extract (structured data, summaries, Q&A, links, and brand profiles from one page)
- PDF text extraction
- YouTube transcript + metadata
- Site Map discovery
- Crawl Stream (stream markdown pages; usage billed per successful page)
All endpoints use typed Pydantic models, mapped exceptions, and support both sync and async via httpx.
What’s new in v2 (breaking changes)
- Routes moved to
/api/v2for all endpoints. - Answer response field renamed:
answer(wasllm_response). - Answer streaming content frames use
{ "type": "answer", "content": "..." }(wastype: "llm"). - Scrape accepts
formats: List["markdown"|"html"|"screenshot"|"pdf"]and returnsmarkdown/html/pdf/screenshot,title,metadata, andstatusCode. - Extract returns structured extraction results in
structured_data. - Map response now uses
statusCode(camelCase). - Crawl request takes a single
url(notseeds) and returns markdown-only page frames:page/usage/done/error. - YouTube response includes metadata:
title,description,author,views,likes,date.
Installation
pip install llmlayer
# or
pipx install llmlayer
Python: 3.9+ recommended (tested 3.9–3.12). The SDK uses httpx under the hood.
Authentication
All requests require a bearer token:
Authorization: Bearer YOUR_LLMLAYER_API_KEY
- Pass
api_key=...to the client or set the environment variableLLMLAYER_API_KEY. - Missing/invalid keys raise
AuthenticationError.
Never embed your API key in public client code. Run calls from trusted server code.
Quickstart
Synchronous
from llmlayer import LLMLayerClient
client = LLMLayerClient() # reads LLMLAYER_API_KEY from env
resp = client.answer(
query="What are the latest AI breakthroughs?",
model="llmlayer-web",
return_sources=True,
)
print(resp.answer)
print("sources:", len(resp.sources))
Asynchronous
import asyncio
from llmlayer import LLMLayerClient
async def main():
client = LLMLayerClient()
resp = await client.answer_async(
query="Explain edge AI in one short paragraph",
model="llmlayer-fast",
)
print(resp.answer)
asyncio.run(main())
Answer API
When to use blocking vs streaming
- Blocking (
POST /api/v2/answer): you need the complete answer before proceeding or you want structured JSON (answer_type='json'withjson_schema). - Streaming (
POST /api/v2/answer_stream): chat UIs, progressive rendering, lower perceived latency.
Note: Streaming does not support
answer_type='json'. Use blocking for structured output.
Blocking Answer — POST /api/v2/answer
from llmlayer import LLMLayerClient
client = LLMLayerClient()
resp = client.answer(
query="Explain quantum computing in simple terms",
model="llmlayer-web",
temperature=0.7,
max_tokens=1000,
return_sources=True,
)
print(resp.answer)
print("sources:", len(resp.sources))
print("total cost =", (resp.model_cost or 0) + (resp.llmlayer_cost or 0))
Structured JSON output
import json
from llmlayer import LLMLayerClient
schema = {
"type": "object",
"properties": {
"topic": {"type": "string"},
"bullets": {"type": "array", "items": {"type": "string"}}
},
"required": ["topic", "bullets"],
}
client = LLMLayerClient()
resp = client.answer(
query="Return a topic and 3 bullets about transformers",
model="llmlayer-web",
answer_type="json",
json_schema=schema, # dict allowed; client serializes to JSON string
)
data = resp.answer if isinstance(resp.answer, dict) else json.loads(resp.answer)
print(data["topic"], len(data["bullets"]))
Streaming Answer — POST /api/v2/answer_stream
The response is Server‑Sent Events (SSE) with data‑only JSON frames that include a type discriminator.
Sync streaming
from llmlayer import LLMLayerClient
client = LLMLayerClient()
text = []
for event in client.stream_answer(
query="History of the Internet in 5 lines",
model="llmlayer-fast",
return_sources=True,
):
t = event.get("type")
if t == "answer": # v2: 'answer' (not 'llm')
chunk = event.get("content", "")
print(chunk, end="")
text.append(chunk)
elif t == "sources":
print("\n[SOURCES]", len(event.get("data", [])))
elif t == "images":
print("\n[IMAGES]", len(event.get("data", [])))
elif t == "usage":
print("\n[USAGE]", event)
elif t == "done":
print("\n✓ finished in", event.get("response_time"), "s")
Async streaming
import asyncio
from llmlayer import LLMLayerClient
async def main():
client = LLMLayerClient()
async for event in client.stream_answer_async(
query="Three concise benefits of edge AI",
model="llmlayer-fast",
return_sources=True,
):
if event.get("type") == "answer":
print(event.get("content", ""), end="")
asyncio.run(main())
Request Parameters (complete reference)
Use the keyword names shown below.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
query |
str |
✅ | — | The question or instruction. |
model |
str |
✅ | — | LLM id, for example llmlayer-web, llmlayer-fast, openai/gpt-4o-mini, or openai/gpt-5.1. Unsupported values fall back to llmlayer-web for backward compatibility. |
provider_key |
str |
— | Accepted for backward compatibility but currently ignored; provider usage is not billed to the user's provider account. | |
location |
str |
'us' |
Market/geo bias for search. | |
system_prompt |
str | None |
None |
Override the default system prompt for non-JSON answers. | |
response_language |
str |
'auto' |
Output language; 'auto' infers from the query. |
|
answer_type |
'markdown' | 'html' | 'json' |
'markdown' |
Output format. If 'json', you must provide json_schema. Not supported by streaming. |
|
search_type |
'general' | 'news' |
'general' |
Search vertical. Use search_web() for shopping, videos, images, and scholar. |
|
json_schema |
str | dict | None |
None |
Required when answer_type='json'. Dicts are serialized automatically. |
|
citations |
bool |
False |
Embed inline citation markers. | |
return_sources |
bool |
False |
Include aggregated sources and emit a sources frame in streaming. |
|
return_images |
bool |
False |
Include image results. | |
date_filter |
'anytime' | 'hour' | 'day' | 'week' | 'month' | 'year' |
'anytime' |
Recency filter. | |
max_tokens |
int |
1500 |
Max LLM output tokens. | |
temperature |
float |
0.7 |
Sampling temperature. | |
domain_filter |
list[str] | None |
None |
Include domains normally; exclude with -domain.com. |
|
max_queries |
int |
1 |
Number of search sub-queries. Each adds a small LLMLayer fee and may improve coverage. | |
search_context_size |
'low' | 'medium' | 'high' |
'medium' |
How much context to feed the LLM. |
Supported locations (examples):
us, ca, uk, mx, es, de, fr, pt, be, nl, ch, no, se, at, dk, fi, tr, it, pl, ru, za, ae, sa, ar, br, au, cn, kr, jp, in, ps, kw, om, qa, il, ma, eg, ir, ly, ye, id, pk, bd, my, ph, th, vn
Response Shape
AnswerResponse (Pydantic model)
{
"answer": str | dict, # markdown/html string, or dict for JSON answers
"response_time": float | str, # e.g. "1.23"
"input_tokens": int,
"output_tokens": int,
"sources": list[dict], # present when return_sources=True
"images": list[dict], # present when return_images=True
"model_cost": float | None, # None for fixed-price LLMLayer models
"llmlayer_cost": float | None
}
Streaming Frames
The server emits JSON frames over SSE with a type discriminator:
type |
Payload Keys | Meaning |
|---|---|---|
answer |
content: str |
Partial LLM text chunk (v2) |
sources |
data: list[dict] |
Aggregated sources |
images |
data: list[dict] |
Relevant images |
usage |
input_tokens: int, output_tokens: int, model_cost: float | None, llmlayer_cost: float |
Token/cost summary |
done |
response_time: str |
Completion |
error |
error: str |
Error frame (raised by SDK) |
The client handles multi‑line
data:frames and early error frames automatically.
Models & Pricing
Prices are USD per 1M tokens (input/output) for provider-backed models. Keep this table aligned with the backend allow-list and pricing configuration.
OpenAI
| Model | Input ($/M) | Output ($/M) | Best For |
|---|---|---|---|
openai/gpt-5.1 |
$1.25 | $10.00 | Complex reasoning & analysis |
openai/gpt-4o-mini |
$0.15 | $0.60 | Fast, affordable searches |
Choosing a model
- Fast & economical:
llmlayer-fast, fixed pricing of $0.009 per request - Balanced quality:
llmlayer-web, fixed pricing of $0.007 per request - Premium reasoning:
openai/gpt-5.1
Cost Model
Costs are reported on every answer response through model_cost and llmlayer_cost.
Total Cost = model_cost + llmlayer_cost
For fixed-price LLMLayer models, model_cost may be None and llmlayer_cost carries the charged request amount. provider_key is accepted for backward compatibility but currently ignored; provider usage is not billed to the user's provider account.
Utilities
Web Search — POST /api/v2/web_search
from llmlayer import LLMLayerClient
client = LLMLayerClient()
res = client.search_web(
query="ai agents",
search_type="general", # "general" | "news" | "shopping" | "videos" | "images" | "scholar"
location="us",
recency="day",
domain_filter=["-reddit.com", "reuters.com"],
)
print("results:", len(res.results), "cost:", res.cost)
for item in res.results:
print(item.get("title"), item.get("link") or item.get("url"), item.get("snippet"))
import asyncio
from llmlayer import LLMLayerClient
async def main():
client = LLMLayerClient()
news = await client.search_web_async(
query="latest ai news",
search_type="news",
location="us",
)
for item in news.results:
print(item.get("title"), item.get("source"), item.get("date"))
asyncio.run(main())
Scrape (multi-format) — POST /api/v2/scrape
from llmlayer import LLMLayerClient
client = LLMLayerClient()
# Request multiple outputs in one call
r = client.scrape(
url="https://example.com",
formats=["markdown", "html"], # "markdown" | "html" | "screenshot" | "pdf"
include_images=True,
include_links=True,
)
print("status:", r.statusCode, "cost:", r.cost)
print("md len:", len(r.markdown or ""), "html?", bool(r.html), "pdf?", bool(r.pdf), "shot?", bool(r.screenshot))
Extract (multi-mode) — POST /api/v2/extract
Extract reads one non-PDF web page and can run several modes in a single request. Pricing is summed per selected mode: json/summary/qa $0.005 each, links $0.001, brand $0.002.
| Mode | Required input | Response field | Notes |
|---|---|---|---|
json |
json_schema |
structured_data |
Extracts structured data matching a schema, example object, or plain-text field description. |
summary |
none | summary |
Returns a markdown summary grounded in the page content. |
qa |
query |
answer |
Answers using only the page content. |
links |
none | links |
Returns page links without an LLM call. |
brand |
none | brand |
Returns brand profile data such as logos, colors, socials, and industry. |
All result fields are present on the response; modes you did not request come back as None. The json mode name means "structured extraction"; the result field is structured_data.
product = client.extract(
"https://example.com/product/123",
modes=["json"],
json_schema={
"name": "string",
"price": "number",
"currency": "string",
"in_stock": "boolean",
},
instructions="Keep prices as numbers and use null when a field is missing.",
)
print(product.structured_data)
print(product.summary) # None
page = client.extract(
"https://example.com/docs/refunds",
modes=["summary", "qa"],
query="What is the refund policy?",
response_language="en",
)
print(page.summary)
print(page.answer)
link_result = client.extract(
"https://example.com",
modes=["links"],
)
internal_links = [link for link in link_result.links or [] if link.internal]
print(internal_links)
profile = client.extract(
"https://example.com",
modes=["brand"],
)
print(profile.brand)
combined = client.extract(
"https://example.com/product/123",
modes=["json", "summary", "qa", "links", "brand"],
json_schema="Product name, price, currency, availability, and warranty length.",
query="Does this page mention a warranty?",
)
print(combined.structured_data)
print(combined.summary)
print(combined.answer)
print(combined.links)
print(combined.brand)
print("cost:", combined.cost)
Async variant:
result = await client.extract_async(
"https://example.com/product/123",
modes=["json"],
json_schema={"name": "string", "price": "number"},
)
print(result.structured_data)
Other options:
advanced_proxy: adds $0.004 only when a page scrape runs.response_language: default"auto"; applies to LLM modes.main_content_only: omit it to let the API choose the per-mode default. Links-only requests scrape the full page; LLM modes use main content by default.
PDF URLs are not supported by extract; use get_pdf_content() for PDFs. Very long repeating JSON lists may be capped so the response stays valid, and links mode returns up to 500 links.
Failures before any AI cost (page fetch failure, empty content, brand failure) are fully refunded.
PDF Content — POST /api/v2/get_pdf_content
from llmlayer import LLMLayerClient
client = LLMLayerClient()
pdf = client.get_pdf_content("https://arxiv.org/pdf/1706.03762.pdf")
print("pages:", pdf.pages, "status:", pdf.statusCode, "cost:", pdf.cost)
print("preview:", pdf.text[:200])
YouTube Transcript — POST /api/v2/youtube_transcript
from llmlayer import LLMLayerClient
client = LLMLayerClient()
yt = client.get_youtube_transcript("https://www.youtube.com/watch?v=dQw4w9WgXcQ", language="en")
print(yt.title, yt.author, yt.views, yt.date,yt.description,yt.likes)
print(yt.transcript[:200])
Map — POST /api/v2/map
from llmlayer import LLMLayerClient
client = LLMLayerClient()
mp = client.map("https://docs.llmlayer.ai", limit=100, include_subdomains=False)
print("status:", mp.statusCode, "links:", len(mp.links), "cost:", mp.cost)
print("first:", mp.links[0].url, mp.links[0].title)
for link in mp.links:
print("----------------")
print("URL:", link.url)
print("TITLE:", link.title)
Crawl Stream — POST /api/v2/crawl_stream
Request a crawl of a single seed url. Crawl currently returns markdown page content only; use scrape() when you need html, screenshot, or pdf artifacts.
from llmlayer import LLMLayerClient
client = LLMLayerClient()
for f in client.crawl_stream(
url="https://docs.llmlayer.ai",
max_pages=5,
max_depth=1,
timeout_seconds=30,
main_content_only=False,
advanced_proxy=False,
):
if f.get("type") == "page":
p = f.get("page", {})
if p.get("success"):
markdown = p.get("markdown") or ""
print("ok:", p.get("final_url"), "md_len:", len(markdown))
else:
print("fail:", p.get("final_url"), "err:", p.get("error"))
elif f.get("type") == "usage":
print("billed:", f.get("billed_count"), "cost:", f.get("cost"))
elif f.get("type") == "done":
print("done in", f.get("response_time"), "s")
max_pagesis an upper bound (not a guarantee). You may receive fewer pages if the site is small, the time budget is hit, pages fail, or duplicates are deduped. Only successful pages are billed.
End‑to‑End Pipelines
Map → Crawl → Save Markdown
import pathlib
from llmlayer import LLMLayerClient
client = LLMLayerClient()
# 1) Map
m = client.map("https://docs.llmlayer.ai", limit=200)
seeds = [l.url for l in m.links][:50]
# 2) Crawl (pick the top seed or a section)
out_dir = pathlib.Path("crawl_out"); out_dir.mkdir(exist_ok=True)
for f in client.crawl_stream(url=seeds[0], max_pages=15, max_depth=2, timeout_seconds=60):
if f.get("type") == "page":
p = f.get("page", {})
if p.get("success") and p.get("markdown"):
name = (p.get("title") or p.get("final_url") or "page").split("/")[-1][:64]
safe = "".join(c if c.isalnum() or c in "._-" else "_" for c in name)
(out_dir / f"{safe}.md").write_text(p["markdown"], encoding="utf-8")
Advanced Usage
Configuration options
from llmlayer import LLMLayerClient
client = LLMLayerClient(
api_key="sk-...", # or via LLMLAYER_API_KEY env var
base_url="https://api.llmlayer.dev", # override if self-hosting / staging
timeout=60.0, # seconds (float or httpx.Timeout)
)
Context managers
from llmlayer import LLMLayerClient
with LLMLayerClient() as client:
res = client.answer(query="hi", model="llmlayer-web")
print(res.answer)
# Async context manager
async def main():
async with LLMLayerClient() as client:
res = await client.answer_async(query="hi", model="llmlayer-fast")
Injecting custom httpx clients / proxies / retries
import httpx
from llmlayer import LLMLayerClient
transport = httpx.HTTPTransport(retries=3)
session = httpx.Client(transport=transport, timeout=30)
client = LLMLayerClient(client=session)
The SDK merges required headers (Authorization, User‑Agent) into injected clients.
Per‑request timeouts & headers
Every method accepts optional timeout= and headers= overrides:
resp = client.answer(
query="hi",
model="llmlayer-web",
timeout=15.0,
headers={"X-Debug": "1"},
)
Errors
All exceptions extend LLMLayerError:
| Condition | Exception |
|---|---|
Status 400 / 422, or validation_error (missing/invalid params; early SSE errors like missing_model) |
InvalidRequest |
Status 401 / 403, or authentication_error (missing/invalid LLMLayer key; provider auth issues) |
AuthenticationError |
| Status 429 | RateLimitError |
Upstream LLM provider errors (provider_error) |
ProviderError |
Status 408 / 504 / 5xx, or internal_error / scraping_error / search_error / map_error |
InternalServerError |
| Anything else — e.g. 402 insufficient credits, 423 key disabled | LLMLayerError (base class, carries the server message) |
from llmlayer.exceptions import LLMLayerError, RateLimitError, InvalidRequest
try:
r = client.answer(query="...", model="llmlayer-fast")
except RateLimitError:
... # back off and retry
except InvalidRequest:
raise # fix the request — never retry as-is
except LLMLayerError as e:
print(e) # catches everything, incl. 402 insufficient credits
Server envelope example (the client unwraps detail automatically):
{
"detail": {
"error_type": "validation_error",
"error_code": "missing_query",
"message": "Query parameter cannot be empty"
}
}
Best Practices
- Use streaming for responsive UIs and blocking calls for structured JSON.
- Start with
max_queries=1, then raise to2or3for research tasks that need broader coverage. - Use
domain_filterto focus search and reduce noise; exclude with-domain.com. - Keep JSON schemas narrow and explicit for
answer_type="json"andextract(modes=["json"]). - Use
instructionsfor extract formatting preferences, such as date or number normalization. - Use
extract(modes=["links"])for links on one page andmap()/crawl_stream()for site-level discovery. provider_keyis accepted for backward compatibility but currently ignored; provider usage is not billed to your provider account.
Troubleshooting
answer_type='json'with streaming → not supported. Use blockinganswer().extract()raisesmissing_json_schema→ includejson_schemawhenmodesincludes"json".extract()raisesmissing_query→ includequerywhenmodesincludes"qa".- PDF URLs with
extract()→ useget_pdf_content()instead. - SSL/Connect errors → configure corporate proxies on your injected
httpx.Client. - Event loop errors in notebooks → run with
asyncio.run(...)or use a fresh kernel. - Large base64 payloads (
pdf/screenshot) → write to disk; avoid keeping big blobs in memory.
Changelog
0.3.0
- New:
extract()/extract_async()— multi-mode page extraction (json,summary,qa,links,brand— combinable in one call, priced per mode). Structured results are exposed asresponse.structured_data. - HTTP 422 responses now raise
InvalidRequest(previously the baseLLMLayerError). - Full refunds on extract failures that occur before any AI cost.
License & Support
License: MIT Issues & feature requests: GitHub Issues Private support: support@llmlayer.ai
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llmlayer-0.3.0.tar.gz.
File metadata
- Download URL: llmlayer-0.3.0.tar.gz
- Upload date:
- Size: 17.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5bf465e42675a42799fb0fc0d15c5a338e6cfab50e26887096f27af49526eebd
|
|
| MD5 |
5316372148c399ac439496f898e75795
|
|
| BLAKE2b-256 |
a6f0d685e85ef34b8d19f928fb0a9dccdb4ac7ab00fd82519817bc7e8f4e6ac3
|
File details
Details for the file llmlayer-0.3.0-py3-none-any.whl.
File metadata
- Download URL: llmlayer-0.3.0-py3-none-any.whl
- Upload date:
- Size: 19.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdd4ce4127ad0f4f887bc9e1e5b51af60dc9fff0f878de3ccfe53e64a7df35cf
|
|
| MD5 |
234982dc40ddf06ed946ced56bdce5b4
|
|
| BLAKE2b-256 |
b745540a44186096f464c229506170e612c505049c3f50f707a14cb6f2f48987
|