Skip to content
Merged
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
OPENAI_API_KEY=sk-proj-***************-***********************-***********************
OPENAI_API_KEY=sk-proj-***************-***********************-***********************
ANTHROPIC_API_KEY=sk-ant-****-***************-***********************-***********************
4 changes: 2 additions & 2 deletions bluebox/agents/docs_digger_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
PendingToolInvocation,
ToolInvocationStatus,
)
from bluebox.data_models.llms.vendors import OpenAIModel
from bluebox.data_models.llms.vendors import LLMModel, OpenAIModel
from bluebox.llms.llm_client import LLMClient
from bluebox.llms.infra.documentation_data_store import (
DocumentationDataStore,
Expand Down Expand Up @@ -185,7 +185,7 @@ def __init__(
persist_chat_callable: Callable[[Chat], Chat] | None = None,
persist_chat_thread_callable: Callable[[ChatThread], ChatThread] | None = None,
stream_chunk_callable: Callable[[str], None] | None = None,
llm_model: OpenAIModel = OpenAIModel.GPT_5_1,
llm_model: LLMModel = OpenAIModel.GPT_5_1,
chat_thread: ChatThread | None = None,
existing_chats: list[Chat] | None = None,
) -> None:
Expand Down
4 changes: 2 additions & 2 deletions bluebox/agents/network_spy_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
PendingToolInvocation,
ToolInvocationStatus,
)
from bluebox.data_models.llms.vendors import OpenAIModel
from bluebox.data_models.llms.vendors import LLMModel, OpenAIModel
from bluebox.llms.llm_client import LLMClient
from bluebox.llms.infra.network_data_store import NetworkDataStore
from bluebox.utils.code_execution_sandbox import execute_python_sandboxed
Expand Down Expand Up @@ -192,7 +192,7 @@ def __init__(
persist_chat_callable: Callable[[Chat], Chat] | None = None,
persist_chat_thread_callable: Callable[[ChatThread], ChatThread] | None = None,
stream_chunk_callable: Callable[[str], None] | None = None,
llm_model: OpenAIModel = OpenAIModel.GPT_5_1,
llm_model: LLMModel = OpenAIModel.GPT_5_1,
chat_thread: ChatThread | None = None,
existing_chats: list[Chat] | None = None,
) -> None:
Expand Down
4 changes: 2 additions & 2 deletions bluebox/agents/trace_hound_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
PendingToolInvocation,
ToolInvocationStatus,
)
from bluebox.data_models.llms.vendors import OpenAIModel
from bluebox.data_models.llms.vendors import LLMModel, OpenAIModel
from bluebox.llms.llm_client import LLMClient
from bluebox.llms.infra.network_data_store import NetworkDataStore
from bluebox.llms.infra.storage_data_store import StorageDataStore
Expand Down Expand Up @@ -184,7 +184,7 @@ def __init__(
persist_chat_callable: Callable[[Chat], Chat] | None = None,
persist_chat_thread_callable: Callable[[ChatThread], ChatThread] | None = None,
stream_chunk_callable: Callable[[str], None] | None = None,
llm_model: OpenAIModel = OpenAIModel.GPT_5_1,
llm_model: LLMModel = OpenAIModel.GPT_5_1,
chat_thread: ChatThread | None = None,
existing_chats: list[Chat] | None = None,
) -> None:
Expand Down
1 change: 1 addition & 0 deletions bluebox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Config():

# API keys
OPENAI_API_KEY: str | None = os.getenv("OPENAI_API_KEY")
ANTHROPIC_API_KEY: str | None = os.getenv("ANTHROPIC_API_KEY")

