Skip to content

Providers

bruno_llm.providers.ollama.OllamaProvider

Bases: BaseProvider, LLMInterface

Ollama provider for local LLM inference.

Ollama runs models locally without API keys. Requires Ollama to be installed and running on the specified base_url.

Parameters:

Name Type Description Default
base_url str

Ollama API endpoint (default: http://localhost:11434)

'http://localhost:11434'
model str

Model name (default: llama2)

'llama2'
timeout float

Request timeout in seconds (default: 30.0)

30.0
**kwargs Any

Additional configuration parameters

{}

Examples:

>>> provider = OllamaProvider(model="llama2")
>>> response = await provider.generate([
...     Message(role=MessageRole.USER, content="Hello")
... ])
>>> # Streaming
>>> async for chunk in provider.stream([
...     Message(role=MessageRole.USER, content="Tell me a story")
... ]):
...     print(chunk, end="")
See Also
  • https://ollama.ai/
  • bruno-core LLMInterface documentation
Source code in bruno_llm/providers/ollama/provider.py
class OllamaProvider(BaseProvider, LLMInterface):
    """
    Ollama provider for local LLM inference.

    Ollama runs models locally without API keys. Requires Ollama
    to be installed and running on the specified base_url.

    Args:
        base_url: Ollama API endpoint (default: http://localhost:11434)
        model: Model name (default: llama2)
        timeout: Request timeout in seconds (default: 30.0)
        **kwargs: Additional configuration parameters

    Examples:
        >>> provider = OllamaProvider(model="llama2")
        >>> response = await provider.generate([
        ...     Message(role=MessageRole.USER, content="Hello")
        ... ])

        >>> # Streaming
        >>> async for chunk in provider.stream([
        ...     Message(role=MessageRole.USER, content="Tell me a story")
        ... ]):
        ...     print(chunk, end="")

    See Also:
        - https://ollama.ai/
        - bruno-core LLMInterface documentation
    """

    def __init__(
        self,
        base_url: str = "http://localhost:11434",
        model: str = "llama2",
        timeout: float = 30.0,
        **kwargs: Any,
    ):
        """Initialize Ollama provider."""
        # Create config
        config = OllamaConfig(base_url=base_url, model=model, timeout=timeout, **kwargs)

        # Initialize base provider
        super().__init__(
            provider_name="ollama",
            max_retries=3,
            timeout=timeout,
        )

        # Store config
        self._config = config
        self._model = config.model

        # Create HTTP client
        self._client = httpx.AsyncClient(
            base_url=config.base_url,
            timeout=httpx.Timeout(config.timeout),
        )

        # Token counter (simple estimation for local models)
        self._token_counter = SimpleTokenCounter()

    @property
    def model(self) -> str:
        """Get current model name."""
        return self._model

    @property
    def config(self) -> OllamaConfig:
        """Get provider configuration."""
        return self._config

    def _format_messages(self, messages: list[Message]) -> list[dict[str, str]]:
        """Convert bruno-core messages to Ollama format."""
        return [{"role": msg.role.value, "content": msg.content} for msg in messages]

    def _build_request(
        self, messages: list[Message], stream: bool = False, **kwargs: Any
    ) -> dict[str, Any]:
        """Build Ollama API request."""
        request = {
            "model": self._model,
            "messages": self._format_messages(messages),
            "stream": stream,
        }

        # Add optional parameters
        options: dict[str, Any] = {}

        if self._config.temperature is not None:
            options["temperature"] = self._config.temperature
        if self._config.top_p is not None:
            options["top_p"] = self._config.top_p
        if self._config.top_k is not None:
            options["top_k"] = self._config.top_k
        if self._config.num_predict is not None:
            options["num_predict"] = self._config.num_predict
        if self._config.stop is not None:
            request["stop"] = self._config.stop

        # Override with kwargs
        options.update(kwargs)

        if options:
            request["options"] = options

        return request

    async def generate(
        self,
        messages: list[Message],
        temperature: Optional[float] = None,
        max_tokens: Optional[int] = None,
        **kwargs: Any,
    ) -> str:
        """
        Generate a complete response from Ollama.

        Args:
            messages: List of conversation messages
            temperature: Sampling temperature (0.0 to 2.0)
            max_tokens: Maximum number of tokens to generate
            **kwargs: Additional generation parameters

        Returns:
            Generated text response

        Raises:
            ModelNotFoundError: If model doesn't exist
            LLMTimeoutError: If request times out
            LLMError: For other API errors
        """
        try:
            # Build parameters including explicit ones
            generation_params = {}
            if temperature is not None:
                generation_params["temperature"] = temperature
            if max_tokens is not None:
                generation_params["num_predict"] = (
                    max_tokens  # Ollama uses num_predict instead of max_tokens
                )
            generation_params.update(kwargs)

            request = self._build_request(messages, stream=False, **generation_params)

            response = await self._client.post(
                "/api/chat",
                json=request,
            )
            response.raise_for_status()

            data = response.json()

            # Extract response content
            if "message" not in data:
                raise InvalidResponseError("No 'message' in response")

            content = data["message"].get("content", "")
            return content

        except httpx.TimeoutException as e:
            raise LLMTimeoutError(f"Request timed out: {e}") from e
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise ModelNotFoundError(
                    f"Model '{self._model}' not found. "
                    f"Run 'ollama pull {self._model}' to download it."
                ) from e
            raise LLMError(f"HTTP error: {e}") from e
        except httpx.RequestError as e:
            raise LLMError(
                f"Failed to connect to Ollama at {self._config.base_url}. "
                f"Make sure Ollama is running: {e}"
            ) from e
        except (KeyError, json.JSONDecodeError) as e:
            raise InvalidResponseError(f"Invalid response format: {e}") from e

    async def stream(
        self,
        messages: list[Message],
        temperature: Optional[float] = None,
        max_tokens: Optional[int] = None,
        **kwargs: Any,
    ) -> AsyncIterator[str]:
        """
        Stream response tokens from Ollama.

        Args:
            messages: List of conversation messages
            temperature: Sampling temperature (0.0 to 2.0)
            max_tokens: Maximum number of tokens to generate
            **kwargs: Additional generation parameters

        Yields:
            Response text chunks

        Raises:
            StreamError: If streaming fails
        """
        try:
            # Build parameters including explicit ones
            generation_params = {}
            if temperature is not None:
                generation_params["temperature"] = temperature
            if max_tokens is not None:
                generation_params["num_predict"] = (
                    max_tokens  # Ollama uses num_predict instead of max_tokens
                )
            generation_params.update(kwargs)

            request = self._build_request(messages, stream=True, **generation_params)

            async with self._client.stream(
                "POST",
                "/api/chat",
                json=request,
            ) as response:
                response.raise_for_status()

                async for line in response.aiter_lines():
                    if not line.strip():
                        continue

                    try:
                        data = json.loads(line)

                        # Check if streaming is done
                        if data.get("done", False):
                            break

                        # Extract content chunk
                        if "message" in data:
                            content = data["message"].get("content", "")
                            if content:
                                yield content

                    except json.JSONDecodeError:
                        # Skip malformed lines
                        continue

        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                raise ModelNotFoundError(
                    f"Model '{self._model}' not found. "
                    f"Run 'ollama pull {self._model}' to download it."
                ) from e
            raise StreamError(f"Streaming failed: {e}") from e
        except httpx.RequestError as e:
            raise StreamError(f"Failed to connect to Ollama: {e}") from e
        except Exception as e:
            raise StreamError(f"Unexpected streaming error: {e}") from e

    async def list_models(self) -> list[str]:
        """
        List available models in Ollama.

        Returns:
            List of model names

        Raises:
            LLMError: If request fails
        """
        try:
            response = await self._client.get("/api/tags")
            response.raise_for_status()

            data = response.json()
            models = data.get("models", [])

            return [model["name"] for model in models]

        except httpx.RequestError as e:
            raise LLMError(f"Failed to list models: {e}") from e
        except (KeyError, json.JSONDecodeError) as e:
            raise InvalidResponseError(f"Invalid response format: {e}") from e

    async def check_connection(self) -> bool:
        """
        Check if Ollama is accessible.

        Returns:
            True if Ollama is running and accessible
        """
        try:
            response = await self._client.get("/api/tags")
            return response.status_code == 200
        except Exception:
            return False

    def get_token_count(self, text: str) -> int:
        """
        Estimate token count for text.

        Args:
            text: Text to count tokens for

        Returns:
            Estimated token count
        """
        return self._token_counter.count_tokens(text)

    def get_model_info(self) -> dict[str, Any]:
        """
        Get current model information.

        Returns:
            Dictionary with model details
        """
        return {
            "provider": "ollama",
            "model": self._model,
            "base_url": self._config.base_url,
            "temperature": self._config.temperature,
            "max_tokens": self._config.num_predict,
        }

    async def close(self) -> None:
        """Close HTTP client and cleanup resources."""
        await self._client.aclose()

    async def __aenter__(self):
        """Context manager entry."""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        await self.close()

model property

Get current model name.

config property

Get provider configuration.

__init__(base_url='http://localhost:11434', model='llama2', timeout=30.0, **kwargs)

Initialize Ollama provider.

Source code in bruno_llm/providers/ollama/provider.py
def __init__(
    self,
    base_url: str = "http://localhost:11434",
    model: str = "llama2",
    timeout: float = 30.0,
    **kwargs: Any,
):
    """Initialize Ollama provider."""
    # Create config
    config = OllamaConfig(base_url=base_url, model=model, timeout=timeout, **kwargs)

    # Initialize base provider
    super().__init__(
        provider_name="ollama",
        max_retries=3,
        timeout=timeout,
    )

    # Store config
    self._config = config
    self._model = config.model

    # Create HTTP client
    self._client = httpx.AsyncClient(
        base_url=config.base_url,
        timeout=httpx.Timeout(config.timeout),
    )

    # Token counter (simple estimation for local models)
    self._token_counter = SimpleTokenCounter()

generate(messages, temperature=None, max_tokens=None, **kwargs) async

Generate a complete response from Ollama.

Parameters:

Name Type Description Default
messages list[Message]

List of conversation messages

required
temperature Optional[float]

Sampling temperature (0.0 to 2.0)

None
max_tokens Optional[int]

Maximum number of tokens to generate

None
**kwargs Any

Additional generation parameters

{}

Returns:

Type Description
str

Generated text response

Raises:

Type Description
ModelNotFoundError

If model doesn't exist

TimeoutError

If request times out

LLMError

For other API errors

Source code in bruno_llm/providers/ollama/provider.py
async def generate(
    self,
    messages: list[Message],
    temperature: Optional[float] = None,
    max_tokens: Optional[int] = None,
    **kwargs: Any,
) -> str:
    """
    Generate a complete response from Ollama.

    Args:
        messages: List of conversation messages
        temperature: Sampling temperature (0.0 to 2.0)
        max_tokens: Maximum number of tokens to generate
        **kwargs: Additional generation parameters

    Returns:
        Generated text response

    Raises:
        ModelNotFoundError: If model doesn't exist
        LLMTimeoutError: If request times out
        LLMError: For other API errors
    """
    try:
        # Build parameters including explicit ones
        generation_params = {}
        if temperature is not None:
            generation_params["temperature"] = temperature
        if max_tokens is not None:
            generation_params["num_predict"] = (
                max_tokens  # Ollama uses num_predict instead of max_tokens
            )
        generation_params.update(kwargs)

        request = self._build_request(messages, stream=False, **generation_params)

        response = await self._client.post(
            "/api/chat",
            json=request,
        )
        response.raise_for_status()

        data = response.json()

        # Extract response content
        if "message" not in data:
            raise InvalidResponseError("No 'message' in response")

        content = data["message"].get("content", "")
        return content

    except httpx.TimeoutException as e:
        raise LLMTimeoutError(f"Request timed out: {e}") from e
    except httpx.HTTPStatusError as e:
        if e.response.status_code == 404:
            raise ModelNotFoundError(
                f"Model '{self._model}' not found. "
                f"Run 'ollama pull {self._model}' to download it."
            ) from e
        raise LLMError(f"HTTP error: {e}") from e
    except httpx.RequestError as e:
        raise LLMError(
            f"Failed to connect to Ollama at {self._config.base_url}. "
            f"Make sure Ollama is running: {e}"
        ) from e
    except (KeyError, json.JSONDecodeError) as e:
        raise InvalidResponseError(f"Invalid response format: {e}") from e

stream(messages, temperature=None, max_tokens=None, **kwargs) async

Stream response tokens from Ollama.

Parameters:

Name Type Description Default
messages list[Message]

List of conversation messages

required
temperature Optional[float]

Sampling temperature (0.0 to 2.0)

None
max_tokens Optional[int]

Maximum number of tokens to generate

None
**kwargs Any

Additional generation parameters

{}

Yields:

Type Description
AsyncIterator[str]

Response text chunks

Raises:

Type Description
StreamError

If streaming fails

Source code in bruno_llm/providers/ollama/provider.py
async def stream(
    self,
    messages: list[Message],
    temperature: Optional[float] = None,
    max_tokens: Optional[int] = None,
    **kwargs: Any,
) -> AsyncIterator[str]:
    """
    Stream response tokens from Ollama.

    Args:
        messages: List of conversation messages
        temperature: Sampling temperature (0.0 to 2.0)
        max_tokens: Maximum number of tokens to generate
        **kwargs: Additional generation parameters

    Yields:
        Response text chunks

    Raises:
        StreamError: If streaming fails
    """
    try:
        # Build parameters including explicit ones
        generation_params = {}
        if temperature is not None:
            generation_params["temperature"] = temperature
        if max_tokens is not None:
            generation_params["num_predict"] = (
                max_tokens  # Ollama uses num_predict instead of max_tokens
            )
        generation_params.update(kwargs)

        request = self._build_request(messages, stream=True, **generation_params)

        async with self._client.stream(
            "POST",
            "/api/chat",
            json=request,
        ) as response:
            response.raise_for_status()

            async for line in response.aiter_lines():
                if not line.strip():
                    continue

                try:
                    data = json.loads(line)

                    # Check if streaming is done
                    if data.get("done", False):
                        break

                    # Extract content chunk
                    if "message" in data:
                        content = data["message"].get("content", "")
                        if content:
                            yield content

                except json.JSONDecodeError:
                    # Skip malformed lines
                    continue

    except httpx.HTTPStatusError as e:
        if e.response.status_code == 404:
            raise ModelNotFoundError(
                f"Model '{self._model}' not found. "
                f"Run 'ollama pull {self._model}' to download it."
            ) from e
        raise StreamError(f"Streaming failed: {e}") from e
    except httpx.RequestError as e:
        raise StreamError(f"Failed to connect to Ollama: {e}") from e
    except Exception as e:
        raise StreamError(f"Unexpected streaming error: {e}") from e

list_models() async

List available models in Ollama.

Returns:

Type Description
list[str]

List of model names

Raises:

Type Description
LLMError

If request fails

Source code in bruno_llm/providers/ollama/provider.py
async def list_models(self) -> list[str]:
    """
    List available models in Ollama.

    Returns:
        List of model names

    Raises:
        LLMError: If request fails
    """
    try:
        response = await self._client.get("/api/tags")
        response.raise_for_status()

        data = response.json()
        models = data.get("models", [])

        return [model["name"] for model in models]

    except httpx.RequestError as e:
        raise LLMError(f"Failed to list models: {e}") from e
    except (KeyError, json.JSONDecodeError) as e:
        raise InvalidResponseError(f"Invalid response format: {e}") from e

check_connection() async

Check if Ollama is accessible.

Returns:

Type Description
bool

True if Ollama is running and accessible

Source code in bruno_llm/providers/ollama/provider.py
async def check_connection(self) -> bool:
    """
    Check if Ollama is accessible.

    Returns:
        True if Ollama is running and accessible
    """
    try:
        response = await self._client.get("/api/tags")
        return response.status_code == 200
    except Exception:
        return False

get_token_count(text)

Estimate token count for text.

Parameters:

Name Type Description Default
text str

Text to count tokens for

required

Returns:

Type Description
int

Estimated token count

Source code in bruno_llm/providers/ollama/provider.py
def get_token_count(self, text: str) -> int:
    """
    Estimate token count for text.

    Args:
        text: Text to count tokens for

    Returns:
        Estimated token count
    """
    return self._token_counter.count_tokens(text)

get_model_info()

Get current model information.

Returns:

Type Description
dict[str, Any]

Dictionary with model details

Source code in bruno_llm/providers/ollama/provider.py
def get_model_info(self) -> dict[str, Any]:
    """
    Get current model information.

    Returns:
        Dictionary with model details
    """
    return {
        "provider": "ollama",
        "model": self._model,
        "base_url": self._config.base_url,
        "temperature": self._config.temperature,
        "max_tokens": self._config.num_predict,
    }

close() async

Close HTTP client and cleanup resources.

Source code in bruno_llm/providers/ollama/provider.py
async def close(self) -> None:
    """Close HTTP client and cleanup resources."""
    await self._client.aclose()

__aenter__() async

Context manager entry.

Source code in bruno_llm/providers/ollama/provider.py
async def __aenter__(self):
    """Context manager entry."""
    return self

__aexit__(exc_type, exc_val, exc_tb) async

Context manager exit.

Source code in bruno_llm/providers/ollama/provider.py
async def __aexit__(self, exc_type, exc_val, exc_tb):
    """Context manager exit."""
    await self.close()

bruno_llm.providers.openai.OpenAIProvider

Bases: BaseProvider, LLMInterface

OpenAI provider for GPT models.

Provides access to OpenAI's GPT models (GPT-4, GPT-3.5-turbo, etc.) via the official OpenAI API. Requires an API key.

Parameters:

Name Type Description Default
api_key str

OpenAI API key (required)

required
model str

Model name (default: gpt-4)

'gpt-4'
organization Optional[str]

Organization ID (optional)

None
timeout float

Request timeout in seconds (default: 30.0)

30.0
**kwargs Any

Additional configuration parameters

{}

Examples:

>>> provider = OpenAIProvider(api_key="sk-...", model="gpt-4")
>>> response = await provider.generate([
...     Message(role=MessageRole.USER, content="Hello")
... ])
>>> # Streaming
>>> async for chunk in provider.stream([
...     Message(role=MessageRole.USER, content="Tell me a story")
... ]):
...     print(chunk, end="")
>>> # With cost tracking
>>> provider = OpenAIProvider(api_key="sk-...", track_cost=True)
>>> await provider.generate([...])
>>> report = provider.cost_tracker.get_usage_report()
See Also
  • https://platform.openai.com/docs/api-reference
  • bruno-core LLMInterface documentation
Source code in bruno_llm/providers/openai/provider.py
class OpenAIProvider(BaseProvider, LLMInterface):
    """
    OpenAI provider for GPT models.

    Provides access to OpenAI's GPT models (GPT-4, GPT-3.5-turbo, etc.)
    via the official OpenAI API. Requires an API key.

    Args:
        api_key: OpenAI API key (required)
        model: Model name (default: gpt-4)
        organization: Organization ID (optional)
        timeout: Request timeout in seconds (default: 30.0)
        **kwargs: Additional configuration parameters

    Examples:
        >>> provider = OpenAIProvider(api_key="sk-...", model="gpt-4")
        >>> response = await provider.generate([
        ...     Message(role=MessageRole.USER, content="Hello")
        ... ])

        >>> # Streaming
        >>> async for chunk in provider.stream([
        ...     Message(role=MessageRole.USER, content="Tell me a story")
        ... ]):
        ...     print(chunk, end="")

        >>> # With cost tracking
        >>> provider = OpenAIProvider(api_key="sk-...", track_cost=True)
        >>> await provider.generate([...])
        >>> report = provider.cost_tracker.get_usage_report()

    See Also:
        - https://platform.openai.com/docs/api-reference
        - bruno-core LLMInterface documentation
    """

    def __init__(
        self,
        api_key: str,
        model: str = "gpt-4",
        organization: Optional[str] = None,
        timeout: float = 30.0,
        track_cost: bool = True,
        **kwargs: Any,
    ):
        """Initialize OpenAI provider."""
        # Create config
        config = OpenAIConfig(
            api_key=api_key, model=model, organization=organization, timeout=timeout, **kwargs
        )

        # Initialize base provider
        super().__init__(
            provider_name="openai",
            max_retries=config.max_retries,
            timeout=timeout,
        )

        # Store config
        self._config = config
        self._model = config.model

        # Create OpenAI client
        self._client = AsyncOpenAI(
            api_key=config.api_key.get_secret_value(),
            organization=config.organization,
            base_url=config.base_url,
            timeout=config.timeout,
            max_retries=0,  # We handle retries in BaseProvider
        )

        # Token counter (tiktoken for accurate counting)
        self._token_counter = create_token_counter("openai", model=model)

        # Cost tracker
        self._track_cost = track_cost
        if track_cost:
            self.cost_tracker = CostTracker(
                provider_name="openai",
                pricing=PRICING_OPENAI,
            )

    @property
    def model(self) -> str:
        """Get current model name."""
        return self._model

    @property
    def config(self) -> OpenAIConfig:
        """Get provider configuration."""
        return self._config

    def _format_messages(self, messages: list[Message]) -> list[dict[str, str]]:
        """Convert bruno-core messages to OpenAI format."""
        return [{"role": msg.role.value, "content": msg.content} for msg in messages]

    def _build_request_params(self, **kwargs: Any) -> dict[str, Any]:
        """Build OpenAI API request parameters."""
        params: dict[str, Any] = {
            "model": self._model,
            "temperature": self._config.temperature,
            "top_p": self._config.top_p,
        }

        # Add optional parameters
        if self._config.max_tokens is not None:
            params["max_tokens"] = self._config.max_tokens
        if self._config.presence_penalty != 0.0:
            params["presence_penalty"] = self._config.presence_penalty
        if self._config.frequency_penalty != 0.0:
            params["frequency_penalty"] = self._config.frequency_penalty
        if self._config.stop is not None:
            params["stop"] = self._config.stop

        # Override with kwargs
        params.update(kwargs)

        return params

    def _track_usage(
        self,
        messages: list[Message],
        response_text: str,
        completion: Optional[ChatCompletion] = None,
    ) -> None:
        """Track token usage and costs."""
        if not self._track_cost:
            return

        # Get token counts from response or estimate
        if completion and hasattr(completion, "usage") and completion.usage:
            input_tokens = completion.usage.prompt_tokens
            output_tokens = completion.usage.completion_tokens
        else:
            # Estimate if usage not available
            input_text = " ".join(msg.content for msg in messages)
            input_tokens = self._token_counter.count_tokens(input_text)
            output_tokens = self._token_counter.count_tokens(response_text)

        # Track in cost tracker
        self.cost_tracker.track_request(
            model=self._model,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
        )

    async def generate(
        self,
        messages: list[Message],
        temperature: Optional[float] = None,
        max_tokens: Optional[int] = None,
        **kwargs: Any,
    ) -> str:
        """
        Generate a complete response from OpenAI.

        Args:
            messages: List of conversation messages
            temperature: Sampling temperature (0.0 to 2.0)
            max_tokens: Maximum number of tokens to generate
            **kwargs: Additional generation parameters

        Returns:
            Generated text response

        Raises:
            AuthenticationError: If API key is invalid
            RateLimitError: If rate limit exceeded
            ModelNotFoundError: If model doesn't exist
            LLMTimeoutError: If request times out
            LLMError: For other API errors
        """
        try:
            # Build parameters including explicit ones
            generation_params = {}
            if temperature is not None:
                generation_params["temperature"] = temperature
            if max_tokens is not None:
                generation_params["max_tokens"] = max_tokens
            generation_params.update(kwargs)

            params = self._build_request_params(**generation_params)

            completion: ChatCompletion = await self._client.chat.completions.create(
                messages=self._format_messages(messages), **params
            )

            # Extract response content
            if not completion.choices:
                raise InvalidResponseError("No choices in response")

            content = completion.choices[0].message.content or ""

            # Track usage
            self._track_usage(messages, content, completion)

            return content

        except APITimeoutError as e:
            raise LLMTimeoutError(f"Request timed out: {e}") from e
        except APIConnectionError as e:
            raise LLMError(f"Connection error: {e}") from e
        except OpenAIError as e:
            # Parse OpenAI-specific errors by exception type
            if isinstance(e, OpenAIAuthError):
                raise AuthenticationError(f"Invalid API key: {e}") from e
            elif isinstance(e, OpenAIRateLimitError):
                raise RateLimitError(f"Rate limit exceeded: {e}") from e
            elif isinstance(e, OpenAINotFoundError):
                raise ModelNotFoundError(f"Model '{self._model}' not found: {e}") from e
            else:
                raise LLMError(f"OpenAI API error: {e}") from e

    async def stream(
        self,
        messages: list[Message],
        temperature: Optional[float] = None,
        max_tokens: Optional[int] = None,
        **kwargs: Any,
    ) -> AsyncIterator[str]:
        """
        Stream response tokens from OpenAI.

        Args:
            messages: List of conversation messages
            temperature: Sampling temperature (0.0 to 2.0)
            max_tokens: Maximum number of tokens to generate
            **kwargs: Additional generation parameters

        Yields:
            Response text chunks

        Raises:
            StreamError: If streaming fails
        """
        try:
            # Build parameters including explicit ones
            generation_params = {}
            if temperature is not None:
                generation_params["temperature"] = temperature
            if max_tokens is not None:
                generation_params["max_tokens"] = max_tokens
            generation_params.update(kwargs)

            params = self._build_request_params(stream=True, **generation_params)

            stream = await self._client.chat.completions.create(
                messages=self._format_messages(messages), **params
            )

            full_response = []

            async for chunk in stream:
                if not chunk.choices:
                    continue

                delta = chunk.choices[0].delta
                content = delta.content

                if content:
                    full_response.append(content)
                    yield content

            # Track usage after streaming completes
            response_text = "".join(full_response)
            self._track_usage(messages, response_text)

        except APITimeoutError as e:
            raise StreamError(f"Stream timed out: {e}") from e
        except APIConnectionError as e:
            raise StreamError(f"Connection error: {e}") from e
        except OpenAIError as e:
            # Parse OpenAI-specific errors by exception type
            if isinstance(e, OpenAIAuthError):
                raise AuthenticationError(f"Invalid API key: {e}") from e
            elif isinstance(e, OpenAIRateLimitError):
                raise RateLimitError(f"Rate limit exceeded: {e}") from e
            elif isinstance(e, OpenAINotFoundError):
                raise ModelNotFoundError(f"Model '{self._model}' not found: {e}") from e
            else:
                raise StreamError(f"Streaming failed: {e}") from e
        except Exception as e:
            raise StreamError(f"Unexpected streaming error: {e}") from e

    async def list_models(self) -> list[str]:
        """
        List available OpenAI models.

        Returns:
            List of model IDs

        Raises:
            LLMError: If request fails
        """
        try:
            models = await self._client.models.list()
            return [model.id for model in models.data]
        except OpenAIError as e:
            raise LLMError(f"Failed to list models: {e}") from e

    async def check_connection(self) -> bool:
        """
        Check if OpenAI API is accessible.

        Returns:
            True if API is accessible with valid credentials
        """
        try:
            await self._client.models.list()
            return True
        except Exception:
            return False

    def get_token_count(self, text: str) -> int:
        """
        Get accurate token count for text using tiktoken.

        Args:
            text: Text to count tokens for

        Returns:
            Exact token count
        """
        return self._token_counter.count_tokens(text)

    def get_model_info(self) -> dict[str, Any]:
        """
        Get current model information.

        Returns:
            Dictionary with model details
        """
        info = {
            "provider": "openai",
            "model": self._model,
            "base_url": self._config.base_url,
            "temperature": self._config.temperature,
            "max_tokens": self._config.max_tokens,
        }

        # Add cost tracking info if enabled
        if self._track_cost:
            info["cost_tracking"] = {
                "enabled": True,
                "total_cost": self.cost_tracker.get_total_cost(),
                "total_requests": self.cost_tracker.get_request_count(),
            }

        return info

    async def close(self) -> None:
        """Close OpenAI client and cleanup resources."""
        await self._client.close()

    async def __aenter__(self):
        """Context manager entry."""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        await self.close()

model property

Get current model name.

config property

Get provider configuration.

__init__(api_key, model='gpt-4', organization=None, timeout=30.0, track_cost=True, **kwargs)

Initialize OpenAI provider.

Source code in bruno_llm/providers/openai/provider.py
def __init__(
    self,
    api_key: str,
    model: str = "gpt-4",
    organization: Optional[str] = None,
    timeout: float = 30.0,
    track_cost: bool = True,
    **kwargs: Any,
):
    """Initialize OpenAI provider."""
    # Create config
    config = OpenAIConfig(
        api_key=api_key, model=model, organization=organization, timeout=timeout, **kwargs
    )

    # Initialize base provider
    super().__init__(
        provider_name="openai",
        max_retries=config.max_retries,
        timeout=timeout,
    )

    # Store config
    self._config = config
    self._model = config.model

    # Create OpenAI client
    self._client = AsyncOpenAI(
        api_key=config.api_key.get_secret_value(),
        organization=config.organization,
        base_url=config.base_url,
        timeout=config.timeout,
        max_retries=0,  # We handle retries in BaseProvider
    )

    # Token counter (tiktoken for accurate counting)
    self._token_counter = create_token_counter("openai", model=model)

    # Cost tracker
    self._track_cost = track_cost
    if track_cost:
        self.cost_tracker = CostTracker(
            provider_name="openai",
            pricing=PRICING_OPENAI,
        )

generate(messages, temperature=None, max_tokens=None, **kwargs) async

Generate a complete response from OpenAI.

Parameters:

Name Type Description Default
messages list[Message]

List of conversation messages

required
temperature Optional[float]

Sampling temperature (0.0 to 2.0)

None
max_tokens Optional[int]

Maximum number of tokens to generate

None
**kwargs Any

Additional generation parameters

{}

Returns:

Type Description
str

Generated text response

Raises:

Type Description
AuthenticationError

If API key is invalid

RateLimitError

If rate limit exceeded

ModelNotFoundError

If model doesn't exist

TimeoutError

If request times out

LLMError

For other API errors

Source code in bruno_llm/providers/openai/provider.py
async def generate(
    self,
    messages: list[Message],
    temperature: Optional[float] = None,
    max_tokens: Optional[int] = None,
    **kwargs: Any,
) -> str:
    """
    Generate a complete response from OpenAI.

    Args:
        messages: List of conversation messages
        temperature: Sampling temperature (0.0 to 2.0)
        max_tokens: Maximum number of tokens to generate
        **kwargs: Additional generation parameters

    Returns:
        Generated text response

    Raises:
        AuthenticationError: If API key is invalid
        RateLimitError: If rate limit exceeded
        ModelNotFoundError: If model doesn't exist
        LLMTimeoutError: If request times out
        LLMError: For other API errors
    """
    try:
        # Build parameters including explicit ones
        generation_params = {}
        if temperature is not None:
            generation_params["temperature"] = temperature
        if max_tokens is not None:
            generation_params["max_tokens"] = max_tokens
        generation_params.update(kwargs)

        params = self._build_request_params(**generation_params)

        completion: ChatCompletion = await self._client.chat.completions.create(
            messages=self._format_messages(messages), **params
        )

        # Extract response content
        if not completion.choices:
            raise InvalidResponseError("No choices in response")

        content = completion.choices[0].message.content or ""

        # Track usage
        self._track_usage(messages, content, completion)

        return content

    except APITimeoutError as e:
        raise LLMTimeoutError(f"Request timed out: {e}") from e
    except APIConnectionError as e:
        raise LLMError(f"Connection error: {e}") from e
    except OpenAIError as e:
        # Parse OpenAI-specific errors by exception type
        if isinstance(e, OpenAIAuthError):
            raise AuthenticationError(f"Invalid API key: {e}") from e
        elif isinstance(e, OpenAIRateLimitError):
            raise RateLimitError(f"Rate limit exceeded: {e}") from e
        elif isinstance(e, OpenAINotFoundError):
            raise ModelNotFoundError(f"Model '{self._model}' not found: {e}") from e
        else:
            raise LLMError(f"OpenAI API error: {e}") from e

stream(messages, temperature=None, max_tokens=None, **kwargs) async

Stream response tokens from OpenAI.

Parameters:

Name Type Description Default
messages list[Message]

List of conversation messages

required
temperature Optional[float]

Sampling temperature (0.0 to 2.0)

None
max_tokens Optional[int]

Maximum number of tokens to generate

None
**kwargs Any

Additional generation parameters

{}

Yields:

Type Description
AsyncIterator[str]

Response text chunks

Raises:

Type Description
StreamError

If streaming fails

Source code in bruno_llm/providers/openai/provider.py
async def stream(
    self,
    messages: list[Message],
    temperature: Optional[float] = None,
    max_tokens: Optional[int] = None,
    **kwargs: Any,
) -> AsyncIterator[str]:
    """
    Stream response tokens from OpenAI.

    Args:
        messages: List of conversation messages
        temperature: Sampling temperature (0.0 to 2.0)
        max_tokens: Maximum number of tokens to generate
        **kwargs: Additional generation parameters

    Yields:
        Response text chunks

    Raises:
        StreamError: If streaming fails
    """
    try:
        # Build parameters including explicit ones
        generation_params = {}
        if temperature is not None:
            generation_params["temperature"] = temperature
        if max_tokens is not None:
            generation_params["max_tokens"] = max_tokens
        generation_params.update(kwargs)

        params = self._build_request_params(stream=True, **generation_params)

        stream = await self._client.chat.completions.create(
            messages=self._format_messages(messages), **params
        )

        full_response = []

        async for chunk in stream:
            if not chunk.choices:
                continue

            delta = chunk.choices[0].delta
            content = delta.content

            if content:
                full_response.append(content)
                yield content

        # Track usage after streaming completes
        response_text = "".join(full_response)
        self._track_usage(messages, response_text)

    except APITimeoutError as e:
        raise StreamError(f"Stream timed out: {e}") from e
    except APIConnectionError as e:
        raise StreamError(f"Connection error: {e}") from e
    except OpenAIError as e:
        # Parse OpenAI-specific errors by exception type
        if isinstance(e, OpenAIAuthError):
            raise AuthenticationError(f"Invalid API key: {e}") from e
        elif isinstance(e, OpenAIRateLimitError):
            raise RateLimitError(f"Rate limit exceeded: {e}") from e
        elif isinstance(e, OpenAINotFoundError):
            raise ModelNotFoundError(f"Model '{self._model}' not found: {e}") from e
        else:
            raise StreamError(f"Streaming failed: {e}") from e
    except Exception as e:
        raise StreamError(f"Unexpected streaming error: {e}") from e

list_models() async

List available OpenAI models.

Returns:

Type Description
list[str]

List of model IDs

Raises:

Type Description
LLMError

If request fails

Source code in bruno_llm/providers/openai/provider.py
async def list_models(self) -> list[str]:
    """
    List available OpenAI models.

    Returns:
        List of model IDs

    Raises:
        LLMError: If request fails
    """
    try:
        models = await self._client.models.list()
        return [model.id for model in models.data]
    except OpenAIError as e:
        raise LLMError(f"Failed to list models: {e}") from e

check_connection() async

Check if OpenAI API is accessible.

Returns:

Type Description
bool

True if API is accessible with valid credentials

Source code in bruno_llm/providers/openai/provider.py
async def check_connection(self) -> bool:
    """
    Check if OpenAI API is accessible.

    Returns:
        True if API is accessible with valid credentials
    """
    try:
        await self._client.models.list()
        return True
    except Exception:
        return False

get_token_count(text)

Get accurate token count for text using tiktoken.

Parameters:

Name Type Description Default
text str

Text to count tokens for

required

Returns:

Type Description
int

Exact token count

Source code in bruno_llm/providers/openai/provider.py
def get_token_count(self, text: str) -> int:
    """
    Get accurate token count for text using tiktoken.

    Args:
        text: Text to count tokens for

    Returns:
        Exact token count
    """
    return self._token_counter.count_tokens(text)

get_model_info()

Get current model information.

Returns:

Type Description
dict[str, Any]

Dictionary with model details

Source code in bruno_llm/providers/openai/provider.py
def get_model_info(self) -> dict[str, Any]:
    """
    Get current model information.

    Returns:
        Dictionary with model details
    """
    info = {
        "provider": "openai",
        "model": self._model,
        "base_url": self._config.base_url,
        "temperature": self._config.temperature,
        "max_tokens": self._config.max_tokens,
    }

    # Add cost tracking info if enabled
    if self._track_cost:
        info["cost_tracking"] = {
            "enabled": True,
            "total_cost": self.cost_tracker.get_total_cost(),
            "total_requests": self.cost_tracker.get_request_count(),
        }

    return info

close() async

Close OpenAI client and cleanup resources.

Source code in bruno_llm/providers/openai/provider.py
async def close(self) -> None:
    """Close OpenAI client and cleanup resources."""
    await self._client.close()

__aenter__() async

Context manager entry.

Source code in bruno_llm/providers/openai/provider.py
async def __aenter__(self):
    """Context manager entry."""
    return self

__aexit__(exc_type, exc_val, exc_tb) async

Context manager exit.

Source code in bruno_llm/providers/openai/provider.py
async def __aexit__(self, exc_type, exc_val, exc_tb):
    """Context manager exit."""
    await self.close()