Architecture¶
Tessaract has three layers:
your agent code
│ canonical types (UserMessage, FunctionTool, ReasoningOptions, …)
▼
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Tessaract │ ──▶ │ Adapter │ ──▶ │ Provider │ ──▶ vendor SDK / API
│ (client.py) │ │ (translation) │ │ (SDK client) │
└──────────────┘ └──────────────────┘ └──────────────────┘
▲ │
│ Response / StreamEventUnion (canonical, with .raw)
└───────────────────────┘
Providers¶
A provider (providers/) holds credentials and the vendor SDK client. OpenAIProvider builds openai.OpenAI(...) from its typed fields plus provider_args, falls back to OPENAI_API_KEY, and imports the SDK lazily, so Tessaract depends only on pydantic unless you install an extra.
Adapters¶
An adapter (adapters/) translates in both directions between canonical types and one vendor's API. The Adapter base class defines the translation hooks:
| Method | Direction | Purpose |
|---|---|---|
map_input_message(UserMessage) |
canonical → native | User turns |
map_tool_result(FunctionToolResult) |
canonical → native | Tool outputs |
map_function_schema(list[FunctionTool]) |
canonical → native | Tool definitions |
map_reasoning_params(ReasoningOptions) |
canonical → native | Reasoning configuration |
map_reasoning(...) |
canonical → native | Reserved |
OpenAIAdapter also implements:
_normalize_output_item/_normalize_output, which turn native output items into canonical ones_normalize_stream_event, which turns native stream events into canonical ones_build_request_kwargs, which merges the canonical parameters withprovider_options(canonical wins)generate_sync(request) -> Responsegenerate_stream(request) -> Iterator[StreamEventUnion]
The request lifecycle¶
Tessaract.send()splits"oai/gpt-…"into the prefix and the model name, and looks up the provider and adapter._build_request_modelconverts eachinputitem:str→UserMessage→adapter.map_input_messageInputType→item.raw(adapter), which calls the matchingmap_*hook- output items (
AssistantMessage,OutputItem) → their stored.rawnative object - A canonical
Requestgoes toadapter.generate_syncoradapter.generate_stream. - The adapter calls the SDK, then normalizes the result into a
Responseor a stream of events.
Design principles¶
- Canonical first, native always available. Every canonical object keeps the native payload (
raw,raw_response,raw_event), so nothing is lost and you can always go down to the SDK. - Lossless history. Output items replay from
.raw, so provider-specific state such as reasoning IDs and encrypted content survives across turns. - Unknowns pass through. Output items that Tessaract can't model become
ProviderOutputItem, and stream events it can't model becomeCustomProviderEvent. Neither is dropped. - Canonical parameters win.
request_optionsandprovider_optionscan add native parameters, but they can't override the canonical ones.
Adding a provider¶
- Subclass
Providerand create the SDK client in__post_init__. Import the SDK lazily and raise a helpfulImportError. - Subclass
Adapterand implement themap_*hooks, output normalization, stream normalization,generate_syncandgenerate_stream. - Register the pair in
Tessaract.register_adapter()and add the provider type to thesend()dispatch. - Add an optional dependency group to
pyproject.toml. Ananthropicextra is already declared. - Export the provider from
tessaract/__init__.py.