# Code execution sandbox configuration
# Mode: "docker" (require Docker), "blocklist" (no Docker), "auto" (Docker if available)
Expand Down
72 changes: 44 additions & 28 deletions bluebox/data_models/llms/vendors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,73 @@
"""

from enum import StrEnum
from typing import ClassVar


class LLMVendor(StrEnum):
"""
Represents the vendor of an LLM.
"""
OPENAI = "openai"
#TODO:ANTHROPIC = "anthropic"
ANTHROPIC = "anthropic"


class OpenAIAPIType(StrEnum):
"""
OpenAI API type.
"""
CHAT_COMPLETIONS = "chat_completions"
RESPONSES = "responses"
class VendorModel(StrEnum):
"""Base for model enums. Subclasses must define _vendor."""
_vendor: ClassVar[LLMVendor]

def __init_subclass__(cls, **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
if not hasattr(cls, '_vendor'):
raise TypeError(f"{cls.__name__} must define _vendor class attribute")

@property
def vendor(self) -> LLMVendor:
return self.__class__._vendor


class OpenAIModel(VendorModel):
"""OpenAI models."""
_vendor = LLMVendor.OPENAI

class OpenAIModel(StrEnum):
"""
OpenAI models.
"""
GPT_5 = "gpt-5"
GPT_5_1 = "gpt-5.1"
GPT_5_2 = "gpt-5.2"
GPT_5_MINI = "gpt-5-mini"
GPT_5_NANO = "gpt-5-nano"


# class AnthropicModel(StrEnum):
# """Anthropic models."""
# CLAUDE_OPUS_4_5 = "claude-opus-4-5-20251101"
# CLAUDE_SONNET_4_5 = "claude-sonnet-4-5-20250929"
# CLAUDE_HAIKU_4_5 = "claude-haiku-4-5-20251001"
class AnthropicModel(VendorModel):
"""Anthropic models."""
_vendor = LLMVendor.ANTHROPIC

CLAUDE_OPUS_4_5 = "claude-opus-4-5"
CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
CLAUDE_HAIKU_4_5 = "claude-haiku-4-5"
Comment thread
alex-w-99 marked this conversation as resolved.

# LLMModel is only OpenAI models for now
LLMModel = OpenAIModel

# LLMModel type: union of all vendor models
LLMModel = OpenAIModel | AnthropicModel

# Build model to vendor lookup from OpenAI models only
_model_to_vendor: dict[str, LLMVendor] = {}
_all_models: dict[str, str] = {}

for model in OpenAIModel:
_model_to_vendor[model.value] = LLMVendor.OPENAI
_all_models[model.name] = model.value
def get_model_by_value(model_value: str) -> LLMModel | None:
"""
Get model enum by value string.

Args:
model_value: The model value string (e.g., "gpt-5.1", "claude-opus-4-5").

def get_model_vendor(model: LLMModel) -> LLMVendor:
"""
Returns the vendor of the LLM model.
Returns:
LLMModel if found, None otherwise. Use model.vendor to get the vendor.
"""
return _model_to_vendor[model.value]
for model_cls in VendorModel.__subclasses__():
try:
return model_cls(model_value)
except ValueError:
continue
return None


def get_all_model_values() -> list[str]:
"""Get all available model value strings."""
return [m.value for cls in VendorModel.__subclasses__() for m in cls]
46 changes: 38 additions & 8 deletions bluebox/llms/abstract_llm_vendor_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pydantic import BaseModel

from bluebox.data_models.llms.interaction import LLMChatResponse
from bluebox.data_models.llms.vendors import OpenAIModel
from bluebox.data_models.llms.vendors import LLMModel, LLMVendor


T = TypeVar("T", bound=BaseModel)
Expand All @@ -27,13 +27,27 @@ class AbstractLLMVendorClient(ABC):

# Class attributes ____________________________________________________________________________________________________

_vendor: ClassVar[LLMVendor]
DEFAULT_MAX_TOKENS: ClassVar[int] = 4_096
DEFAULT_TEMPERATURE: ClassVar[float] = 0.7
DEFAULT_STRUCTURED_TEMPERATURE: ClassVar[float] = 0.0

