4a19d70af1
Lint with Ruff / ruff (push) Has been cancelled
MCP Server Tests / live-mcp-tests (push) Has been cancelled
Tests / unit-tests (push) Has been cancelled
Tests / database-integration-tests (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Server Tests / live-server-tests (push) Has been cancelled
Pyright Type Check / pyright (push) Has been cancelled
134 lines
4.9 KiB
Python
134 lines
4.9 KiB
Python
"""
|
|
Copyright 2024, Zep Software, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
"""
|
|
|
|
import typing
|
|
|
|
from openai import AsyncOpenAI
|
|
from openai.types.chat import ChatCompletionMessageParam
|
|
from pydantic import BaseModel
|
|
|
|
from .config import DEFAULT_MAX_TOKENS, LLMConfig
|
|
from .openai_base_client import DEFAULT_REASONING, DEFAULT_VERBOSITY, BaseOpenAIClient
|
|
|
|
|
|
class OpenAIClient(BaseOpenAIClient):
|
|
"""
|
|
OpenAIClient is a client class for interacting with OpenAI's language models.
|
|
|
|
This class extends the BaseOpenAIClient and provides OpenAI-specific implementation
|
|
for creating completions.
|
|
|
|
Attributes:
|
|
client (AsyncOpenAI): The OpenAI client used to interact with the API.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
config: LLMConfig | None = None,
|
|
cache: bool = False,
|
|
client: typing.Any = None,
|
|
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
reasoning: str = DEFAULT_REASONING,
|
|
verbosity: str = DEFAULT_VERBOSITY,
|
|
):
|
|
"""
|
|
Initialize the OpenAIClient with the provided configuration, cache setting, and client.
|
|
|
|
Args:
|
|
config (LLMConfig | None): The configuration for the LLM client, including API key, model, base URL, temperature, and max tokens.
|
|
cache (bool): Whether to use caching for responses. Defaults to False.
|
|
client (Any | None): An optional async client instance to use. If not provided, a new AsyncOpenAI client is created.
|
|
"""
|
|
super().__init__(config, cache, max_tokens, reasoning, verbosity)
|
|
|
|
if config is None:
|
|
config = LLMConfig()
|
|
|
|
if client is None:
|
|
self.client = AsyncOpenAI(api_key=config.api_key, base_url=config.base_url)
|
|
else:
|
|
self.client = client
|
|
|
|
async def _create_structured_completion(
|
|
self,
|
|
model: str,
|
|
messages: list[ChatCompletionMessageParam],
|
|
temperature: float | None,
|
|
max_tokens: int,
|
|
response_model: type[BaseModel],
|
|
reasoning: str | None = None,
|
|
verbosity: str | None = None,
|
|
):
|
|
"""Create a structured completion using OpenAI's beta parse API."""
|
|
# Reasoning models (gpt-5 family) don't support temperature
|
|
is_reasoning_model = (
|
|
model.startswith('gpt-5') or model.startswith('o1') or model.startswith('o3')
|
|
)
|
|
|
|
request_kwargs = {
|
|
'model': model,
|
|
'input': messages, # type: ignore
|
|
'max_output_tokens': max_tokens,
|
|
'text_format': response_model, # type: ignore
|
|
}
|
|
|
|
temperature_value = temperature if not is_reasoning_model else None
|
|
if temperature_value is not None:
|
|
request_kwargs['temperature'] = temperature_value
|
|
|
|
# Only include reasoning and verbosity parameters for reasoning models.
|
|
# 'auto' is resolved to a per-model effort (e.g. 'none' for gpt-5.5).
|
|
# Truthiness guard (matches the Azure client) skips both None and an
|
|
# empty string, so a stray reasoning='' never sends an invalid effort.
|
|
effort = self._resolve_reasoning_effort(model, reasoning)
|
|
if is_reasoning_model and effort:
|
|
request_kwargs['reasoning'] = {'effort': effort} # type: ignore
|
|
|
|
if is_reasoning_model and verbosity is not None:
|
|
request_kwargs['text'] = {'verbosity': verbosity} # type: ignore
|
|
|
|
response = await self.client.responses.parse(**request_kwargs)
|
|
|
|
return response
|
|
|
|
async def _create_completion(
|
|
self,
|
|
model: str,
|
|
messages: list[ChatCompletionMessageParam],
|
|
temperature: float | None,
|
|
max_tokens: int,
|
|
response_model: type[BaseModel] | None = None,
|
|
reasoning: str | None = None,
|
|
verbosity: str | None = None,
|
|
):
|
|
"""Create a regular completion with JSON format."""
|
|
# Reasoning models (gpt-5 family, o1, o3) reject temperature.
|
|
is_reasoning_model = (
|
|
model.startswith('gpt-5') or model.startswith('o1') or model.startswith('o3')
|
|
)
|
|
|
|
request_kwargs: dict[str, typing.Any] = {
|
|
'model': model,
|
|
'messages': messages,
|
|
'max_tokens': max_tokens,
|
|
'response_format': {'type': 'json_object'},
|
|
}
|
|
# Omit temperature entirely for reasoning models — don't even send None.
|
|
if not is_reasoning_model and temperature is not None:
|
|
request_kwargs['temperature'] = temperature
|
|
|
|
return await self.client.chat.completions.create(**request_kwargs)
|