From 7ad58d6c7d078391c3e7bec0021230e95c17fa49 Mon Sep 17 00:00:00 2001 From: Lorenzo Drudi Date: Mon, 8 Apr 2024 11:47:55 +0200 Subject: [PATCH] feat/ci: Add deployment workflow. --- .github/workflows/deploy.yml | 35 ++++ .gitignore | 12 +- README.md | 19 -- README.rst | 35 ++++ demo.ipynb | 184 +++++++++++++----- openai_cost_logger/__init__.py | 1 + {src => openai_cost_logger}/constants.py | 12 +- .../openai_cost_logger.py | 30 ++- .../openai_cost_logger_utils.py | 4 +- .../openai_cost_logger_viz.py | 14 +- requirements.txt | 3 +- setup.py | 26 +++ 12 files changed, 277 insertions(+), 98 deletions(-) create mode 100644 .github/workflows/deploy.yml delete mode 100644 README.md create mode 100644 README.rst create mode 100644 openai_cost_logger/__init__.py rename {src => openai_cost_logger}/constants.py (72%) rename src/openai_cost_tracker.py => openai_cost_logger/openai_cost_logger.py (92%) rename src/openai_cost_tracker_utils.py => openai_cost_logger/openai_cost_logger_utils.py (80%) rename src/openai_cost_tracker_viz.py => openai_cost_logger/openai_cost_logger_viz.py (81%) create mode 100644 setup.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..03fac07 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,35 @@ +on: + push: + branches: + - master + +permissions: + contents: write + pull-requests: read + +jobs: + release-on-push: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - id: release + uses: rymndhng/release-on-push-action@master + with: + bump_version_scheme: minor + + - uses: actions/checkout@v2 + with: + ref: ${{ steps.release.outputs.tag_name }} + + - name: deploy + env: + TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + VERSION_NUMBER: ${{ steps.release.outputs.tag_name }} + run: | + + echo "Deploying ${{ steps.release.outputs.tag_name }} to PyPI" + pip install twine + python setup.py sdist + twine upload dist/* -u $TWINE_USERNAME -p $TWINE_PASSWORD \ No newline at end of file diff --git a/.gitignore b/.gitignore index d2fcb76..e8f9ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,15 @@ azure_openai_key.txt openai_api_key.txt openai_organization.txt +# PyPi API token +pypi_api_token.txt + # Cost log files -cost-logs/ \ No newline at end of file +cost-logs/ + +# PyTest +.pytest_cache +*.egg-info/ + +build/ +dist/ \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 4c7ecea..0000000 --- a/README.md +++ /dev/null @@ -1,19 +0,0 @@ -![ChatGPT](https://img.shields.io/badge/chatGPT-74aa9c?style=for-the-badge&logo=openai&logoColor=white) -![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54) - -# track-openai-cost - -Simple cost tracker for OpenAI requests.\ -Do every request tracking also the live cost and raising an exception when it exceeds the bound you have set.\ -Visualize the costs filtering by model and day. - -
-  See the demo file for a usage example.
-
- -**Clients supported:** -- OpenAI -- Azure OpenAI - -**Endpoint supported:** -- Chat completion diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8d65c80 --- /dev/null +++ b/README.rst @@ -0,0 +1,35 @@ +.. image:: https://img.shields.io/badge/chatGPT-74aa9c.svg?logo=openai +.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg + +================== +OpenAI Cost Logger +================== + +Simple cost logger for OpenAI requests. +Track the cost of every request you make to OpenAI and visualize them in a user-friendly way. + +How to install: +--------------- +* .. code-block:: python + + pip install openai-cost-logger + +* .. code-block:: python + + from openai_cost_logger.constants import DEFAULT_LOG_PATH, Models, MODELS_COST + from openai_cost_logger.openai_cost_logger_viz import OpenAICostLoggerViz + from openai_cost_logger.openai_cost_logger_utils import OpenAICostLoggerUtils + from openai_cost_logger.openai_cost_logger import OpenAICostLogger + +* See also the homepage on `PyPI `_. +* See the `demo file `_ for a usage example. + +Key Features: +------------- +* Track the cost of every request you make to OpenAI and save them in a csv file. +* Visualize the cost of all the requests you have made. + +Endpoint supported: +------------------- +* Chat completion. +* Every endpoint which response contains the field "*usage.prompt_tokens*" and "*usage.completion_tokens*". diff --git a/demo.ipynb b/demo.ipynb index 5f88d0e..09af674 100644 --- a/demo.ipynb +++ b/demo.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 64, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -28,11 +28,97 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 29, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Defaulting to user installation because normal site-packages is not writeable\n", + "Requirement already satisfied: openai==1.13.3 in /home/drudao/.local/lib/python3.10/site-packages (from -r requirements.txt (line 1)) (1.13.3)\n", + "Requirement already satisfied: matplotlib==3.6.3 in /home/drudao/.local/lib/python3.10/site-packages (from -r requirements.txt (line 2)) (3.6.3)\n", + "Requirement already satisfied: pytest==7.4.2 in /home/drudao/.local/lib/python3.10/site-packages (from -r requirements.txt (line 3)) (7.4.2)\n", + "Requirement already satisfied: tqdm>4 in /home/drudao/.local/lib/python3.10/site-packages (from openai==1.13.3->-r requirements.txt (line 1)) (4.66.2)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in /usr/lib/python3/dist-packages (from openai==1.13.3->-r requirements.txt (line 1)) (1.7.0)\n", + "Requirement already satisfied: pydantic<3,>=1.9.0 in /home/drudao/.local/lib/python3.10/site-packages (from openai==1.13.3->-r requirements.txt (line 1)) (2.6.4)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in /home/drudao/.local/lib/python3.10/site-packages (from openai==1.13.3->-r requirements.txt (line 1)) (0.27.0)\n", + "Requirement already satisfied: anyio<5,>=3.5.0 in /home/drudao/.local/lib/python3.10/site-packages (from openai==1.13.3->-r requirements.txt (line 1)) (4.3.0)\n", + "Requirement already satisfied: sniffio in /home/drudao/.local/lib/python3.10/site-packages (from openai==1.13.3->-r requirements.txt (line 1)) (1.3.1)\n", + "Requirement already satisfied: typing-extensions<5,>=4.7 in /home/drudao/.local/lib/python3.10/site-packages (from openai==1.13.3->-r requirements.txt (line 1)) (4.10.0)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (4.42.1)\n", + "Requirement already satisfied: cycler>=0.10 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (0.11.0)\n", + "Requirement already satisfied: packaging>=20.0 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (23.1)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (2.8.2)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (1.1.1)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (1.4.5)\n", + "Requirement already satisfied: pillow>=6.2.0 in /usr/lib/python3/dist-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (9.0.1)\n", + "Requirement already satisfied: numpy>=1.19 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (1.26.4)\n", + "Requirement already satisfied: pyparsing>=2.2.1 in /usr/lib/python3/dist-packages (from matplotlib==3.6.3->-r requirements.txt (line 2)) (2.4.7)\n", + "Requirement already satisfied: iniconfig in /home/drudao/.local/lib/python3.10/site-packages (from pytest==7.4.2->-r requirements.txt (line 3)) (2.0.0)\n", + "Requirement already satisfied: pluggy<2.0,>=0.12 in /home/drudao/.local/lib/python3.10/site-packages (from pytest==7.4.2->-r requirements.txt (line 3)) (1.3.0)\n", + "Requirement already satisfied: exceptiongroup>=1.0.0rc8 in /home/drudao/.local/lib/python3.10/site-packages (from pytest==7.4.2->-r requirements.txt (line 3)) (1.1.3)\n", + "Requirement already satisfied: tomli>=1.0.0 in /home/drudao/.local/lib/python3.10/site-packages (from pytest==7.4.2->-r requirements.txt (line 3)) (2.0.1)\n", + "Requirement already satisfied: idna>=2.8 in /usr/lib/python3/dist-packages (from anyio<5,>=3.5.0->openai==1.13.3->-r requirements.txt (line 1)) (3.3)\n", + "Requirement already satisfied: certifi in /usr/lib/python3/dist-packages (from httpx<1,>=0.23.0->openai==1.13.3->-r requirements.txt (line 1)) (2020.6.20)\n", + "Requirement already satisfied: httpcore==1.* in /home/drudao/.local/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai==1.13.3->-r requirements.txt (line 1)) (1.0.4)\n", + "Requirement already satisfied: h11<0.15,>=0.13 in /home/drudao/.local/lib/python3.10/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai==1.13.3->-r requirements.txt (line 1)) (0.14.0)\n", + "Requirement already satisfied: annotated-types>=0.4.0 in /home/drudao/.local/lib/python3.10/site-packages (from pydantic<3,>=1.9.0->openai==1.13.3->-r requirements.txt (line 1)) (0.6.0)\n", + "Requirement already satisfied: pydantic-core==2.16.3 in /home/drudao/.local/lib/python3.10/site-packages (from pydantic<3,>=1.9.0->openai==1.13.3->-r requirements.txt (line 1)) (2.16.3)\n", + "Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.7->matplotlib==3.6.3->-r requirements.txt (line 2)) (1.16.0)\n" + ] + } + ], + "source": [ + "!pip install -r requirements.txt" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Defaulting to user installation because normal site-packages is not writeable\n", + "Requirement already satisfied: openai_cost_logger in /home/drudao/.local/lib/python3.10/site-packages (0.0.2)\n", + "Requirement already satisfied: openai in /home/drudao/.local/lib/python3.10/site-packages (from openai_cost_logger) (1.13.3)\n", + "Requirement already satisfied: pandas in /home/drudao/.local/lib/python3.10/site-packages (from openai_cost_logger) (2.1.1)\n", + "Requirement already satisfied: matplotlib in /home/drudao/.local/lib/python3.10/site-packages (from openai_cost_logger) (3.6.3)\n", + "Requirement already satisfied: numpy>=1.19 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib->openai_cost_logger) (1.26.4)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib->openai_cost_logger) (1.1.1)\n", + "Requirement already satisfied: cycler>=0.10 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib->openai_cost_logger) (0.11.0)\n", + "Requirement already satisfied: pillow>=6.2.0 in /usr/lib/python3/dist-packages (from matplotlib->openai_cost_logger) (9.0.1)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib->openai_cost_logger) (4.42.1)\n", + "Requirement already satisfied: pyparsing>=2.2.1 in /usr/lib/python3/dist-packages (from matplotlib->openai_cost_logger) (2.4.7)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib->openai_cost_logger) (2.8.2)\n", + "Requirement already satisfied: packaging>=20.0 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib->openai_cost_logger) (23.1)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in /home/drudao/.local/lib/python3.10/site-packages (from matplotlib->openai_cost_logger) (1.4.5)\n", + "Requirement already satisfied: sniffio in /home/drudao/.local/lib/python3.10/site-packages (from openai->openai_cost_logger) (1.3.1)\n", + "Requirement already satisfied: typing-extensions<5,>=4.7 in /home/drudao/.local/lib/python3.10/site-packages (from openai->openai_cost_logger) (4.10.0)\n", + "Requirement already satisfied: tqdm>4 in /home/drudao/.local/lib/python3.10/site-packages (from openai->openai_cost_logger) (4.66.2)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in /usr/lib/python3/dist-packages (from openai->openai_cost_logger) (1.7.0)\n", + "Requirement already satisfied: pydantic<3,>=1.9.0 in /home/drudao/.local/lib/python3.10/site-packages (from openai->openai_cost_logger) (2.6.4)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in /home/drudao/.local/lib/python3.10/site-packages (from openai->openai_cost_logger) (0.27.0)\n", + "Requirement already satisfied: anyio<5,>=3.5.0 in /home/drudao/.local/lib/python3.10/site-packages (from openai->openai_cost_logger) (4.3.0)\n", + "Requirement already satisfied: pytz>=2020.1 in /usr/lib/python3/dist-packages (from pandas->openai_cost_logger) (2022.1)\n", + "Requirement already satisfied: tzdata>=2022.1 in /home/drudao/.local/lib/python3.10/site-packages (from pandas->openai_cost_logger) (2023.3)\n", + "Requirement already satisfied: idna>=2.8 in /usr/lib/python3/dist-packages (from anyio<5,>=3.5.0->openai->openai_cost_logger) (3.3)\n", + "Requirement already satisfied: exceptiongroup>=1.0.2 in /home/drudao/.local/lib/python3.10/site-packages (from anyio<5,>=3.5.0->openai->openai_cost_logger) (1.1.3)\n", + "Requirement already satisfied: httpcore==1.* in /home/drudao/.local/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai->openai_cost_logger) (1.0.4)\n", + "Requirement already satisfied: certifi in /usr/lib/python3/dist-packages (from httpx<1,>=0.23.0->openai->openai_cost_logger) (2020.6.20)\n", + "Requirement already satisfied: h11<0.15,>=0.13 in /home/drudao/.local/lib/python3.10/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai->openai_cost_logger) (0.14.0)\n", + "Requirement already satisfied: annotated-types>=0.4.0 in /home/drudao/.local/lib/python3.10/site-packages (from pydantic<3,>=1.9.0->openai->openai_cost_logger) (0.6.0)\n", + "Requirement already satisfied: pydantic-core==2.16.3 in /home/drudao/.local/lib/python3.10/site-packages (from pydantic<3,>=1.9.0->openai->openai_cost_logger) (2.16.3)\n", + "Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.7->matplotlib->openai_cost_logger) (1.16.0)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], "source": [ - "# !pip install -r requirements.txt" + "pip install openai_cost_logger" ] }, { @@ -44,14 +130,14 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "import os\n", "import sys\n", - "import pathlib\n", "import openai\n", + "import pathlib\n", "\n", "# Add the src directory to the path\n", "sys.path.insert(0, str(pathlib.Path('src')))" @@ -59,14 +145,14 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 32, "metadata": {}, "outputs": [], "source": [ - "from constants import DEFAULT_LOG_PATH, Models, MODELS_COST\n", - "from openai_cost_tracker_viz import OpenAICostTrackerViz\n", - "from openai_cost_tracker_utils import OpenAICostTrackerUtils\n", - "from openai_cost_tracker import OpenAICostTracker" + "from openai_cost_logger.constants import DEFAULT_LOG_PATH, Models, MODELS_COST\n", + "from openai_cost_logger.openai_cost_logger_viz import OpenAICostLoggerViz\n", + "from openai_cost_logger.openai_cost_logger_utils import OpenAICostLoggerUtils\n", + "from openai_cost_logger.openai_cost_logger import OpenAICostLogger" ] }, { @@ -78,18 +164,18 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "# Export the proper environment variables based on the client you are using.\n", "\n", "# OpenAI API Key\n", - "os.environ[\"OPENAI_ORGANIZATION\"] = OpenAICostTrackerUtils.get_api_key(path='openai_organization.txt')\n", - "os.environ[\"OPENAI_API_KEY\"] = OpenAICostTrackerUtils.get_api_key(path='openai_api_key.txt')\n", + "os.environ[\"OPENAI_ORGANIZATION\"] = OpenAICostLoggerUtils.get_api_key(path='openai_organization.txt')\n", + "os.environ[\"OPENAI_API_KEY\"] = OpenAICostLoggerUtils.get_api_key(path='openai_api_key.txt')\n", "\n", "# Azure OpenAI API Key\n", - "os.environ[\"AZURE_OPENAI_KEY\"] = OpenAICostTrackerUtils.get_api_key('azure_openai_key.txt')" + "os.environ[\"AZURE_OPENAI_KEY\"] = OpenAICostLoggerUtils.get_api_key('azure_openai_key.txt')" ] }, { @@ -101,7 +187,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 34, "metadata": {}, "outputs": [], "source": [ @@ -116,7 +202,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 35, "metadata": {}, "outputs": [], "source": [ @@ -133,7 +219,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 36, "metadata": {}, "outputs": [], "source": [ @@ -152,14 +238,23 @@ "### 5. Demo" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**NOTE:**\n", + "\n", + "The logger is independent of the OpenAI api call. Indeed, It only require the endpoint answer as input and the user is fully responsible of the model call. Despite that, in the cells below you can find a full working demo." + ] + }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 37, "metadata": {}, "outputs": [], "source": [ - "# Create the OpenAICostTracker object\n", - "cost_tracker = OpenAICostTracker(\n", + "# Create the OpenAICostLogger object\n", + "cost_logger = OpenAICostLogger(\n", " experiment_name = experiment_name,\n", " model = model.value,\n", " input_cost = input_cost,\n", @@ -171,24 +266,13 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 38, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Italy\n" - ] - } - ], + "outputs": [], "source": [ - "# Run the chat completion\n", + "# Run the chat completion endpoint\n", "response = client.chat.completions.create(model=model.value, messages=messages, max_tokens=1, temperature=0)\n", - "\n", - "print(response.choices[0].message.content)\n", - "\n", - "cost_tracker.update_cost(response)" + "cost_logger.update_cost(response)" ] }, { @@ -200,48 +284,49 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Total cost: 1.4e-05 (USD)\n" + "Total cost: 0.000986 (USD)\n" ] } ], "source": [ "# Print the total cost\n", - "OpenAICostTrackerViz.print_total_cost(path=DEFAULT_LOG_PATH)" + "OpenAICostLoggerViz.print_total_cost(path=DEFAULT_LOG_PATH)" ] }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "gpt-3.5-turbo: 1.4e-05 (USD)\n" + "gpt-3.5-turbo: 0.000887 (USD)\n", + "gpt-35-turbo-0125: 9.9e-05 (USD)\n" ] } ], "source": [ "# Cost by model\n", - "OpenAICostTrackerViz.print_total_cost_by_model(path=DEFAULT_LOG_PATH)" + "OpenAICostLoggerViz.print_total_cost_by_model(path=DEFAULT_LOG_PATH)" ] }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 41, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHHCAYAAABDUnkqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAzwElEQVR4nO3de1xU5b7H8e8AMpg6o6ZcJAI1rewChkpobjNR8pZ22amZkGVqadvklEkXkS5iNyOVMrtopablTq0wOgqlXeh4xCg1rczrUcE7I6igzDp/dJx9ZoMKCgwsP+/Xa/0xz3ouv8Uf+n2t9awZi2EYhgAAAEzCy9MFAAAAVCXCDQAAMBXCDQAAMBXCDQAAMBXCDQAAMBXCDQAAMBXCDQAAMBXCDQAAMBXCDQAAMBXCDYA6ae7cubJYLFq7dq1H1r/55pt18803e2RtAGdHuAFQrj///FOjRo1Sq1at5OfnJ5vNpi5duuj111/X8ePHq3y9Y8eOafLkyfrmm2+qfG4AFxcfTxcAoPZJT0/X3//+d1mtVsXFxenaa69VSUmJvvvuOz3++OPauHGjZs+eXaVrHjt2TMnJyZLEHREAF4RwA8DNtm3bNHjwYIWGhiorK0tBQUGuc2PGjNGWLVuUnp7uwQoB4Ox4LAXAzUsvvaTCwkK9++67bsHmtCuuuELjxo1zfT516pSee+45tW7dWlarVWFhYXryySdVXFzsNm7t2rWKjY1Vs2bNVL9+fbVs2VL333+/JGn79u1q3ry5JCk5OVkWi0UWi0WTJ08+Z73Hjh3TqFGjdOmll8pmsykuLk6HDx92nY+Pj1ezZs108uTJMmN79eqlK6+88pxrzJ49W61bt1b9+vXVqVMnffvtt2X6lJSUaNKkSYqMjJTdbleDBg3UtWtXff31164+hmEoLCxMAwYMKDP+xIkTstvtGjVq1DnrAXB2hBsAbj7//HO1atVKnTt3rlD/ESNGaNKkSbrhhhv02muvqVu3bkpJSdHgwYNdffbt26devXpp+/btmjhxombMmKGhQ4fqxx9/lCQ1b95cb775piTp9ttv14cffqgPP/xQd9xxxznXHzt2rDZt2qTJkycrLi5O8+fP18CBA2UYhiRp2LBhOnjwoL766iu3cXl5ecrKytK999571vnfffddjRo1SoGBgXrppZfUpUsX3Xbbbdq1a5dbP4fDoXfeeUc333yzXnzxRU2ePFn79+9XbGyscnNzJUkWi0X33nuvvvzySx06dMht/Oeffy6Hw3HOegBUgAEA/6egoMCQZAwYMKBC/XNzcw1JxogRI9zaH3vsMUOSkZWVZRiGYSxZssSQZPz3f//3Gefav3+/IclISkqq0Npz5swxJBmRkZFGSUmJq/2ll14yJBnLli0zDMMwSktLjcsuu8wYNGiQ2/hp06YZFovF2Lp16xnXKCkpMfz9/Y2IiAijuLjY1T579mxDktGtWzdX26lTp9z6GIZhHD582AgICDDuv/9+V9tvv/1mSDLefPNNt7633XabERYWZjidzgpdP4Az484NABeHwyFJatSoUYX6L1++XJKUkJDg1v4f//EfkuTam9O4cWNJ0hdffFHu46ELMXLkSNWrV8/1+aGHHpKPj4+rNi8vLw0dOlSfffaZjh496uo3f/58de7cWS1btjzj3GvXrtW+ffs0evRo+fr6utrvu+8+2e12t77e3t6uPk6nU4cOHdKpU6fUoUMHrVu3ztWvbdu2ioqK0vz5811thw4d0pdffqmhQ4fKYrGc518CwGkXdbhZvXq1+vfvrxYtWshisWjp0qXVut7kyZNdewlOH1dddVW1rglUhs1mkyS3EHA2O3bskJeXl6644gq39sDAQDVu3Fg7duyQJHXr1k133nmnkpOT1axZMw0YMEBz5swpsy/nfLRp08btc8OGDRUUFKTt27e72uLi4nT8+HEtWbJEkvTbb78pJydHw4YNO+f1lbdGvXr11KpVqzL933//fV1//fXy8/PTpZdequbNmys9PV0FBQVu/eLi4vT999+75v/kk0908uTJc9YDoGIu6nBTVFSk8PBwpaWl1dia11xzjfbu3es6vvvuuxpbGzgXm82mFi1aaMOGDZUad667DRaLRYsXL1Z2drbGjh2r3bt36/7771dkZKQKCwsvpOQKadeunSIjIzVv3jxJ0rx58+Tr66u77767ytaYN2+e7rvvPrVu3VrvvvuuMjIytGLFCt1yyy1yOp1ufQcPHqx69eq57t7MmzdPHTp0qNDmZgDndlGHm969e+v555/X7bffXu754uJiPfbYYwoODlaDBg0UFRV1wV8w5uPjo8DAQNfRrFmzC5oPqGr9+vXTn3/+qezs7HP2DQ0NldPp1B9//OHWnp+fryNHjig0NNSt/cYbb9QLL7ygtWvXav78+dq4caMWLlwo6dwB6Uz+fe3CwkLt3btXYWFhbu1xcXHKysrS3r17tWDBAvXt21dNmjQ55/WVt8bJkye1bds2t7bFixerVatW+vTTTzVs2DDFxsYqJiZGJ06cKDNv06ZN1bdvX82fP187duzQ999/z10boApd1OHmXMaOHavs7GwtXLhQv/zyi/7+97/r1ltvLfMPXWX88ccfatGihVq1aqWhQ4dq586dVVgxcOEmTJigBg0aaMSIEcrPzy9z/s8//9Trr78uSerTp48kKTU11a3PtGnTJEl9+/aVJB0+fNj19tJpERERkuR6NHXJJZdIko4cOVKpemfPnu22j+fNN9/UqVOn1Lt3b7d+Q4YMkcVi0bhx47R169YKvZXUoUMHNW/eXLNmzVJJSYmrfe7cuWXq9Pb2liS36/yv//qvM4bEYcOG6ddff9Xjjz8ub29vt7fLAFwgT+9ori0kGUuWLHF93rFjh+Ht7W3s3r3brV+PHj2MxMTE81pj+fLlxscff2z8/PPPRkZGhhEdHW1cfvnlhsPhuJDSgSq3bNkyw8/Pz2jSpIkxbtw44+233zbS0tKMoUOHGr6+vsbIkSNdfePj4w1Jxt13322kpaW5Pg8cONDV57XXXjPatGljTJgwwXjrrbeMV155xbjyyisNm83m9rZSu3btjMDAQCMtLc346KOPjPXr15+xxtNvS1133XVG165djRkzZhhjx441vLy8jJtuuqnct4769etnSDIaN25snDhxokJ/i7feesuQZHTp0sWYPn26MX78eKNx48ZGq1at3N6Weu+99wxJxm233Wa89dZbxsSJE43GjRsb11xzjREaGlpm3uLiYuPSSy81JBm9e/euUC0AKoZw83/+Pdx88cUXhiSjQYMGboePj49x9913G4ZhGJs2bTIknfV44oknzrjm4cOHDZvNZrzzzjvVfXlApf3+++/Ggw8+aISFhRm+vr5Go0aNjC5duhgzZsxwCwYnT540kpOTjZYtWxr16tUzQkJCjMTERLc+69atM4YMGWJcfvnlhtVqNfz9/Y1+/foZa9eudVvzhx9+MCIjIw1fX99zvhZ+OtysWrXKGDlypNGkSROjYcOGxtChQ42DBw+WO+bjjz82JLmFs4p44403jJYtWxpWq9Xo0KGDsXr1aqNbt25u4cbpdBpTpkwxQkNDDavVarRv39744osvjPj4+HLDjWEYxsMPP2xIMhYsWFCpegCcncUw/u1e8UXKYrFoyZIlGjhwoCRp0aJFGjp0qDZu3Oi63Xxaw4YNFRgYqJKSEm3duvWs855+Y+JMOnbsqJiYGKWkpFzwNQA4u2XLlmngwIFavXq1unbt6ulyNH78eL377rvKy8tzPZYDcOH4bakzaN++vUpLS7Vv374z/iPo6+t7Qa9yFxYW6s8//2QjIVBD3n77bbVq1Uo33XSTp0vRiRMnNG/ePN15550EG6CKXdThprCwUFu2bHF93rZtm3Jzc9W0aVO1bdtWQ4cOVVxcnF599VW1b99e+/fvV2Zmpq6//nrXRsnKeOyxx9S/f3+FhoZqz549SkpKkre3t4YMGVKVlwXg35x+KSA9PV2vv/66R78ob9++fVq5cqUWL16sgwcPuv1OF4Aq4unnYp709ddfl7tPJj4+3jCMv756fdKkSUZYWJhRr149IygoyLj99tuNX3755bzWGzRokBEUFGT4+voawcHBxqBBg4wtW7ZU4RUBKI8ko2HDhsYDDzxgnDx50qO1nP53x9/f35gxY4ZHawHMij03AADAVPieGwAAYCqEGwAAYCoX3YZip9OpPXv2qFGjRvz6LgAAdYRhGDp69KhatGghL6+z35u56MLNnj17FBIS4ukyAADAedi1a5cuu+yys/a56MJNo0aNJP31x7HZbB6uBgAAVITD4VBISIjr//GzuejCzelHUTabjXADAEAdU5EtJWwoBgAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApuLRcLN69Wr1799fLVq0kMVi0dKlSys89vvvv5ePj48iIiKqrT4AAFD3eDTcFBUVKTw8XGlpaZUad+TIEcXFxalHjx7VVBkAAKirPPrDmb1791bv3r0rPW706NG655575O3tXam7PQAAwPzq3J6bOXPmaOvWrUpKSvJ0KQAAoBby6J2byvrjjz80ceJEffvtt/LxqVjpxcXFKi4udn12OBzVVR4AAKgF6ky4KS0t1T333KPk5GS1bdu2wuNSUlKUnJxcjZW5C5uYXmNrAQBQG22f2tej69eZx1JHjx7V2rVrNXbsWPn4+MjHx0fPPvusfv75Z/n4+CgrK6vccYmJiSooKHAdu3btquHKAQBATaozd25sNpvWr1/v1vbGG28oKytLixcvVsuWLcsdZ7VaZbVaa6JEAABQC3g03BQWFmrLli2uz9u2bVNubq6aNm2qyy+/XImJidq9e7c++OADeXl56dprr3Ub7+/vLz8/vzLtAADg4uXRcLN27Vp1797d9TkhIUGSFB8fr7lz52rv3r3auXOnp8oDAAB1kMUwDMPTRdQkh8Mhu92ugoIC2Wy2Kp+fDcUAgItddWworsz/33VmQzEAAEBFEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpeDTcrF69Wv3791eLFi1ksVi0dOnSs/b/9NNP1bNnTzVv3lw2m03R0dH66quvaqZYAABQJ3g03BQVFSk8PFxpaWkV6r969Wr17NlTy5cvV05Ojrp3767+/fvrp59+quZKAQBAXeHjycV79+6t3r17V7h/amqq2+cpU6Zo2bJl+vzzz9W+ffsqrg4AANRFHg03F8rpdOro0aNq2rTpGfsUFxeruLjY9dnhcNREaQAAwEPq9IbiV155RYWFhbr77rvP2CclJUV2u911hISE1GCFAACgptXZcLNgwQIlJyfr448/lr+//xn7JSYmqqCgwHXs2rWrBqsEAAA1rU4+llq4cKFGjBihTz75RDExMWfta7VaZbVaa6gyAADgaXXuzs1HH32k4cOH66OPPlLfvn09XQ4AAKhlPHrnprCwUFu2bHF93rZtm3Jzc9W0aVNdfvnlSkxM1O7du/XBBx9I+utRVHx8vF5//XVFRUUpLy9PklS/fn3Z7XaPXAMAAKhdPHrnZu3atWrfvr3rNe6EhAS1b99ekyZNkiTt3btXO3fudPWfPXu2Tp06pTFjxigoKMh1jBs3ziP1AwCA2sejd25uvvlmGYZxxvNz5851+/zNN99Ub0EAAKDOq3N7bgAA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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlEAAAHHCAYAAACfqw0dAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/P9b71AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA8bUlEQVR4nO3de1hVZd7/8Q8HOXgANJWDoqB5zJJRE9HKShIVC8qe0KEwNbUDZZn5U/OQnWisHPNQjFOpmY6FU1Zq9BimlTKomOUBzRTNpsADAooKyr5/f/i4Z3aiwgrdgO/Xde2Li3t9172+a7eKT2vfLFyMMUYAAACoEFdnNwAAAFAdEaIAAAAsIEQBAABYQIgCAACwgBAFAABgASEKAADAAkIUAACABYQoAAAACwhRAAAAFhCiAOAi5s+fLxcXF23atMkpx7/11lt16623OuXYAC6OEAXAqfbs2aORI0eqRYsW8vLyko+Pj3r06KE33nhDJ0+erPTjnThxQs8995zWrFlT6XMDuLq4O7sBAFevFStW6H/+53/k6emphIQEdejQQSUlJfr222/1zDPPaPv27Zo7d26lHvPEiROaOnWqJHGHB8AfQogC4BTZ2dkaOHCgmjdvrtWrVyswMNC+7bHHHtNPP/2kFStWOLFDALg4Ps4D4BTTpk3T8ePH9c477zgEqHOuvfZajRo1yv79mTNn9MILL6hly5by9PRUSEiIJkyYoOLiYof9Nm3apKioKDVs2FDe3t4KDQ3V0KFDJUn79u1To0aNJElTp06Vi4uLXFxc9Nxzz12y3xMnTmjkyJG65ppr5OPjo4SEBB09etS+ffDgwWrYsKFOnz593r69e/dWmzZtLnmMuXPnqmXLlvL29lbXrl31zTffnFdTUlKiyZMnq3PnzvL19VWdOnV0880366uvvrLXGGMUEhKimJiY8/Y/deqUfH19NXLkyEv2A+DiCFEAnOKzzz5TixYt1L1793LVP/TQQ5o8ebI6deqkv/71r+rZs6eSkpI0cOBAe83BgwfVu3dv7du3T+PGjdOsWbMUHx+vf/3rX5KkRo0a6a233pIk3X333Vq4cKEWLlyoe+6555LHT0xMVFZWlp577jklJCRo0aJFio2NlTFGkvTAAw/oyJEj+uKLLxz2y8nJ0erVq3X//fdfdP533nlHI0eOVEBAgKZNm6YePXrorrvu0oEDBxzqCgsL9fbbb+vWW2/VX/7yFz333HM6dOiQoqKitGXLFkmSi4uL7r//fn3++efKy8tz2P+zzz5TYWHhJfsBUA4GAK6wgoICI8nExMSUq37Lli1GknnooYccxseMGWMkmdWrVxtjjPn444+NJLNx48YLznXo0CEjyUyZMqVcx543b56RZDp37mxKSkrs49OmTTOSzCeffGKMMaa0tNQ0bdrUxMXFOew/ffp04+LiYvbu3XvBY5SUlJjGjRubsLAwU1xcbB+fO3eukWR69uxpHztz5oxDjTHGHD161Pj7+5uhQ4fax3bt2mUkmbfeesuh9q677jIhISHGZrOV6/wBXBh3ogBccYWFhZKkevXqlat+5cqVkqTRo0c7jD/99NOSZF875efnJ0lavnx5mR+r/REjRoxQrVq17N8/8sgjcnd3t/fm6uqq+Ph4ffrppzp27Ji9btGiRerevbtCQ0MvOPemTZt08OBBPfzww/Lw8LCPP/jgg/L19XWodXNzs9fYbDbl5eXpzJkz6tKlizZv3myva926tcLDw7Vo0SL7WF5enj7//HPFx8fLxcXF4jsB4BxCFIArzsfHR5IcwsbF7N+/X66urrr22msdxgMCAuTn56f9+/dLknr27KkBAwZo6tSpatiwoWJiYjRv3rzz1k1Z0apVK4fv69atq8DAQO3bt88+lpCQoJMnT+rjjz+WJO3atUuZmZl64IEHLnl+ZR2jVq1aatGixXn1CxYs0A033CAvLy9dc801atSokVasWKGCggKHuoSEBK1bt84+f0pKik6fPn3JfgCUDyEKwBXn4+OjoKAgbdu2rUL7XeruiYuLi5YuXar09HQlJibq3//+t4YOHarOnTvr+PHjf6Tlcmnfvr06d+6s999/X5L0/vvvy8PDQ/fdd1+lHeP999/Xgw8+qJYtW+qdd95RamqqVq1apdtvv102m82hduDAgapVq5b9btT777+vLl26lGuRO4BLI0QBcIr+/ftrz549Sk9Pv2Rt8+bNZbPZtHv3bofx3Nxc5efnq3nz5g7j3bp100svvaRNmzZp0aJF2r59u5YsWSLp0kHsQn5/7OPHj+u3335TSEiIw3hCQoJWr16t3377TYsXL1Z0dLTq169/yfMr6xinT59Wdna2w9jSpUvVokULffTRR3rggQcUFRWlyMhInTp16rx5GzRooOjoaC1atEj79+/XunXruAsFVCJCFACnGDt2rOrUqaOHHnpIubm5523fs2eP3njjDUlSv379JEkzZsxwqJk+fbokKTo6WpJ09OhR+2/LnRMWFiZJ9o/0ateuLUnKz8+vUL9z5851WGf11ltv6cyZM+rbt69D3aBBg+Ti4qJRo0Zp79695fotuC5duqhRo0ZKTk5WSUmJfXz+/Pnn9enm5iZJDueZkZFxwTD6wAMPaMeOHXrmmWfk5ubm8NuMAP4YHrYJwClatmypxYsXKy4uTu3atXN4Yvn69euVkpKiBx98UJLUsWNHDR48WHPnzlV+fr569uypDRs2aMGCBYqNjdVtt90m6exaoTfffFN33323WrZsqWPHjunvf/+7fHx87EHM29tb7du31wcffKDWrVurQYMG6tChgzp06HDRfktKStSrVy/dd9992rVrl958803ddNNNuuuuuxzqGjVqpD59+iglJUV+fn72gHcxtWrV0osvvqiRI0fq9ttvV1xcnLKzszVv3rzz1kT1799fH330ke6++25FR0crOztbycnJat++fZkfWUZHR+uaa65RSkqK+vbtq8aNG1+yHwDl5OxfDwRwdfvxxx/N8OHDTUhIiPHw8DD16tUzPXr0MLNmzTKnTp2y150+fdpMnTrVhIaGmlq1apng4GAzfvx4h5rNmzebQYMGmWbNmhlPT0/TuHFj079/f7Np0yaHY65fv9507tzZeHh4XPJxB+cecbB27VozYsQIU79+fVO3bl0THx9vjhw5UuY+H374oZFkRowYUaH34s033zShoaHG09PTdOnSxXz99demZ8+eDo84sNls5uWXXzbNmzc3np6e5k9/+pNZvny5GTx4sGnevHmZ8z766KNGklm8eHGF+gFwcS7G/O7eNwDgD/nkk08UGxurr7/+WjfffLOz29FTTz2ld955Rzk5OfaPMwH8cYQoAKhk/fv3V1ZWln766SenP4/p1KlTCg4OVv/+/TVv3jyn9gLUNKyJAoBKsmTJEv3www9asWKF3njjDacGqIMHD+rLL7/U0qVLdeTIEYe/QwigcnAnCgAqiYuLi+rWrau4uDglJyfL3d15/5+6Zs0a3XbbbWrcuLEmTZqkxMREp/UC1FSEKAAAAAt4ThQAAIAFhCgAAAALWFh+GdlsNv3666+qV6+e039DBwAAlI8xRseOHVNQUJBc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6oknnrgcPQIAAFQ5Fb4TlZqaqi+//FLt2rWzj7Vv315z5sxR7969K7U5AACAqqrCd6JsNptq1ap13nitWrXsz48CAACo6Socom6//XaNGjVKv/76q33s3//+t5566in16tWrUpsDAACoqiocombPnq3CwkKFhISoZcuWatmypUJDQ1VYWKhZs2Zdjh4BAACqnAqviQoODtbmzZv15ZdfaufOnZKkdu3aKTIystKbAwAAqKosPSfKxcVFd9xxh+64447K7gcAAKBaKPfHeatXr1b79u1VWFh43raCggJdd911+uabbyq1OQAAgKqq3CFqxowZGj58uHx8fM7b5uvrq5EjR2r69OmV2hwAAEBVVe4Q9f3336tPnz4X3N67d29lZmZaamLOnDkKCQmRl5eXwsPDtWHDhovWp6SkqG3btvLy8tL111+vlStXOmw3xmjy5MkKDAyUt7e3IiMjtXv3boeavLw8xcfHy8fHR35+fho2bJiOHz9e5vF++ukn1atXz/6EdgAAgHKHqNzc3DKfD3WOu7u7Dh06VOEGPvjgA40ePVpTpkzR5s2b1bFjR0VFRengwYNl1q9fv16DBg3SsGHD9N133yk2NlaxsbHatm2bvWbatGmaOXOmkpOTlZGRoTp16igqKkqnTp2y18THx2v79u1atWqVli9frq+//lojRow473inT5/WoEGDdPPNN1f43AAAQM1V7hDVpEkTh6Dyez/88IMCAwMr3MD06dM1fPhwDRkyRO3bt1dycrJq166td999t8z6N954Q3369NEzzzyjdu3a6YUXXlCnTp00e/ZsSWfvQs2YMUMTJ05UTEyMbrjhBr333nv69ddftWzZMklSVlaWUlNT9fbbbys8PFw33XSTZs2apSVLljg8/0qSJk6cqLZt2+q+++6r8LkBAICaq9whql+/fpo0aZLD3ZxzTp48qSlTpqh///4VOnhJSYkyMzMdHo/g6uqqyMhIpaenl7lPenr6eY9TiIqKstdnZ2crJyfHocbX11fh4eH2mvT0dPn5+alLly72msjISLm6uiojI8M+tnr1aqWkpGjOnDnlOp/i4mIVFhY6vAAAQM1U7kccTJw4UR999JFat26txMREtWnTRpK0c+dOzZkzR6WlpXr22WcrdPDDhw+rtLRU/v7+DuP+/v72Z1D9Xk5OTpn1OTk59u3nxi5W07hx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33130Xn37t1rateubUaPHm127NhhZs2aZdzc3Exqaqq95qWXXjLXXHONWb58ucnOzjYpKSmmbt265o033rhk36g6qtI1vmTJEuPh4WHeffdds337djN8+HDj5+dncnNz7TV/+9vfzNq1a012drbJzMw0d955pwkODjZnzpypUC933XWXCQ8PN6tWrTLZ2dlm/fr15ttvv62MtxROdjVe02+++aapV6+eWbJkidmzZ4/5xz/+YerWrWs+/fTTynpbrwhCVA138OBBI8msXbvWGHP2Sey1atUyKSkp9pqsrCwjyaSnp19wnkcffdTcdttt543HxcWZiRMnmilTplwyRNlsNhMQEGBeffVV+1h+fr7x9PQ0//jHPxxqV65cadq2bWu2b99erhA1duxYc911153XW1RUlP376OhoM3ToUIeae+65x8THx190blRtzrzGu3btah577DH796WlpSYoKMgkJSVd8Djff/+9kWR++umncvfy+eefG19fX3PkyJEL7oOa42q4piMiIsyYMWMcakaPHm169OhxwTmqIj7Oq+EKCgokSQ0aNJAkZWZm6vTp04qMjLTXtG3bVs2aNVN6evpF5zk3xznz5s3T3r17NWXKlHL1kp2drZycHIdj+/r6Kjw83OHYubm5Gj58uBYuXKjatWuXa+709HSHeSUpKirKYd7u3bsrLS1NP/74o6Szt5y//fZb9e3bt1zHQNXkrGu8pKREmZmZDsdxdXVVZGTkBY9TVFSkefPmKTQ0VMHBweXu5dNPP1WXLl00bdo0NWnSRK1bt9aYMWN08uTJC86B6utquKaLi4vl5eXlUOPt7a0NGzbo9OnTF5ynqnHqHyDG5WWz2fTkk0+qR48e6tChgyQpJydHHh4e5/1hZH9/f+Xk5JQ5z/r16/XBBx9oxYoV9rHdu3dr3Lhx+uabb+TuXr7L6Nz8/v7+Fzy2MUYPPvigHn74YXXp0kX79u0r99xlzVtYWKiTJ0/K29tb48aNU2Fhodq2bSs3NzeVlpbqpZdeUnx8fLmOgarHmdf44cOHVVpaWuZ199/rQyTpzTff1NixY1VUVKQ2bdpo1apV8vDwKHcve/fu1bfffisvLy99/PHHOnz4sB599FEdOXJE8+bNu/AbhGrnarmmo6Ki9Pbbbys2NladOnVSZmam3n77bZ0+fVqHDx9WYGDghd+kKoQ7UTXYY489pm3btmnJkiWW59i2bZtiYmI0ZcoU9e7dW9LZBdp//vOfNXXqVLVu3brM/RYtWqS6devaX9988025jjdr1iwdO3ZM48ePv2DNf8/78MMPl/tcPvzwQy1atEiLFy/W5s2btWDBAr322mtasGBBuedA1eLMa7wi4uPj9d1332nt2rVq3bq17rvvPp06dapcvUhnf7C6uLho0aJF6tq1q/r166fp06drwYIF3I2qYa6Wa3rSpEnq27evunXrplq1aikmJkaDBw+WdPbuV7Xh7M8TcXk89thjpmnTpmbv3r0O42lpaUaSOXr0qMN4s2bNzPTp0x3Gtm/fbho3bmwmTJjgMH706FEjybi5udlfLi4u9rG0tDRTWFhodu/ebX+dOHHC7Nmzp8z1Tbfccot54oknjDHGxMTEGFdXV4e5z82bkJBgjDEO855b7HjzzTebUaNGOcz77rvvGh8fH/v3TZs2NbNnz3aoeeGFF0ybNm0u/YaiynH2NV5cXGzc3NzMxx9/7LBvQkKCueuuuy7Yd3Fxsaldu7ZZvHhxuXo5N2fLli0dxnbs2GEkmR9//PGCx0L1cjVd0+eUlJSYAwcOmDNnztgXm5eWll6wvqohRNUwNpvNPPbYYyYoKKjM/7ieW6C4dOlS+9jOnTvPW6C4bds207hxY/PMM8+cN0dpaanZunWrw+uRRx4xbdq0MVu3bnX4jZLf9xYQEGBee+01+1hBQYHDwvL9+/c7zPvFF18YSWbp0qXmwIEDFzzvsWPHmg4dOjiMDRo0yGFheYMGDcybb77pUPPyyy+bVq1aXXBeVD1V6Rrv2rWrSUxMdNivSZMmF12Ee+rUKePt7W3mzZtXrl6MOfvbUN7e3ubYsWP2sWXLlhlXV1dz4sSJCx4L1cPVeE2X5ZZbbjGDBg0qd31VQIiqYR555BHj6+tr1qxZY3777Tf767//Q/vwww+bZs2amdWrV5tNmzaZiIgIExERYd++detW06hRI3P//fc7zHHw4MELHrc8v51nzNlHHPj5+ZlPPvnE/PDDDyYmJuaCjzgwxpjs7OwKPeLgmWeeMVlZWWbOnDnnPeJg8ODBpkmTJvZHHHz00UemYcOGZuzYsZfsG1VHVbrGlyxZYjw9Pc38+fPNjh07zIgRI4yfn5/JyckxxhizZ88e8/LLL5tNmzaZ/fv3m3Xr1pk777zTNGjQwH4XtTy9HDt2zDRt2tTce++9Zvv27Wbt2rWmVatW5qGHHqqMtxROdjVe07t27TILFy40P/74o8nIyDBxcXGmQYMGJjs7uxLe0SuHEFXDSCrz9d//h3Dy5Enz6KOPmvr165vatWubu+++2/z222/27VOmTClzjubNm1/wuOUNUTabzUyaNMn4+/sbT09P06tXL7Nr164L1pc3RBljzFdffWXCwsKMh4eHadGihcM5G2NMYWGhGTVqlGnWrJnx8vIyLVq0MM8++6wpLi6+5NyoOqraNT5r1izTrFkz4+HhYbp27Wr+9a9/2bf9+9//Nn379jWNGzc2tWrVMk2bNjV//vOfzc6dOyvcS1ZWlomMjDTe3t6madOmZvTo0dyFqiGuxmt6x44dJiwszHh7exsfHx8TExPjMEd14WIMj2sGAACoqGq0BB4AAKDqIEQBAABYQIgCAACwgBAFAABgASEKAADAAkIUAACABYQoAAAACwhRAAAAFhCiAEDSgw8+KBcXF7m4uKhWrVry9/fXHXfcoXfffVc2m83Z7QGogghRAPB/+vTpo99++0379u3T559/rttuu02jRo1S//79debMGWe3B6CKIUQBwP/x9PRUQECAmjRpok6dOmnChAn65JNP9Pnnn2v+/PmSpOnTp+v6669XnTp1FBwcrEcffVTHjx+XJBUVFcnHx0dLly51mHfZsmWqU6eOjh07dqVPCcBlRIgCgIu4/fbb1bFjR3300UeSJFdXV82cOVPbt2/XggULtHr1ao0dO1aSVKdOHQ0cOFDz5s1zmGPevHm69957Va9evSveP4DLhz9ADAA6uyYqPz9fy5YtO2/bwIED9cMPP2jHjh3nbVu6dKkefvhhHT58WJK0YcMGde/eXQcOHFBgYKAOHjyoJk2a6Msvv1TPnj0v92kAuIK4EwUAl2CMkYuLiyTpyy+/VK9evdSkSRPVq1dPDzzwgI4cOaITJ05Ikrp27arrrrtOCxYskCS9//77at68uW655Ran9Q/g8iBEAcAlZGVlKTQ0VPv27VP//v11ww036J///KcyMzM1Z84cSVJJSYm9/qGHHrKvoZo3b56GDBliD2EAag5CFABcxOrVq7V161YNGDBAmZmZstlsev3119WtWze1bt1av/7663n73H///dq/f79mzpypHTt2aPDgwU7oHMDl5u7sBgCgqiguLlZOTo5KS0uVm5ur1NRUJSUlqX///kpISNC2bdt0+vRpzZo1S3feeafWrVun5OTk8+apX7++7rnnHj3zzDPq3bu3mjZt6oSzAXC5cScKAP5PamqqAgMDFRISoj59+uirr77SzJkz9cknn8jNzU0dO3bU9OnT9Ze//EUdOnTQokWLlJSUVOZcw4YNU0lJiYYOHXqFzwLAlcJv5wHAZbBw4UI99dRT+vXXX+Xh4eHsdgBcBnycBwCV6MSJE/rtt9/0yiuvaOTIkQQooAbj4zwAqETTpk1T27ZtFRAQoPHjxzu7HQCXER/nAQAAWMCdKAAAAAsIUQAAABYQogAAACwgRAEAAFhAiAIAALCAEAUAAGABIQoAAMACQhQAAIAFhCgAAAAL/j+tm/sa9GvIcgAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -252,15 +337,8 @@ ], "source": [ "# Visualize the cost by day\n", - "OpenAICostTrackerViz.plot_cost_by_day(path=DEFAULT_LOG_PATH)" + "OpenAICostLoggerViz.plot_cost_by_day(path=DEFAULT_LOG_PATH)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -279,7 +357,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/openai_cost_logger/__init__.py b/openai_cost_logger/__init__.py new file mode 100644 index 0000000..d87ceb1 --- /dev/null +++ b/openai_cost_logger/__init__.py @@ -0,0 +1 @@ +print('imported openai_cost_tracker') \ No newline at end of file diff --git a/src/constants.py b/openai_cost_logger/constants.py similarity index 72% rename from src/constants.py rename to openai_cost_logger/constants.py index 2062f64..6a72e32 100644 --- a/src/constants.py +++ b/openai_cost_logger/constants.py @@ -8,6 +8,8 @@ class Models(Enum): TURBO_3_5 = "gpt-3.5-turbo" TURBO_3_5_INSTRUCT = "gpt-3.5-turbo-instruct" AZURE_3_5_TURBO = "gpt-35-turbo-0125" + AZURE_4_TURBO = "gpt-4-0125-Preview" + AZURE_4 = "gpt-4-0613" """The costs of the models above (per million tokens).""" MODELS_COST = { @@ -22,5 +24,13 @@ class Models(Enum): "gpt-3.5-turbo-instruct": { "input": 1.5, "output": 2 - } + }, + "gpt-4-0125-Preview": { + "input": 10, + "output": 30 + }, + "gpt-4-0613": { + "input": 30, + "output": 60 + }, } \ No newline at end of file diff --git a/src/openai_cost_tracker.py b/openai_cost_logger/openai_cost_logger.py similarity index 92% rename from src/openai_cost_tracker.py rename to openai_cost_logger/openai_cost_logger.py index 16b40d8..ed12181 100644 --- a/src/openai_cost_tracker.py +++ b/openai_cost_logger/openai_cost_logger.py @@ -1,11 +1,10 @@ import csv -from enum import Enum +from typing import Dict from pathlib import Path from time import strftime -from typing import List, Dict from openai.types.chat.chat_completion import ChatCompletion -from constants import DEFAULT_LOG_PATH +from openai_cost_logger.constants import DEFAULT_LOG_PATH """Every cost is per million tokens.""" COST_UNIT = 1_000_000 @@ -17,8 +16,8 @@ "cost" ] -"""OpenAI cost tracker""" -class OpenAICostTracker: +"""OpenAI cost logger""" +class OpenAICostLogger: def __init__( self, model: str, @@ -28,7 +27,7 @@ def __init__( cost_upperbound: float = float('inf'), log_folder: str = DEFAULT_LOG_PATH, ): - """Initialize the cost tracker. + """Initialize the cost logger. Args: client (enum.ClientType): The client to use. @@ -49,7 +48,6 @@ def __init__( self.cost_upperbound = cost_upperbound self.filename = f"{experiment_name}_cost_" + strftime("%Y-%m-%d_%H:%M:%S") + ".csv" - def update_cost(self, response: ChatCompletion) -> None: """Extract the number of input and output tokens from a chat completion response and update the cost. Saves experiment costs to file, overwriting it. @@ -68,6 +66,14 @@ def update_cost(self, response: ChatCompletion) -> None: csvwriter.writerow(FILE_HEADER) csvwriter.writerow([self.experiment_name, self.model, self.cost]) + def get_current_cost(self) -> float: + """Get the current cost of the cost tracker. + + Returns: + float: The current cost. + """ + return self.cost + def __get_answer_cost(self, answer: Dict) -> float: """Calculate the cost of the answer based on the input and output tokens. @@ -86,12 +92,4 @@ def __validate_cost(self): Exception: If the cost exceeds the upperbound. """ if self.cost > self.cost_upperbound: - raise Exception(f"Cost exceeded upperbound: {self.cost} > {self.cost_upperbound}") - - def get_current_cost(self) -> float: - """Get the current cost of the cost tracker. - - Returns: - float: The current cost. - """ - return self.cost + raise Exception(f"Cost exceeded upperbound: {self.cost} > {self.cost_upperbound}") \ No newline at end of file diff --git a/src/openai_cost_tracker_utils.py b/openai_cost_logger/openai_cost_logger_utils.py similarity index 80% rename from src/openai_cost_tracker_utils.py rename to openai_cost_logger/openai_cost_logger_utils.py index cde43bf..362a8fc 100644 --- a/src/openai_cost_tracker_utils.py +++ b/openai_cost_logger/openai_cost_logger_utils.py @@ -1,7 +1,7 @@ from pathlib import Path -"""OpenAI cost tracker utilities functions.""" -class OpenAICostTrackerUtils: +"""OpenAI cost logger utilities functions.""" +class OpenAICostLoggerUtils: @staticmethod def get_api_key(path: str) -> str: diff --git a/src/openai_cost_tracker_viz.py b/openai_cost_logger/openai_cost_logger_viz.py similarity index 81% rename from src/openai_cost_tracker_viz.py rename to openai_cost_logger/openai_cost_logger_viz.py index 6b1e628..80ddf3c 100644 --- a/src/openai_cost_tracker_viz.py +++ b/openai_cost_logger/openai_cost_logger_viz.py @@ -5,9 +5,9 @@ import matplotlib.pyplot as plt from collections import defaultdict -from constants import DEFAULT_LOG_PATH +from openai_cost_logger.constants import DEFAULT_LOG_PATH -class OpenAICostTrackerViz: +class OpenAICostLoggerViz: @staticmethod def get_total_cost(path: str = DEFAULT_LOG_PATH) -> float: @@ -15,7 +15,7 @@ def get_total_cost(path: str = DEFAULT_LOG_PATH) -> float: Args: log_folder (str, optional): Cost logs directory. Defaults to DEFAULT_LOG_PATH. - + This method reads all the files in the specified directory. Returns: float: the total cost. """ @@ -34,9 +34,10 @@ def print_total_cost(path: str = DEFAULT_LOG_PATH) -> None: Args: log_folder (str, optional): Cost logs directory. Defaults to DEFAULT_LOG_PATH. + This method reads all the files in the specified directory. """ - print(f"Total cost: {round(OpenAICostTrackerViz.get_total_cost(path), 6)} (USD)") + print(f"Total cost: {round(OpenAICostLoggerViz.get_total_cost(path), 6)} (USD)") @staticmethod def get_total_cost_by_model(path: str = DEFAULT_LOG_PATH) -> Dict[str, float]: @@ -44,6 +45,7 @@ def get_total_cost_by_model(path: str = DEFAULT_LOG_PATH) -> Dict[str, float]: Args: log_folder (str, optional): Cost logs directory. Defaults to DEFAULT_LOG_PATH. + This method reads all the files in the specified directory. Returns: Dict[str, float]: the total cost by model. @@ -64,8 +66,9 @@ def print_total_cost_by_model(path: str = DEFAULT_LOG_PATH) -> None: Args: log_folder (str, optional): Cost logs directory. Defaults to DEFAULT_LOG_PATH. + This method reads all the files in the specified directory. """ - cost_by_model = OpenAICostTrackerViz.get_total_cost_by_model(path) + cost_by_model = OpenAICostLoggerViz.get_total_cost_by_model(path) for model, cost in cost_by_model.items(): print(f"{model}: {round(cost, 6)} (USD)") @@ -75,6 +78,7 @@ def plot_cost_by_day(path: str = DEFAULT_LOG_PATH, last_n_days: int = None) -> N Args: path (str, optional): Cost logs directory. Defaults to DEFAULT_LOG_PATH. + This method reads all the files in the specified directory. last_n_days (int, optional): The number of last days to plot. Defaults to None. """ cost_by_day = defaultdict(float) diff --git a/requirements.txt b/requirements.txt index c699b15..918b143 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ openai==1.13.3 -matplotlib \ No newline at end of file +matplotlib==3.6.3 +pytest==7.4.2 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..47003ba --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup, find_packages +import os + +# Read the README file (reStructuredText format) +with open('README.rst') as f: + long_description = f.read() + +# Get the version number from the environment +version_number = os.getenv('VERSION_NUMBER') +version_number = version_number.strip("v") + +setup( + name='openai_cost_logger', + version=version_number, + author='Lorenzo Drudi | Mikolaj Boronski | Ivan Zakazov', + description='OpenAI Cost Logger', + author_email='lorenzodrudi11@gmail.com', + url='https://github.com/drudilorenzo/openai-cost-tracker', + long_description=long_description, + long_description_content_type='text/x-rst', + keywords=['openai', 'cost', 'logger', 'tracker'], + license='MIT', + packages=find_packages(include=['openai_cost_logger', 'openai_cost_logger.*']), + requires=['openai', 'pandas', 'matplotlib'], + install_requires=['openai', 'pandas', 'matplotlib'] +) \ No newline at end of file