def __init_subclass__(cls, **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
if not hasattr(cls, '_vendor'):
raise TypeError(f"{cls.__name__} must define _vendor class attribute")

@classmethod
def get_llm_vendor_client(cls, model: LLMModel) -> "AbstractLLMVendorClient":
"""Create the appropriate vendor client for the given model."""
for subclass in cls.__subclasses__():
if subclass._vendor == model.vendor:
return subclass(model=model)
raise ValueError(f"No client found for vendor: {model.vendor}")

# Magic methods ________________________________________________________________________________________________________

def __init__(self, model: OpenAIModel) -> None:
def __init__(self, model: LLMModel) -> None:
"""
Initialize the vendor client.

Expand All @@ -43,6 +57,22 @@ def __init__(self, model: OpenAIModel) -> None:
self.model = model
self._tools: list[dict[str, Any]] = []

# Protected methods ____________________________________________________________________________________________________

def _resolve_max_tokens(self, max_tokens: int | None) -> int:
"""Resolve max_tokens, using default if None."""
return max_tokens if max_tokens is not None else self.DEFAULT_MAX_TOKENS

def _resolve_temperature(
self,
temperature: float | None,
structured: bool = False,
) -> float:
"""Resolve temperature, using appropriate default if None."""
if temperature is not None:
return temperature
return self.DEFAULT_STRUCTURED_TEMPERATURE if structured else self.DEFAULT_TEMPERATURE

# Tool management ______________________________________________________________________________________________________

@abstractmethod
Expand Down Expand Up @@ -83,8 +113,8 @@ def call_sync(
temperature: float | None = None,
response_model: type[T] | None = None,
extended_reasoning: bool = False,
stateful: bool = False,
previous_response_id: str | None = None,
tool_choice: str | None = None,
) -> LLMChatResponse | T:
"""
Unified sync call to the LLM.
Expand All @@ -97,8 +127,8 @@ def call_sync(
temperature: Sampling temperature (0.0-1.0).
response_model: Pydantic model class for structured response.
extended_reasoning: Enable extended reasoning (if supported).
stateful: Enable stateful conversation (if supported).
previous_response_id: Previous response ID for chaining (if supported).
tool_choice: Tool selection mode ("auto", "none", "required", or specific tool).

Returns:
LLMChatResponse or parsed Pydantic model if response_model is provided.
Expand All @@ -115,8 +145,8 @@ async def call_async(
temperature: float | None = None,
response_model: type[T] | None = None,
extended_reasoning: bool = False,
stateful: bool = False,
previous_response_id: str | None = None,
tool_choice: str | None = None,
) -> LLMChatResponse | T:
"""
Unified async call to the LLM.
Expand All @@ -129,8 +159,8 @@ async def call_async(
temperature: Sampling temperature (0.0-1.0).
response_model: Pydantic model class for structured response.
extended_reasoning: Enable extended reasoning (if supported).
stateful: Enable stateful conversation (if supported).
previous_response_id: Previous response ID for chaining (if supported).
tool_choice: Tool selection mode ("auto", "none", "required", or specific tool).

Returns:
LLMChatResponse or parsed Pydantic model if response_model is provided.
Expand All @@ -146,8 +176,8 @@ def call_stream_sync(
max_tokens: int | None = None,
temperature: float | None = None,
extended_reasoning: bool = False,
stateful: bool = False,
previous_response_id: str | None = None,
tool_choice: str | None = None,
) -> Generator[str | LLMChatResponse, None, None]:
"""
Unified streaming call to the LLM.
Expand All @@ -161,8 +191,8 @@ def call_stream_sync(
max_tokens: Maximum tokens in the response.
temperature: Sampling temperature (0.0-1.0).
extended_reasoning: Enable extended reasoning (if supported).
stateful: Enable stateful conversation (if supported).
previous_response_id: Previous response ID for chaining (if supported).
tool_choice: Tool selection mode ("auto", "none", "required", or specific tool).

Yields:
str: Text chunks as they arrive.
Expand Down
Loading