Skip to main content

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/v2 for all endpoints.
  • Answer response field renamed: answer (was llm_response).
  • Answer streaming content frames use { "type": "answer", "content": "..." } (was type: "llm").
  • Scrape accepts formats: List["markdown"|"html"|"screenshot"|"pdf"] and returns markdown/html/pdf/screenshot, title, metadata, and statusCode.
  • Extract returns structured extraction results in structured_data.
  • Map response now uses statusCode (camelCase).
  • Crawl request takes a single url (not seeds) 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 variable LLMLAYER_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' with json_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_pages is 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 to 2 or 3 for research tasks that need broader coverage.
  • Use domain_filter to focus search and reduce noise; exclude with -domain.com.
  • Keep JSON schemas narrow and explicit for answer_type="json" and extract(modes=["json"]).
  • Use instructions for extract formatting preferences, such as date or number normalization.
  • Use extract(modes=["links"]) for links on one page and map()/crawl_stream() for site-level discovery.
  • provider_key is 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 blocking answer().
  • extract() raises missing_json_schema → include json_schema when modes includes "json".
  • extract() raises missing_query → include query when modes includes "qa".
  • PDF URLs with extract() → use get_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 as response.structured_data.
  • HTTP 422 responses now raise InvalidRequest (previously the base LLMLayerError).
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llmlayer-0.3.0.tar.gz (17.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llmlayer-0.3.0-py3-none-any.whl (19.9 kB view details)

Uploaded Python 3

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

Hashes for llmlayer-0.3.0.tar.gz
Algorithm Hash digest
SHA256 5bf465e42675a42799fb0fc0d15c5a338e6cfab50e26887096f27af49526eebd
MD5 5316372148c399ac439496f898e75795
BLAKE2b-256 a6f0d685e85ef34b8d19f928fb0a9dccdb4ac7ab00fd82519817bc7e8f4e6ac3

See more details on using hashes here.

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

Hashes for llmlayer-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cdd4ce4127ad0f4f887bc9e1e5b51af60dc9fff0f878de3ccfe53e64a7df35cf
MD5 234982dc40ddf06ed946ced56bdce5b4
BLAKE2b-256 b745540a44186096f464c229506170e612c505049c3f50f707a14cb6f2f48987

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page