Tessaract
A provider-agnostic SDK for building AI agents natively.
Tessaract exists to make it straightforward to build agents that work across multiple providers. It is an unopinionated portal to multi-model intelligence giving you model diversity and full control over how you compose models in your applications.
Today, Tessaract supports OpenAI's Responses API and Anthropic's Messages API, including streaming, reasoning, and tool calling. More provider adapters are planned.
More concretely, Tessaract exposes a canonical model of the LLM ecosystem: requests, responses, content, streaming events, tools, usage, errors and reasoning. You write an agent loop once against Tessaract's types, and a provider adapter translates to and from each vendor's native API.
Status: OpenAI and Anthropic are supported today. The same agent code runs on both; switch providers by changing the model prefix.
Contents
- Features
- Installation
- Quickstart
- Build an agent with reasoning and tool calling
- Streaming
- Documentation
- Project layout
- Status and roadmap
Features
- Provider prefixes. Register a provider under a prefix and address models as
"<prefix>/<model>", e.g."oai/gpt-5.6-luna". - Canonical tools. Define function tools once with
FunctionTool/InputSchema/Property, including nested objects, arrays, enums and nullable fields. The adapter emits the provider's native schema. - Canonical reasoning. Control effort and summaries with
ReasoningOptions; read reasoning back asReasoningOutputItems. - Typed output. Responses contain
AssistantMessage,ReasoningOutputItem,FunctionCallOutputItemandProviderOutputItemobjects. Function-call arguments are already parsed into adict. - Canonical streaming events. Text, reasoning and tool-argument deltas are normalized into typed events (
text.delta,reasoning_summary.delta,response.completed, …). - Lossless multi-turn history. Every output item keeps its provider-native
rawpayload, so you can append a response's output straight back into the conversation history. - Escape hatches everywhere.
request_optionspasses any native request parameter through,FunctionTool.provider_optionsadds native tool fields, andraw_response/raw_eventexpose the original SDK objects.
Installation
Tessaract requires Python 3.11+. Install the extra for each provider you use: openai, anthropic, or both.
With pip:
python -m pip install "tessaract[openai]" # OpenAI
python -m pip install "tessaract[anthropic]" # Anthropic
python -m pip install "tessaract[openai,anthropic]" # both
With uv:
uv add "tessaract[openai,anthropic]"
Quickstart
Set the API key for each provider you use:
export OPENAI_API_KEY="your-openai-key"
export ANTHROPIC_API_KEY="your-anthropic-key"
Then send your first request:
from tessaract import AnthropicProvider, OpenAIProvider, Tessaract
client = Tessaract(providers={"oai": OpenAIProvider(), "ant": AnthropicProvider()})
response = client.send(model="oai/gpt-6-luna", input="Say hello in five words.")
print(response.output_text)
response = client.send(model="ant/claude-opus-5-5", input="Say hello in five words.")
print(response.output_text)
OpenAIProvider reads OPENAI_API_KEY and AnthropicProvider reads ANTHROPIC_API_KEY from the environment. Register only the providers you need. The oai and ant prefixes are your choice; Tessaract sends the model name after / to the provider registered under that prefix. For multi-turn conversations, try the simple chat loop.
Build an agent with reasoning and tool calling
An agent is a loop:
- Send the conversation history, plus the tools the model may use.
- Append the model's output (reasoning, messages and function calls) to the history.
- If the model asked for function calls, run them and append a
FunctionToolResultfor each one. - Repeat until the model replies without calling a tool.
import json
import os
from datetime import datetime, timezone
from tessaract import (
FunctionTool,
FunctionToolResult,
InputSchema,
OpenAIProvider,
Property,
ReasoningOptions,
Tessaract,
UserMessage,
)
# 1. Plain Python functions the agent can call
def get_time() -> str:
return datetime.now(timezone.utc).isoformat()
def get_weather(city: str) -> str:
temps = {"paris": "19C", "amsterdam": "20C"}
return json.dumps({"temperature": temps.get(city.lower(), "unknown")})
FUNCTIONS = {"get_time": get_time, "get_weather": get_weather}
# 2. Describe them to the model
tools = [
FunctionTool(
name="get_time",
description="Get the current UTC time as an ISO-8601 string.",
strict=False,
),
FunctionTool(
name="get_weather",
description="Get the current temperature for a city.",
input_schema=InputSchema(
properties={
"city": Property(type="string", description="City name, e.g. Paris."),
},
required=["city"],
additionalProperties=False,
),
),
]
client = Tessaract(
providers={"oai": OpenAIProvider(api_key=os.environ["OPENAI_API_KEY"])}
)
# 3. The agent loop
def run_agent(history: list, user_text: str) -> str:
history.append(UserMessage(content=user_text))
while True:
response = client.send(
model="oai/gpt-5.6-luna",
input=history,
tools=tools,
reasoning=ReasoningOptions(effort="medium", summary="auto"),
instructions="You are a concise, helpful assistant.",
)
# Keep reasoning, messages and calls in the history for the next turn
history.extend(response.output)
for item in response.output:
if item.type == "reasoning" and item.text:
print(f"[thinking] {item.text}")
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
return response.output_text
for call in calls:
result = FUNCTIONS[call.name](**call.arguments) # arguments is already a dict
history.append(FunctionToolResult(call_id=call.call_id, result=result))
history: list = []
print(run_agent(history, "What's the weather in Paris, and what time is it?"))
To run the same agent on Anthropic, register AnthropicProvider and change model to "ant/claude-opus-5-5". Nothing else in the loop changes. Anthropic requires max_tokens; Tessaract sends 16,000 when you don't set it, but passing max_tokens= explicitly is recommended.
The step-by-step walkthrough is in the building an agent guide, with runnable versions in examples/.
Streaming
Pass stream=True to get an iterator of canonical events. The final event is response.completed, which carries the same Response object that a non-streaming call returns, so the agent loop doesn't change:
completed = None
for event in client.send(
model="oai/gpt-5.6-luna",
input=history,
tools=tools,
stream=True,
reasoning=ReasoningOptions(effort="medium", summary="auto"),
):
if event.type == "reasoning.started":
print("\n[thinking] ", end="")
elif event.type == "reasoning_summary.delta":
print(event.delta, end="", flush=True)
elif event.type == "text.delta":
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
completed = event.response
history.extend(completed.output)
See the streaming guide for the full event list.
Documentation
The documentation site is tessaractai.com.
| Guide | What it covers |
|---|---|
| Getting started | Installing, configuring providers, sending your first request, and building multi-turn history |
| Building an agent | A step-by-step tutorial for an agent with reasoning and tool calling, both synchronous and streaming |
| Tool calling | FunctionTool, InputSchema, Property, strict mode, tool results and provider_options |
| Reasoning | ReasoningOptions, effort levels, summaries, and reading and preserving reasoning items |
| Streaming | Every canonical stream event, and how to build a streaming agent loop |
| API reference | Every public class, field and method |
| Architecture | Canonical types, adapters and providers, and how to add a new provider |
Project layout
src/tessaract/
├── client.py # Tessaract client: routes requests to adapters
├── providers/
│ ├── provider.py # Provider base dataclass
│ ├── openai_provider.py # OpenAIProvider (wraps openai.OpenAI)
│ └── anthropic_provider.py # AnthropicProvider (wraps anthropic.Anthropic)
├── adapters/
│ ├── adapter.py # Adapter base class + protocols
│ ├── openai/openai_adapter.py # Canonical <-> OpenAI Responses API translation
│ └── anthropic/anthropic_adapter.py # Canonical <-> Anthropic Messages API translation
├── tools/function.py # FunctionTool, InputSchema, Property
└── types/
├── input_types.py # UserMessage, FunctionToolResult
├── output_types.py # AssistantMessage, ReasoningOutputItem, FunctionCallOutputItem, ...
├── request.py # Request, ReasoningOptions
├── response.py # Response, ResponseStatus, ResponseError
└── streaming/event_types.py # Canonical stream events
Status and roadmap
Working today (OpenAI):
- Synchronous and streaming requests via the Responses API
- Multi-turn conversations
- Function tools with parallel calls
- Reasoning effort and summaries
- Pass-through request options
- Prompt caching via
CacheOptions(mode and key) andcache=Truebreakpoints - Token usage on
Response, including OpenAI usage details
Working today (Anthropic):
- Synchronous and streaming requests via the Messages API
- Multi-turn conversations, with automatic grouping into user/assistant turns
- Function tools with parallel calls, and native tool-result error flags
- Adaptive, budgeted and between-tools thinking, effort, and thinking display (including beta
updates) - System prompts via
instructions=orSystemPrompt, and prompt caching viaCacheOptions,cache=Truebreakpoints orcache_controlpass-through - Token usage on
Response, normalized soinput_tokensincludes cached tokens
Planned:
- Google GenAI provider and adapter
- Finish details on
Response - TestProvider for CI and testing without making live API calls
- built-in utils: agent loop helper, tool schema autowriter
- Token counting
Metadata
Release files for tessaract 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tessaract-0.2.1.tar.gz | 22.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tessaract-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.0 kB
Release files / tessaract-0.2.1.tar.gz
| Download URL | tessaract-0.2.1.tar.gz |
|---|---|
| Size | 22.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ab6323ba8a0f9ef5e9f3a2afa8b59faec7e410de0271934c317e27b462c8b7b1
|
|
BLAKE2b-256 checksum How to use checksums |
85f0d44d09831a00c986418518fc082b5684c5f8f47fca52140353413b59fa4a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logRelease files / tessaract-0.2.1-py3-none-any.whl
| Download URL | tessaract-0.2.1-py3-none-any.whl |
|---|---|
| Size | 31.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
be8a55be7ff7157cfc05e63cbdafe17444d38901496968d2aa97d873171a3a1e
|
|
BLAKE2b-256 checksum How to use checksums |
8596cc9b21bbe7f8fb10dd42ddbcbc394c98ba664963d7c8332de1bd161fa63e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency log