Metadata-Version: 2.4
Name: clearllm
Version: 0.1.0
Summary: A lightweight, provider-agnostic LLM client library
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: pydantic>=2.0
Requires-Dist: Pillow>=10.0.0
Requires-Dist: requests>=2.31.0
Requires-Dist: anyio
Provides-Extra: litellm
Requires-Dist: litellm>=1.64.0; extra == "litellm"
Provides-Extra: openai
Requires-Dist: openai>=1.0; extra == "openai"
Provides-Extra: gemini
Requires-Dist: google-genai>=1.0; extra == "gemini"
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.20.0; extra == "anthropic"
Provides-Extra: all
Requires-Dist: litellm>=1.64.0; extra == "all"
Requires-Dist: openai>=1.0; extra == "all"
Requires-Dist: google-genai>=1.0; extra == "all"
Requires-Dist: anthropic>=0.20.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"

# ClearLLM

A lightweight, provider-agnostic LLM client. Think LiteLLM, but with clean Python types — structured returns, frozen pydantic models, no raw dicts.

## Install

```bash
pip install clearllm                  # core only (no LLM SDKs)
pip install "clearllm[litellm]"       # + LiteLLM (OpenAI, Anthropic, Azure, OpenRouter…)
pip install "clearllm[gemini]"        # + native google-genai SDK
pip install "clearllm[openai]"        # + direct openai SDK
pip install "clearllm[all]"           # everything
```

For local development:

```bash
git clone https://github.com/you/clearllm
cd clearllm
pip install -e ".[all,dev]"
```

## Quick start

```python
from clearllm import LLM, Chat, UserTurn

llm = LLM("gpt-4o")
llm = LLM("gemini-2.5-flash")           # native Gemini SDK auto-selected
llm = LLM("claude-3-5-sonnet-latest")   # Anthropic via LiteLLM

# Build a conversation
chat = Chat(system_prompt="You are a helpful assistant.")
chat.add_user_turn(UserTurn().add_text("What is 2 + 2?"))

# Call — always returns AssistantTurn
turn = llm(chat)
print(turn.to_text())

# Or pass a raw message list
turn = llm([{"role": "user", "content": "Hello!"}])

# Async
turn = await llm.acall(chat)
```

## Provider selection

Provider is auto-detected from the model name. You can override it explicitly.

```python
LLM("gpt-4o")                              # → LiteLLMProvider (default catch-all)
LLM("gemini-2.5-flash")                    # → GeminiProvider  (native SDK)
LLM("claude-3-5-sonnet-latest")            # → LiteLLMProvider
LLM("gpt-4o", provider="openai")           # → OpenAIProvider  (direct SDK, no LiteLLM)
LLM("azure/gpt-4o", api_key=..., api_base=..., api_version=...)  # Azure via LiteLLM
LLM("openrouter/google/gemini-2.5-pro")    # OpenRouter via LiteLLM

# Bring your own provider
LLM("my-model", provider=my_provider_obj)
```

Default generation params can be set at construction and overridden per-call:

```python
llm = LLM("gpt-4o", temperature=0.3, max_tokens=1024)
turn = llm(chat, temperature=1.0)   # overrides for this call only
```

## Tool use

```python
from clearllm.tools import tool, simple_tool_result
from clearllm.types import ToolResult
from typing import Annotated
import json

@tool
async def search(query: Annotated[str, "Search query"]) -> ToolResult:
    """Search the web for information."""
    results = await do_search(query)
    return simple_tool_result(json.dumps(results))

# Pass tool specs to the model
turn = llm(chat, tools=[search.to_spec()])
```

## Message types

All messages use the standard `Message` TypedDict shape. Content can be a plain string or a list of typed parts.

```python
from clearllm.types import Message, TextPart, ImagePart, ThinkingPart

msg: Message = {
    "role": "user",
    "content": [
        TextPart(type="text",  text="What's in this image?"),
        ImagePart(type="image", image="https://example.com/photo.jpg"),
    ]
}
```

`ToolCall` and `UnparsedToolCall` are immutable pydantic models validated on construction.

## Multimodal (backbone)

The `UserTurn` / `AssistantTurn` / `Chat` classes in `backbone.py` give a fluent
builder API on top of raw messages:

```python
from clearllm import Chat, UserTurn

chat = Chat(system_prompt="Describe images.")
turn = (UserTurn()
        .add_text("What is in these photos?")
        .add_image(url="https://…/photo1.jpg")
        .add_image_file("local.png"))
chat.add_user_turn(turn)

response = llm(chat)
print(response.to_text())
print(response.prompt_tokens, response.completion_tokens)
```

## Architecture

```
clearllm/
  types.py          # Message, ToolCall, TextPart, ImagePart, ToolSpec, … (no heavy deps)
  tools.py          # @tool decorator, FunctionTool, simple_tool_result
  backbone.py       # Chat, UserTurn, AssistantTurn, ContentBlockList, …
  llm.py            # LLM — public entry-point
  retry.py          # exponential-backoff retry
  providers/
    base.py         # Provider protocol
    litellm.py      # LiteLLMProvider   (optional: litellm)
    openai.py       # OpenAIProvider    (optional: openai)
    gemini.py       # GeminiProvider    (optional: google-genai)
  display/
    jupyter.py      # HTML rendering for Jupyter notebooks
    themes.py       # glassmorphism theme tokens
```

```mermaid
flowchart TD
    tests[tests/] --> backbone
    backbone[backbone.py] --> types[types.py]
    backbone --> display_jupyter[display/jupyter.py]
    display_jupyter --> backbone
    tools[tools.py] --> types
    types --> pydantic[pydantic]
    types --> PIL[Pillow]
    llm[llm.py] --> providers
    providers[providers/] --> backbone
    providers --> retry[retry.py]
    providers -.->|optional| litellm_dep[litellm]
    providers -.->|optional| openai_dep[openai]
    providers -.->|optional| gemini_dep[google-genai]
```

## Design notes

- **Always returns `AssistantTurn`** — no `mm_beta` flag, no raw dict responses.
- **Lazy optional deps** — each provider only imports its SDK when first called.
- **Clean types** — `ToolCall`/`UnparsedToolCall` are frozen pydantic models; `Message` is a TypedDict.
- **No auto-refresh / factory pattern** — connection lifecycle is the SDK's job.
- **Retry is a utility** — `retry_with_exponential_backoff` in `retry.py`, configurable per provider via `max_retries` / `base_delay` constructor args.
