OpenAI¶
The OpenAI provider connects Tulip directly to OpenAI's API (api.openai.com). It's what you reach
for when you want the latest OpenAI model the day it ships without
going through any gateway, translation layer, or middleware.
It's also the fastest way to try the SDK — one env var, one line of code, you're talking to GPT-5 or the o-series reasoning models.
When to pick OpenAI¶
| You want… | This is the right provider |
|---|---|
| GPT-5, GPT-4o, or any latest OpenAI release | ✓ |
The o-series reasoning models (o3, o4-mini) |
✓ |
| To go through Azure / Portkey / LiteLLM / vLLM | ✓ — same class, different base_url |
Getting started¶
1. Set your API key¶
That's the only setup. The SDK reads the env var on import.
2. Pick a model¶
from tulip.agent import Agent
agent = Agent(model="openai:gpt-5.5", system_prompt="You are a SOC triage analyst.")
The string "openai:gpt-5.5" does two things: tells the SDK to use
the OpenAI provider (openai: prefix), and which model id to call
(gpt-5.5). Any model id OpenAI accepts, the SDK accepts.
3. Run it¶
result = agent.run_sync("Is 198.51.100.23 a known C2 endpoint?")
print(result.message)
# → 'Yes — flagged in threat intel as a known C2 endpoint.'
Done. Streaming, tool calls, structured output — all of it works without further configuration.
What you get out of the box¶
Chat completions across the GPT family¶
Every chat-shaped OpenAI model: gpt-4o, gpt-4.1, gpt-5, gpt-5.5,
gpt-image-1. Vision input (image URLs / base64), audio input, and
function calling work the same way you'd use them on the OpenAI SDK
directly — the SDK just normalises the events the model emits.
Reasoning models — the o-series¶
o1, o3, o4-mini route through the same Agent(model="openai:o3")
call. They're slower and more expensive but think before they answer.
The SDK detects these models and adapts the request automatically —
it sends max_completion_tokens instead of max_tokens and drops the
temperature / top_p sampling params the o-series rejects.
Each turn's assistant message is surfaced as a ThinkEvent (carrying
response.message.content) so your UI can show the model's working as
it goes. Note: OpenAI does not return separate reasoning traces over the
API, so the ThinkEvent carries the model's normal per-turn message
text, not hidden chain-of-thought.
Real SSE streaming¶
Token-level streaming over Server-Sent Events. The model emits
deltas, the SDK turns them into ModelChunkEvents, your async for
loop reads them as they arrive — no buffering, no fake chunking.
async for event in agent.run("Summarise the timeline of alert A-42."):
if isinstance(event, ModelChunkEvent) and event.content:
print(event.content, end="", flush=True)
Tool calling — the OpenAI protocol¶
@tool functions are converted to OpenAI's tool-call schema and
the structured tool_calls field in the response is parsed back into
SDK ToolCall objects. Parallel tool calls are supported (the
model can request multiple tools per turn; the SDK runs them
concurrently via the ConcurrentExecutor).
Structured output — Pydantic models in, validated objects out¶
from pydantic import BaseModel
class Finding(BaseModel):
summary: str
confidence: float
agent = Agent(
model="openai:gpt-5.5",
output_schema=Finding,
system_prompt="Reply as JSON matching the schema.",
)
result = agent.run_sync("Is the WIN-7731 beacon malicious?")
print(result.parsed) # Finding(summary='...', confidence=0.83)
Under the hood, the SDK sends an OpenAI response_format with the
schema and a strict-mode flag; if the model produces invalid JSON,
the SDK retries with the validation errors in the prompt
(output_schema_retries=2 by default).
Going through a gateway¶
A base_url override turns OpenAIModel into a client for any
OpenAI-compatible endpoint:
| Gateway | When to use it | base_url |
|---|---|---|
| Azure OpenAI | Enterprise / regulated workloads, Azure billing | https://<resource>.openai.azure.com/openai/deployments/<deployment-id> |
| Portkey | Virtual keys, request routing across providers, retries | https://api.portkey.ai/v1 |
| LiteLLM Proxy | Self-hosted control plane in front of N providers | https://<your-litellm-host>/v1 |
| vLLM | Self-hosted inference for open models with the OpenAI shape | http://localhost:8000/v1 |
| together.ai / fireworks / groq | Hosted open-model inference at OpenAI-shape | their published /v1 |
Build an OpenAIModel with the base_url (and api_key) you want and
hand it to the agent:
from tulip.agent import Agent
from tulip.models.native.openai import OpenAIModel
agent = Agent(
model=OpenAIModel(model="gpt-4o", base_url="https://api.portkey.ai/v1"),
)
The api_key your OPENAI_API_KEY provides is forwarded — for Azure
that's the Azure resource key, for Portkey it's the Portkey virtual
key, etc.
Handling sensitive incident data¶
Provider traffic carries the same SOC data your tools touch — raw alert payloads, host names, attacker-supplied free-text. Two defaults keep it out of the wrong places:
- Tool args/results aren't traced by default.
record_argumentsandrecord_resultsare off, so SIEM payloads and indicators don't leak into your tracing backend unless you opt in (and verify its retention and access controls first). See Observability. - Redact before the reply leaves the box. An
OutputFilterHookstrips PII or blocks topics in the model's output, so nothing attacker-supplied round-trips into a downstream ticket or chat.
Every model call, tool invocation, and containment decision still lands
on the SDK's forensic audit trail — the same typed SSE event stream you
forward to a SIEM verbatim. Routing through a gateway (Azure / Portkey /
LiteLLM) keeps your OPENAI_API_KEY off the egress path when policy
requires it.
Common gotchas¶
| Symptom | Likely cause |
|---|---|
401 Unauthorized |
OPENAI_API_KEY not set, or set to the wrong project's key |
429 Rate limit exceeded |
OpenAI quota; ModelRetryHook (if installed) retries with backoff |
model_not_found |
Model id doesn't exist for your tier — check https://platform.openai.com/docs/models |
Empty tool_calls |
Model decided not to call a tool; check the system prompt |
temperature/top_p rejected on o-series |
The SDK drops these for reasoning models automatically; remove any manual override that re-adds them |
Source¶
OpenAIModel in src/tulip/models/native/openai.py
See also¶
- Models overview — the full provider tree.
- Anthropic — Claude family direct.