Skip to content

Model providers

A model is a string. The prefix before the colon (openai: or anthropic:) tells the Tulip SDK which provider to use; the rest is the model id that provider expects. get_model() parses the string and returns a ready client.

# tools, system_prompt, and other kwargs are the same across providers
Agent(model="openai:gpt-4o", tools=security_toolset())                # OpenAI direct
Agent(model="anthropic:claude-sonnet-4-6", tools=security_toolset())  # Anthropic direct

To reach a self-hosted model (Ollama, vLLM, or any OpenAI-compatible server), use the openai provider with a custom base_url — point it at your endpoint and no prompt leaves your network:

from tulip.models.native.openai import OpenAIModel

# Ollama / vLLM expose an OpenAI-compatible API — reach it via base_url.
local = OpenAIModel(model="llama-3.3-70b",
                    base_url="http://localhost:11434/v1",  # Ollama default
                    api_key="ollama")                      # any non-empty string
Agent(model=local, tools=security_toolset())               # on-prem, no data egress

The same SOC agent works against any provider — only the model id, the base_url, and the credentials change. Provider choice is a security control: it decides where triage prompts (which carry SIEM rows, host names, and indicators) are sent, who logs them, and which jurisdiction holds them.

The provider tree at a glance

tulip.models
├── openai:                                ── OpenAI direct · OpenAIModel
│   ├─ chat completions       — gpt-* family
│   ├─ reasoning models       — o-series
│   └─ base_url override      — Azure · Portkey · LiteLLM · vLLM ·
│                               Ollama · together.ai · fireworks · groq —
│                               any OpenAI-compatible endpoint, incl.
│                               self-hosted / air-gapped (no data egress)
├── anthropic:                             ── Anthropic direct · AnthropicModel
│   ├─ Claude family          — opus · sonnet · haiku
│   └─ prompt caching         — cache the playbook + GSAR rubric once;
│                               subsequent triage turns pay 1/10th input cost
└── custom:                                ── register_provider("myco", MyModel)
    └─ implement ModelProtocol — complete · stream

Pick the prefix that matches both your auth surface and your data-handling rules. The hosted API endpoints send prompts off-box — fine when the vendor's audit logging and data-residency terms cover your telemetry. For air-gapped SOCs / classified telemetry, point openai at a self-hosted OpenAI-compatible server (Ollama / vLLM) via base_url so alert payloads never leave the network.

Provider Detail page
OpenAI OpenAI →
Anthropic Anthropic →

Custom providers

Implement the ModelProtocol interface — two methods (complete and stream) — and you are a first-class provider. No adapter layer, no inheritance from OpenAIModel. Register the class with the prefix you want; it becomes a valid model id. This is the hook for a self-hosted model behind your own audit proxy — every triage prompt logged to your SIEM before it reaches the LLM.

from tulip.models import register_provider

class AuditedModel:                          # duck-typed ModelProtocol
    async def complete(self, messages, tools=None, **kw): ...  # tee → SIEM, then forward
    async def stream(self, messages, tools=None, **kw): ...

register_provider("soc", lambda model_id, **kw: AuditedModel(model_id, **kw))

agent = Agent(model="soc:internal-triage-llm", tools=security_toolset())

Source: register_provider in models/registry.py:21.

Credential pooling & rotation

Incident response can't stall on a rate-limited credential. For always-on triage, wrap the model in a CredentialPoolModel that rotates through a pool of API keys for the same provider:

import os
from pydantic import SecretStr
from tulip.models.credentials import Credential, CredentialPool
from tulip.models.pooled import CredentialPoolModel
from tulip.models.native.anthropic import AnthropicModel

pool = CredentialPool([
    Credential(label="primary", api_key=SecretStr(os.environ["KEY_A"])),
    Credential(label="backup",  api_key=SecretStr(os.environ["KEY_B"])),
])

def _build(cred: Credential) -> AnthropicModel:
    return AnthropicModel(model="claude-sonnet-4-6", api_key=cred.api_key)

agent = Agent(
    model=CredentialPoolModel(pool=pool, build_model=_build),
    tools=security_toolset(),
)

Each call picks the active credential; when the error classifier says rotation should help (rate-limit / auth errors), the credential is marked bad with a cooldown and the next one is tried. It rotates credentials, not providers — to fail over across providers, compose the failover classifier (tulip.models.failover) yourself. Source: CredentialPoolModel in models/pooled.py.

Notebook

notebook_56_model_providers.py runs the same SOC triage agent against OpenAI and Anthropic by swapping one string.

Source

Area Path
Provider registry models/registry.py
OpenAIModel models/native/openai.py
AnthropicModel models/native/anthropic.py
CredentialPoolModel models/pooled.py