From 8c0ae6e1cf60ec2eabee4b282562ae9d4c1d68ca Mon Sep 17 00:00:00 2001 From: Lorenzo Drudi Date: Tue, 28 May 2024 16:01:48 +0200 Subject: [PATCH] feat: thread-safe logger. --- README.rst | 5 ++ changes_proposal.md | 6 +- openai_cost_logger/__init__.py | 1 + .../openai_cost_logger_singleton.py | 66 +++++++++++++++++++ setup.py | 2 +- 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 openai_cost_logger/openai_cost_logger_singleton.py diff --git a/README.rst b/README.rst index 08d7307..61462dd 100644 --- a/README.rst +++ b/README.rst @@ -33,6 +33,11 @@ Models supported: ------------------- * The response generation is totally up to the user. The library support every model which response contains the fields **usage.prompt_tokens** and **usage.total_tokens** (e.g. chat completions, embeddings, etc.). +Multithreading: +--------------- +* Be aware that the classic cost logger is not thread-safe. +* If you want to use it in a multithreading environment, you should use the **thread-safe** version of the logger: **OpenAICostLogger_Singleton**. The interface is the same as the classic logger. This will prevent multiple threads from writing to the same file at the same time. + Note: ----- * Every cost is specified per **million tokens**. diff --git a/changes_proposal.md b/changes_proposal.md index 80dbfd4..e0901e6 100644 --- a/changes_proposal.md +++ b/changes_proposal.md @@ -4,7 +4,7 @@ Change: - we can just infer it from `response.model` - removes possible problems with choosing the right enum or forgetting to change it while changing the model for experiment -2. ⌛ allow for experiment/subexperiment stats +2. ✅ allow for experiment/subexperiment stats 3. ✅ cost tracker handles completion creation - Merged @@ -45,6 +45,4 @@ Change: - change strftime format to `strftime("%Y-%m-%d_%H:%M:%S")`, makes it more readable - we could possibly infer the datetime and do plots with datetime instead of str -6. ⌛ web ui for stats viz - -7. WIP +6. ⌛ web ui for stats viz \ No newline at end of file diff --git a/openai_cost_logger/__init__.py b/openai_cost_logger/__init__.py index 4a243e6..02e39aa 100644 --- a/openai_cost_logger/__init__.py +++ b/openai_cost_logger/__init__.py @@ -2,4 +2,5 @@ from .constants import MODELS_COST, DEFAULT_LOG_PATH from .openai_cost_logger_viz import OpenAICostLoggerViz from .openai_cost_logger_utils import OpenAICostLoggerUtils +from .openai_cost_logger_singleton import OpenAICostLogger_Singleton print('imported openai-cost-logger') \ No newline at end of file diff --git a/openai_cost_logger/openai_cost_logger_singleton.py b/openai_cost_logger/openai_cost_logger_singleton.py new file mode 100644 index 0000000..c3f02e1 --- /dev/null +++ b/openai_cost_logger/openai_cost_logger_singleton.py @@ -0,0 +1,66 @@ +import threading + +from openai_cost_logger.constants import DEFAULT_LOG_PATH +from openai_cost_logger.openai_cost_logger import OpenAICostLogger + + +""" Metaclass for creating singletons.""" +class Singleton(type): + _instance = None + _lock = threading.Lock() + + + def __call__(cls, *args, **kwargs): + if not cls._instance: + with cls._lock: + if not cls._instance: + # We have not every built an instance before. Build one now. + instance = super().__call__(*args, **kwargs) + cls._instance = instance + else: + instance = cls._instance + return instance + + +"""Singleton class for the OpenAICostLogger class.""" +class OpenAICostLogger_Singleton(metaclass=Singleton): + def __init__(self, experiment_name: str, cost_upperbound: float, log_folder: str = DEFAULT_LOG_PATH): + """Initializes the OpenAICostLogger_Singleton class. + + Args: + experiment_name (str): the name of the experiment. + log_folder (str): the folder where the logs will be stored. + cost_upperbound (float): the upperbound of the cost. + """ + self.__cost_logger = OpenAICostLogger( + experiment_name=experiment_name, + cost_upperbound=cost_upperbound, + log_folder=log_folder + ) + self.lock = threading.Lock() # Lock to ensure thread safety when updating the cost logger. + + + def update_cost(self, response: dict, input_cost: float, output_cost: float = 0): + """Updates the cost logger with the response, input cost, and output cost. + + Args: + response (dict): the response from the model. + input_cost (float): the cost of the input per million tokens. + output_cost (float, optional): the cost of the output per million tokens.. Defaults to 0. + """ + with self.lock: + self.__cost_logger.update_cost( + response=response, + input_cost=input_cost, + output_cost=output_cost + ) + + + def get_current_cost(self) -> float: + """Returns the current cost of the experiment. + + Returns: + float: the current cost of the experiment. + """ + with self.lock: + return self.__cost_logger.get_current_cost() \ No newline at end of file diff --git a/setup.py b/setup.py index 704f5c2..6677509 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ with open('README.rst') as f: long_description = f.read() -version_number = '0.4.1' +version_number = '0.5.0' setup( name='openai-cost-logger',