Skip to content

Getting started

This guide covers installing Tessaract, configuring the OpenAI provider, sending requests, and keeping conversation history across turns.

Installation

Tessaract requires Python 3.11+. Install the openai extra to use the OpenAI provider:

python -m pip install "tessaract[openai]"

If you use uv, add it to your project with uv add "tessaract[openai]".

To work from a checkout of this repository, run uv sync --extra openai or python -m pip install -e ".[openai]" from the repository root.

The extra installs the OpenAI SDK. Tessaract imports it only when you create an OpenAIProvider, so installing the core package alone is possible, but the OpenAI provider will ask you to install the extra before use.

Configure a provider

Set your OpenAI API key in your shell:

export OPENAI_API_KEY="your-api-key"
import os
from tessaract import OpenAIProvider, Tessaract

client = Tessaract(
    providers={
        "oai": OpenAIProvider(api_key=os.environ["OPENAI_API_KEY"]),
    }
)

Tessaract(providers=...) takes a dict that maps a prefix to a provider. The prefix is your choice. It's what you put before the / in a model name:

client.send(model="oai/gpt-5.6-luna", input="Hi")
#                  ^^^ prefix   ^^^^^^^^^^^^^ model sent to OpenAI

If the prefix isn't registered, send() raises ValueError.

Passing options to the OpenAI client

OpenAIProvider builds an openai.OpenAI client. The common client options are fields on the provider, and they're passed to the SDK when they're set:

OpenAIProvider(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url="https://my-proxy.example.com/v1",
    timeout=60,
    max_retries=3,
    default_headers={"X-Team": "agents"},
    default_query={"api-version": "2026-01-01"},
)

For any other openai.OpenAI(...) argument, such as organization or project, use provider_args. Keys in provider_args override the typed fields:

OpenAIProvider(provider_args={"organization": "org_...", "project": "proj_..."})

API key

If you don't pass api_key, OpenAIProvider reads the OPENAI_API_KEY environment variable. If neither is set, it raises ValueError when you create it, before any request is made:

client = Tessaract(providers={"oai": OpenAIProvider()})   # uses $OPENAI_API_KEY

A common pattern is a .env file with python-dotenv:

from dotenv import load_dotenv
load_dotenv()

Send a request

response = client.send(model="oai/gpt-5.6-luna", input="Write a haiku about tesseracts.")

print(response.output_text)   # concatenated assistant text
print(response.status)        # "completed", "incomplete", ...
print(response.id)            # provider response id

send() takes these parameters:

Parameter Type Default Description
model str required "<prefix>/<model>"
input str \| list required A prompt string, or the conversation history
stream bool False If True, returns an iterator of stream events
reasoning ReasoningOptions \| None None Reasoning effort and summary settings. See Reasoning
tools list[FunctionTool] \| None None Tools the model may call. See Tool calling
request_options dict \| None None Extra native request parameters, passed through to the provider

request_options: native pass-through

Anything in request_options is merged into the OpenAI responses.create(...) call. Use it for parameters Tessaract doesn't model yet:

client.send(
    model="oai/gpt-5.6-luna",
    input="Tell me about the sea.",
    request_options={
        "instructions": "Answer like a pirate.",   # system prompt
        "max_output_tokens": 500,
        "temperature": 0.7,
    },
)

Canonical parameters (model, input, tools, reasoning) always take precedence. If request_options contains one of them, the canonical value wins. The same rule applies to keys inside request_options["extra_body"].

Multi-turn conversations

input can be a list that mixes these types:

Item Meaning
str Shorthand for UserMessage(content=...)
UserMessage A user turn
SystemPrompt System instructions
FunctionToolResult The result of a function call the model asked for
Any item from response.output The model's earlier output, replayed from its native raw form

So to continue a conversation, you append the model's output to your history:

from tessaract import SystemPrompt, UserMessage

history = [SystemPrompt(content="Answer concisely."), UserMessage(content="My name is Ada.")]
response = client.send(model="oai/gpt-5.6-luna", input=history)
history.extend(response.output)

history.append(UserMessage(content="What's my name?"))
response = client.send(model="oai/gpt-5.6-luna", input=history)
print(response.output_text)   # "Your name is Ada."

Every output item keeps the provider's native object in .raw. When you send it back, Tessaract passes the raw object as-is, so the history stays lossless. Reasoning items, message IDs and anything else the provider needs for the next turn are preserved.

Append all of response.output, not only the text. Reasoning models perform best, and function calling only works, when earlier reasoning items and function calls are sent back with their results.

A simple chat loop

With OPENAI_API_KEY set as above, save this example as chat.py and run python chat.py:

from tessaract import OpenAIProvider, Tessaract, UserMessage

client = Tessaract(providers={"oai": OpenAIProvider()})
history = []

print("Chat with Tessaract. Type /exit to quit.")

while True:
    try:
        message = input("You: ").strip()
    except (EOFError, KeyboardInterrupt):
        print()
        break

    if message.lower() == "/exit":
        break
    if not message:
        continue

    history.append(UserMessage(content=message))
    response = client.send(model="oai/gpt-6-luna", input=history)
    print(f"Assistant: {response.output_text}")
    history.extend(response.output)

The loop sends the full conversation on each turn. It stores every item in response.output, so a follow-up question has the context from earlier replies. No tools are involved.

Inspecting output

response.output is a list of typed items. You can branch on item.type:

for item in response.output:
    match item.type:
        case "assistant_message":
            print("assistant:", "".join(part.text for part in item.content))
        case "reasoning":
            print("reasoning summary:", item.text)
        case "function_call":
            print("call:", item.name, item.arguments)
        case "provider_output":
            print("native item Tessaract doesn't model:", item.provider_type)

The full field list is in the API reference.

Next steps