├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .github └── workflows │ ├── integration-tests.yml │ ├── pre-commit.yaml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── README.md ├── __init__.py ├── conftest.py ├── poetry.lock ├── promptlayer ├── __init__.py ├── groups │ ├── __init__.py │ └── groups.py ├── promptlayer.py ├── promptlayer_base.py ├── promptlayer_mixins.py ├── span_exporter.py ├── templates.py ├── track │ ├── __init__.py │ └── track.py ├── types │ ├── __init__.py │ ├── prompt_template.py │ └── request_log.py └── utils.py ├── pyproject.toml └── tests ├── fixtures ├── __init__.py ├── auth.py ├── cassettes │ ├── test_anthropic_chat_completion.yaml │ ├── test_anthropic_chat_completion_async.yaml │ ├── test_anthropic_chat_completion_async_stream_with_pl_id.yaml │ ├── test_anthropic_chat_completion_with_pl_id.yaml │ ├── test_anthropic_chat_completion_with_stream.yaml │ ├── test_anthropic_chat_completion_with_stream_and_pl_id.yaml │ ├── test_arun_workflow_request.yaml │ ├── test_get_all_templates.yaml │ ├── test_get_final_output_1.yaml │ ├── test_get_final_output_2.yaml │ ├── test_get_prompt_template_provider_base_url_name.yaml │ ├── test_get_template_async.yaml │ ├── test_log_request_async.yaml │ ├── test_make_message_listener_1.yaml │ ├── test_make_message_listener_2.yaml │ ├── test_openai_chat_completion.yaml │ ├── test_openai_chat_completion_async.yaml │ ├── test_openai_chat_completion_async_stream_with_pl_id.yaml │ ├── test_openai_chat_completion_with_pl_id.yaml │ ├── test_openai_chat_completion_with_stream.yaml │ ├── test_openai_chat_completion_with_stream_and_pl_id.yaml │ ├── test_publish_template_async.yaml │ ├── test_run_prompt_async.yaml │ └── test_track_and_templates.yaml ├── clients.py ├── setup.py ├── templates.py └── workflow_update_messages.py ├── test_agents ├── __init__.py ├── test_arun_workflow_request.py └── test_misc.py ├── test_anthropic_proxy.py ├── test_get_prompt_template.py ├── test_openai_proxy.py ├── test_promptlayer_run.py ├── test_templates_groups_track.py └── utils ├── mocks.py └── vcr.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:1-3.9-bullseye", 7 | "features": { 8 | "ghcr.io/devcontainers-contrib/features/poetry:2": {} 9 | }, 10 | "customizations": { 11 | "vscode": { 12 | "extensions": [ 13 | "ms-python.python", 14 | "charliermarsh.ruff" 15 | ], 16 | "settings": { 17 | "editor.formatOnSave": true, 18 | "python.analysis.autoImportCompletions": true, 19 | "[python]": { 20 | "editor.defaultFormatter": "charliermarsh.ruff" 21 | }, 22 | // TODO(dmu) HIGH: Make linter configuration consistent with .pre-commit-config.yaml 23 | "ruff.organizeImports": true 24 | } 25 | } 26 | }, 27 | 28 | // Features to add to the dev container. More info: https://containers.dev/features. 29 | // "features": {}, 30 | 31 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 32 | // "forwardPorts": [], 33 | 34 | // Use 'postCreateCommand' to run commands after the container is created. 35 | "postCreateCommand": "poetry install" 36 | 37 | // Configure tool-specific properties. 38 | // "customizations": {}, 39 | 40 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 41 | // "remoteUser": "root", 42 | } 43 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # See https://editorconfig.org/ for more info about this file 2 | [*.py] 3 | max_line_length = 120 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | push: 5 | branches: [$default-branch] 6 | 7 | workflow_dispatch: 8 | 9 | env: 10 | PROMPTLAYER_API_KEY: ${{ secrets.PROMPTLAYER_API_KEY }} 11 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 12 | 13 | jobs: 14 | integration-tests: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Python 20 | uses: actions/setup-python@v3 21 | with: 22 | python-version: "3.x" 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | python -m pip install behave openai 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Run Integration Tests 29 | run: behave 30 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: [push] 4 | 5 | jobs: 6 | pre-commit: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Setup Python 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.x" 15 | cache: "pip" 16 | 17 | - name: Install pre-commit 18 | run: pip install pre-commit 19 | 20 | - name: Cache pre-commit 21 | uses: actions/cache@v3 22 | with: 23 | path: ~/.cache/pre-commit 24 | key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 25 | 26 | - name: Run pre-commit hooks 27 | run: pre-commit run -a 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.9" 19 | 20 | - name: Install poetry 21 | run: pipx install poetry 22 | 23 | - name: Running poetry install 24 | run: poetry install 25 | 26 | - name: Build and publish 27 | env: 28 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_PASSWORD }} 29 | run: poetry publish --build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pycharm 129 | .idea 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | .test/ 135 | plvenv 136 | test.py 137 | 138 | # VSCode 139 | .vscode/ 140 | 141 | # Internal development 142 | /samples 143 | testing_* 144 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | exclude: (app/utils/evaluate/(javascript_code_wrapper|python_code_wrapper)\.txt|langchain_prompts\.txt) 7 | - id: trailing-whitespace 8 | - repo: https://github.com/astral-sh/ruff-pre-commit 9 | rev: v0.9.4 10 | hooks: 11 | - id: ruff 12 | name: 'ruff: fix imports' 13 | args: ["--select", "I", "--fix"] 14 | - id: ruff 15 | - id: ruff-format 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RUN_TEST := test -f .env && set -a; . ./.env; set +a; poetry run pytest 2 | 3 | .PHONY: lint 4 | lint: 5 | poetry run pre-commit run --all-files 6 | 7 | .PHONY: test 8 | test: 9 | ${RUN_TEST} 10 | 11 | .PHONY: test-sw 12 | test-sw: 13 | ${RUN_TEST} -vv --sw --show-capture=no 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🍰 PromptLayer 4 | 5 | **The first platform built for prompt engineers** 6 | 7 | Python 8 | Docs 9 | Demo with Loom 10 | 11 | --- 12 | 13 |
14 | 15 | [PromptLayer](https://promptlayer.com/) is the first platform that allows you to track, manage, and share your GPT prompt engineering. PromptLayer acts a middleware between your code and OpenAI’s python library. 16 | 17 | PromptLayer records all your OpenAI API requests, allowing you to search and explore request history in the PromptLayer dashboard. 18 | 19 | This repo contains the Python wrapper library for PromptLayer. 20 | 21 | ## Quickstart ⚡ 22 | 23 | ### Install PromptLayer 24 | 25 | ```bash 26 | pip install promptlayer 27 | ``` 28 | 29 | ### Installing PromptLayer Locally 30 | 31 | Use `pip install .` to install locally. 32 | 33 | ### Using PromptLayer 34 | 35 | To get started, create an account by clicking “*Log in*” on [PromptLayer](https://promptlayer.com/). Once logged in, click the button to create an API key and save this in a secure location ([Guide to Using Env Vars](https://towardsdatascience.com/the-quick-guide-to-using-environment-variables-in-python-d4ec9291619e)). 36 | 37 | Once you have that all set up, [install PromptLayer using](https://pypi.org/project/promptlayer/) `pip`. 38 | 39 | In the Python file where you use OpenAI APIs, add the following. This allows us to keep track of your requests without needing any other code changes. 40 | 41 | ```python 42 | from promptlayer import PromptLayer 43 | 44 | promptlayer = PromptLayer(api_key="") 45 | openai = promptlayer.openai 46 | ``` 47 | 48 | **You can then use `openai` as you would if you had imported it directly.** 49 | 50 | 53 | 54 | ### Adding PromptLayer tags: `pl_tags` 55 | 56 | PromptLayer allows you to add tags through the `pl_tags` argument. This allows you to track and group requests in the dashboard. 57 | 58 | *Tags are not required but we recommend them!* 59 | 60 | ```python 61 | openai.Completion.create( 62 | engine="text-ada-001", 63 | prompt="My name is", 64 | pl_tags=["name-guessing", "pipeline-2"] 65 | ) 66 | ``` 67 | 68 | After making your first few requests, you should be able to see them in the PromptLayer dashboard! 69 | 70 | ## Using the REST API 71 | 72 | This Python library is a wrapper over PromptLayer's REST API. If you use another language, like Javascript, just interact directly with the API. 73 | 74 | Here is an example request below: 75 | 76 | ```python 77 | import requests 78 | request_response = requests.post( 79 | "https://api.promptlayer.com/track-request", 80 | json={ 81 | "function_name": "openai.Completion.create", 82 | "args": [], 83 | "kwargs": {"engine": "text-ada-001", "prompt": "My name is"}, 84 | "tags": ["hello", "world"], 85 | "request_response": {"id": "cmpl-6TEeJCRVlqQSQqhD8CYKd1HdCcFxM", "object": "text_completion", "created": 1672425843, "model": "text-ada-001", "choices": [{"text": " advocacy\"\n\nMy name is advocacy.", "index": 0, "logprobs": None, "finish_reason": "stop"}]}, 86 | "request_start_time": 1673987077.463504, 87 | "request_end_time": 1673987077.463504, 88 | "api_key": "pl_", 89 | }, 90 | ) 91 | ``` 92 | 93 | ## Contributing 94 | 95 | We welcome contributions to our open source project, including new features, infrastructure improvements, and better documentation. For more information or any questions, contact us at [hello@promptlayer.com](mailto:hello@promptlayer.com). 96 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # TODO(dmu) LOW: This file seems unnecessary. Consider removal 2 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | # This file need to be in the root of repo, so it is imported before tests 2 | import pytest 3 | 4 | # we need this to get assert diffs everywhere `tests.*`, it must execute before importing `tests` 5 | pytest.register_assert_rewrite("tests") 6 | 7 | from tests.fixtures import * # noqa: F401, F403, E402 8 | -------------------------------------------------------------------------------- /promptlayer/__init__.py: -------------------------------------------------------------------------------- 1 | from .promptlayer import AsyncPromptLayer, PromptLayer 2 | 3 | __version__ = "1.0.50" 4 | __all__ = ["PromptLayer", "AsyncPromptLayer", "__version__"] 5 | -------------------------------------------------------------------------------- /promptlayer/groups/__init__.py: -------------------------------------------------------------------------------- 1 | from promptlayer.groups.groups import acreate, create 2 | 3 | 4 | class GroupManager: 5 | def __init__(self, api_key: str): 6 | self.api_key = api_key 7 | 8 | def create(self): 9 | return create(self.api_key) 10 | 11 | 12 | class AsyncGroupManager: 13 | def __init__(self, api_key: str): 14 | self.api_key = api_key 15 | 16 | async def create(self) -> str: 17 | return await acreate(self.api_key) 18 | 19 | 20 | __all__ = ["GroupManager", "AsyncGroupManager"] 21 | -------------------------------------------------------------------------------- /promptlayer/groups/groups.py: -------------------------------------------------------------------------------- 1 | from promptlayer.utils import apromptlayer_create_group, promptlayer_create_group 2 | 3 | 4 | def create(api_key: str = None): 5 | """Create a new group.""" 6 | return promptlayer_create_group(api_key) 7 | 8 | 9 | async def acreate(api_key: str = None) -> str: 10 | """Asynchronously create a new group.""" 11 | return await apromptlayer_create_group(api_key) 12 | -------------------------------------------------------------------------------- /promptlayer/promptlayer_base.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import inspect 3 | import re 4 | 5 | from promptlayer.utils import async_wrapper, promptlayer_api_handler 6 | 7 | 8 | class PromptLayerBase(object): 9 | __slots__ = [ 10 | "_obj", 11 | "__weakref__", 12 | "_function_name", 13 | "_provider_type", 14 | "_api_key", 15 | "_tracer", 16 | ] 17 | 18 | def __init__(self, obj, function_name="", provider_type="openai", api_key=None, tracer=None): 19 | object.__setattr__(self, "_obj", obj) 20 | object.__setattr__(self, "_function_name", function_name) 21 | object.__setattr__(self, "_provider_type", provider_type) 22 | object.__setattr__(self, "_api_key", api_key) 23 | object.__setattr__(self, "_tracer", tracer) 24 | 25 | def __getattr__(self, name): 26 | attr = getattr(object.__getattribute__(self, "_obj"), name) 27 | 28 | if ( 29 | name != "count_tokens" # fix for anthropic count_tokens 30 | and not re.match(r"", str(attr)) # fix for anthropic errors 31 | and not re.match(r"", str(attr)) # fix for openai errors 32 | and ( 33 | inspect.isclass(attr) 34 | or inspect.isfunction(attr) 35 | or inspect.ismethod(attr) 36 | or str(type(attr)) == "" 37 | or str(type(attr)) == "" 38 | or str(type(attr)) == "" 39 | or str(type(attr)) == "" 40 | or re.match(r"", str(type(attr))) 41 | ) 42 | ): 43 | return PromptLayerBase( 44 | attr, 45 | function_name=f"{object.__getattribute__(self, '_function_name')}.{name}", 46 | provider_type=object.__getattribute__(self, "_provider_type"), 47 | api_key=object.__getattribute__(self, "_api_key"), 48 | tracer=object.__getattribute__(self, "_tracer"), 49 | ) 50 | return attr 51 | 52 | def __delattr__(self, name): 53 | delattr(object.__getattribute__(self, "_obj"), name) 54 | 55 | def __setattr__(self, name, value): 56 | setattr(object.__getattribute__(self, "_obj"), name, value) 57 | 58 | def __call__(self, *args, **kwargs): 59 | tags = kwargs.pop("pl_tags", None) 60 | if tags is not None and not isinstance(tags, list): 61 | raise Exception("pl_tags must be a list of strings.") 62 | 63 | return_pl_id = kwargs.pop("return_pl_id", False) 64 | request_start_time = datetime.datetime.now().timestamp() 65 | function_object = object.__getattribute__(self, "_obj") 66 | tracer = object.__getattribute__(self, "_tracer") 67 | function_name = object.__getattribute__(self, "_function_name") 68 | 69 | if tracer: 70 | with tracer.start_as_current_span(function_name) as llm_request_span: 71 | llm_request_span_id = hex(llm_request_span.context.span_id)[2:].zfill(16) 72 | llm_request_span.set_attribute("provider", object.__getattribute__(self, "_provider_type")) 73 | llm_request_span.set_attribute("function_name", function_name) 74 | llm_request_span.set_attribute("function_input", str({"args": args, "kwargs": kwargs})) 75 | 76 | if inspect.isclass(function_object): 77 | result = PromptLayerBase( 78 | function_object(*args, **kwargs), 79 | function_name=function_name, 80 | provider_type=object.__getattribute__(self, "_provider_type"), 81 | api_key=object.__getattribute__(self, "_api_key"), 82 | tracer=tracer, 83 | ) 84 | llm_request_span.set_attribute("function_output", str(result)) 85 | return result 86 | 87 | function_response = function_object(*args, **kwargs) 88 | 89 | if inspect.iscoroutinefunction(function_object) or inspect.iscoroutine(function_response): 90 | return async_wrapper( 91 | function_response, 92 | return_pl_id, 93 | request_start_time, 94 | function_name, 95 | object.__getattribute__(self, "_provider_type"), 96 | tags, 97 | api_key=object.__getattribute__(self, "_api_key"), 98 | llm_request_span_id=llm_request_span_id, 99 | tracer=tracer, # Pass the tracer to async_wrapper 100 | *args, 101 | **kwargs, 102 | ) 103 | 104 | request_end_time = datetime.datetime.now().timestamp() 105 | result = promptlayer_api_handler( 106 | function_name, 107 | object.__getattribute__(self, "_provider_type"), 108 | args, 109 | kwargs, 110 | tags, 111 | function_response, 112 | request_start_time, 113 | request_end_time, 114 | object.__getattribute__(self, "_api_key"), 115 | return_pl_id=return_pl_id, 116 | llm_request_span_id=llm_request_span_id, 117 | ) 118 | llm_request_span.set_attribute("function_output", str(result)) 119 | return result 120 | else: 121 | # Without tracing 122 | if inspect.isclass(function_object): 123 | return PromptLayerBase( 124 | function_object(*args, **kwargs), 125 | function_name=function_name, 126 | provider_type=object.__getattribute__(self, "_provider_type"), 127 | api_key=object.__getattribute__(self, "_api_key"), 128 | ) 129 | 130 | function_response = function_object(*args, **kwargs) 131 | 132 | if inspect.iscoroutinefunction(function_object) or inspect.iscoroutine(function_response): 133 | return async_wrapper( 134 | function_response, 135 | return_pl_id, 136 | request_start_time, 137 | function_name, 138 | object.__getattribute__(self, "_provider_type"), 139 | tags, 140 | api_key=object.__getattribute__(self, "_api_key"), 141 | *args, 142 | **kwargs, 143 | ) 144 | 145 | request_end_time = datetime.datetime.now().timestamp() 146 | return promptlayer_api_handler( 147 | function_name, 148 | object.__getattribute__(self, "_provider_type"), 149 | args, 150 | kwargs, 151 | tags, 152 | function_response, 153 | request_start_time, 154 | request_end_time, 155 | object.__getattribute__(self, "_api_key"), 156 | return_pl_id=return_pl_id, 157 | ) 158 | -------------------------------------------------------------------------------- /promptlayer/promptlayer_mixins.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | from copy import deepcopy 4 | from functools import wraps 5 | from typing import Dict, Union 6 | 7 | from opentelemetry.sdk.resources import Resource 8 | from opentelemetry.sdk.trace import TracerProvider 9 | from opentelemetry.sdk.trace.export import BatchSpanProcessor 10 | from opentelemetry.semconv.resource import ResourceAttributes 11 | 12 | from promptlayer.span_exporter import PromptLayerSpanExporter 13 | from promptlayer.utils import ( 14 | aanthropic_request, 15 | aanthropic_stream_completion, 16 | aanthropic_stream_message, 17 | aazure_openai_request, 18 | agoogle_request, 19 | agoogle_stream_chat, 20 | agoogle_stream_completion, 21 | amistral_request, 22 | amistral_stream_chat, 23 | anthropic_request, 24 | anthropic_stream_completion, 25 | anthropic_stream_message, 26 | aopenai_request, 27 | aopenai_stream_chat, 28 | aopenai_stream_completion, 29 | azure_openai_request, 30 | google_request, 31 | google_stream_chat, 32 | google_stream_completion, 33 | mistral_request, 34 | mistral_stream_chat, 35 | openai_request, 36 | openai_stream_chat, 37 | openai_stream_completion, 38 | ) 39 | 40 | MAP_PROVIDER_TO_FUNCTION_NAME = { 41 | "openai": { 42 | "chat": { 43 | "function_name": "openai.chat.completions.create", 44 | "stream_function": openai_stream_chat, 45 | }, 46 | "completion": { 47 | "function_name": "openai.completions.create", 48 | "stream_function": openai_stream_completion, 49 | }, 50 | }, 51 | "anthropic": { 52 | "chat": { 53 | "function_name": "anthropic.messages.create", 54 | "stream_function": anthropic_stream_message, 55 | }, 56 | "completion": { 57 | "function_name": "anthropic.completions.create", 58 | "stream_function": anthropic_stream_completion, 59 | }, 60 | }, 61 | "openai.azure": { 62 | "chat": { 63 | "function_name": "openai.AzureOpenAI.chat.completions.create", 64 | "stream_function": openai_stream_chat, 65 | }, 66 | "completion": { 67 | "function_name": "openai.AzureOpenAI.completions.create", 68 | "stream_function": openai_stream_completion, 69 | }, 70 | }, 71 | "mistral": { 72 | "chat": { 73 | "function_name": "mistral.client.chat", 74 | "stream_function": mistral_stream_chat, 75 | }, 76 | "completion": { 77 | "function_name": None, 78 | "stream_function": None, 79 | }, 80 | }, 81 | "google": { 82 | "chat": { 83 | "function_name": "google.convo.send_message", 84 | "stream_function": google_stream_chat, 85 | }, 86 | "completion": { 87 | "function_name": "google.model.generate_content", 88 | "stream_function": google_stream_completion, 89 | }, 90 | }, 91 | } 92 | 93 | 94 | MAP_PROVIDER_TO_FUNCTION = { 95 | "openai": openai_request, 96 | "anthropic": anthropic_request, 97 | "openai.azure": azure_openai_request, 98 | "mistral": mistral_request, 99 | "google": google_request, 100 | } 101 | 102 | AMAP_PROVIDER_TO_FUNCTION_NAME = { 103 | "openai": { 104 | "chat": { 105 | "function_name": "openai.chat.completions.create", 106 | "stream_function": aopenai_stream_chat, 107 | }, 108 | "completion": { 109 | "function_name": "openai.completions.create", 110 | "stream_function": aopenai_stream_completion, 111 | }, 112 | }, 113 | "anthropic": { 114 | "chat": { 115 | "function_name": "anthropic.messages.create", 116 | "stream_function": aanthropic_stream_message, 117 | }, 118 | "completion": { 119 | "function_name": "anthropic.completions.create", 120 | "stream_function": aanthropic_stream_completion, 121 | }, 122 | }, 123 | "openai.azure": { 124 | "chat": { 125 | "function_name": "openai.AzureOpenAI.chat.completions.create", 126 | "stream_function": aopenai_stream_chat, 127 | }, 128 | "completion": { 129 | "function_name": "openai.AzureOpenAI.completions.create", 130 | "stream_function": aopenai_stream_completion, 131 | }, 132 | }, 133 | "mistral": { 134 | "chat": { 135 | "function_name": "mistral.client.chat", 136 | "stream_function": amistral_stream_chat, 137 | }, 138 | "completion": { 139 | "function_name": None, 140 | "stream_function": None, 141 | }, 142 | }, 143 | "google": { 144 | "chat": { 145 | "function_name": "google.convo.send_message", 146 | "stream_function": agoogle_stream_chat, 147 | }, 148 | "completion": { 149 | "function_name": "google.model.generate_content", 150 | "stream_function": agoogle_stream_completion, 151 | }, 152 | }, 153 | } 154 | 155 | 156 | AMAP_PROVIDER_TO_FUNCTION = { 157 | "openai": aopenai_request, 158 | "anthropic": aanthropic_request, 159 | "openai.azure": aazure_openai_request, 160 | "mistral": amistral_request, 161 | "google": agoogle_request, 162 | } 163 | 164 | 165 | class PromptLayerMixin: 166 | @staticmethod 167 | def _initialize_tracer(api_key: str = None, enable_tracing: bool = False): 168 | if enable_tracing: 169 | resource = Resource(attributes={ResourceAttributes.SERVICE_NAME: "prompt-layer-library"}) 170 | tracer_provider = TracerProvider(resource=resource) 171 | promptlayer_exporter = PromptLayerSpanExporter(api_key=api_key) 172 | span_processor = BatchSpanProcessor(promptlayer_exporter) 173 | tracer_provider.add_span_processor(span_processor) 174 | tracer = tracer_provider.get_tracer(__name__) 175 | return tracer_provider, tracer 176 | else: 177 | return None, None 178 | 179 | @staticmethod 180 | def _prepare_get_prompt_template_params(*, prompt_version, prompt_release_label, input_variables, metadata): 181 | params = {} 182 | 183 | if prompt_version: 184 | params["version"] = prompt_version 185 | if prompt_release_label: 186 | params["label"] = prompt_release_label 187 | if input_variables: 188 | params["input_variables"] = input_variables 189 | if metadata: 190 | params["metadata_filters"] = metadata 191 | 192 | return params 193 | 194 | @staticmethod 195 | def _prepare_llm_request_params( 196 | *, 197 | prompt_blueprint, 198 | prompt_template, 199 | prompt_blueprint_model, 200 | model_parameter_overrides, 201 | stream, 202 | is_async=False, 203 | ): 204 | provider = prompt_blueprint_model["provider"] 205 | kwargs = deepcopy(prompt_blueprint["llm_kwargs"]) 206 | if is_async: 207 | config = AMAP_PROVIDER_TO_FUNCTION_NAME[provider][prompt_template["type"]] 208 | request_function = AMAP_PROVIDER_TO_FUNCTION[provider] 209 | else: 210 | config = MAP_PROVIDER_TO_FUNCTION_NAME[provider][prompt_template["type"]] 211 | request_function = MAP_PROVIDER_TO_FUNCTION[provider] 212 | 213 | if provider_base_url := prompt_blueprint.get("provider_base_url"): 214 | kwargs["base_url"] = provider_base_url["url"] 215 | 216 | if model_parameter_overrides: 217 | kwargs.update(model_parameter_overrides) 218 | 219 | kwargs["stream"] = stream 220 | if stream and provider in ["openai", "openai.azure"]: 221 | kwargs["stream_options"] = {"include_usage": True} 222 | 223 | return { 224 | "provider": provider, 225 | "function_name": config["function_name"], 226 | "stream_function": config["stream_function"], 227 | "request_function": request_function, 228 | "kwargs": kwargs, 229 | "prompt_blueprint": prompt_blueprint, 230 | } 231 | 232 | @staticmethod 233 | def _validate_and_extract_model_from_prompt_blueprint(*, prompt_blueprint, prompt_name): 234 | if not prompt_blueprint["llm_kwargs"]: 235 | raise ValueError( 236 | f"Prompt '{prompt_name}' does not have any LLM kwargs associated with it. Please set your model parameters in the registry in the PromptLayer dashbaord." 237 | ) 238 | 239 | prompt_blueprint_metadata = prompt_blueprint.get("metadata") 240 | 241 | if not prompt_blueprint_metadata: 242 | raise ValueError(f"Prompt '{prompt_name}' does not have any metadata associated with it.") 243 | 244 | prompt_blueprint_model = prompt_blueprint_metadata.get("model") 245 | 246 | if not prompt_blueprint_model: 247 | raise ValueError(f"Prompt '{prompt_name}' does not have a model parameters associated with it.") 248 | 249 | return prompt_blueprint_model 250 | 251 | @staticmethod 252 | def _prepare_track_request_kwargs( 253 | api_key, 254 | request_params, 255 | tags, 256 | input_variables, 257 | group_id, 258 | pl_run_span_id: Union[str, None] = None, 259 | metadata: Union[Dict[str, str], None] = None, 260 | **body, 261 | ): 262 | return { 263 | "function_name": request_params["function_name"], 264 | "provider_type": request_params["provider"], 265 | "args": [], 266 | "kwargs": request_params["kwargs"], 267 | "tags": tags, 268 | "request_start_time": datetime.datetime.now(datetime.timezone.utc).timestamp(), 269 | "request_end_time": datetime.datetime.now(datetime.timezone.utc).timestamp(), 270 | "api_key": api_key, 271 | "metadata": metadata, 272 | "prompt_id": request_params["prompt_blueprint"]["id"], 273 | "prompt_version": request_params["prompt_blueprint"]["version"], 274 | "prompt_input_variables": input_variables or {}, 275 | "group_id": group_id, 276 | "return_prompt_blueprint": True, 277 | "span_id": pl_run_span_id, 278 | **body, 279 | } 280 | 281 | def traceable(self, attributes=None, name=None): 282 | def decorator(func): 283 | @wraps(func) 284 | def sync_wrapper(*args, **kwargs): 285 | if self.tracer: 286 | span_name = name or func.__name__ 287 | with self.tracer.start_as_current_span(span_name) as span: 288 | if attributes: 289 | for key, value in attributes.items(): 290 | span.set_attribute(key, value) 291 | 292 | span.set_attribute("function_input", str({"args": args, "kwargs": kwargs})) 293 | result = func(*args, **kwargs) 294 | span.set_attribute("function_output", str(result)) 295 | 296 | return result 297 | else: 298 | return func(*args, **kwargs) 299 | 300 | @wraps(func) 301 | async def async_wrapper(*args, **kwargs): 302 | if self.tracer: 303 | span_name = name or func.__name__ 304 | with self.tracer.start_as_current_span(span_name) as span: 305 | if attributes: 306 | for key, value in attributes.items(): 307 | span.set_attribute(key, value) 308 | 309 | span.set_attribute("function_input", str({"args": args, "kwargs": kwargs})) 310 | result = await func(*args, **kwargs) 311 | span.set_attribute("function_output", str(result)) 312 | 313 | return result 314 | else: 315 | return await func(*args, **kwargs) 316 | 317 | return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper 318 | 319 | return decorator 320 | -------------------------------------------------------------------------------- /promptlayer/span_exporter.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | 3 | import requests 4 | from opentelemetry.sdk.trace import ReadableSpan 5 | from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult 6 | 7 | from promptlayer.utils import URL_API_PROMPTLAYER 8 | 9 | 10 | class PromptLayerSpanExporter(SpanExporter): 11 | def __init__(self, api_key: str = None): 12 | self.api_key = api_key 13 | self.url = f"{URL_API_PROMPTLAYER}/spans-bulk" 14 | 15 | def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: 16 | request_data = [] 17 | 18 | for span in spans: 19 | span_info = { 20 | "name": span.name, 21 | "context": { 22 | "trace_id": hex(span.context.trace_id)[2:].zfill(32), # Ensure 32 characters 23 | "span_id": hex(span.context.span_id)[2:].zfill(16), # Ensure 16 characters 24 | "trace_state": str(span.context.trace_state), 25 | }, 26 | "kind": str(span.kind), 27 | "parent_id": hex(span.parent.span_id)[2:] if span.parent else None, 28 | "start_time": span.start_time, 29 | "end_time": span.end_time, 30 | "status": { 31 | "status_code": str(span.status.status_code), 32 | "description": span.status.description, 33 | }, 34 | "attributes": dict(span.attributes), 35 | "events": [ 36 | { 37 | "name": event.name, 38 | "timestamp": event.timestamp, 39 | "attributes": dict(event.attributes), 40 | } 41 | for event in span.events 42 | ], 43 | "links": [{"context": link.context, "attributes": dict(link.attributes)} for link in span.links], 44 | "resource": { 45 | "attributes": dict(span.resource.attributes), 46 | "schema_url": span.resource.schema_url, 47 | }, 48 | } 49 | request_data.append(span_info) 50 | 51 | try: 52 | response = requests.post( 53 | self.url, 54 | headers={"X-Api-Key": self.api_key, "Content-Type": "application/json"}, 55 | json={"spans": request_data}, 56 | ) 57 | response.raise_for_status() 58 | return SpanExportResult.SUCCESS 59 | except requests.RequestException: 60 | return SpanExportResult.FAILURE 61 | 62 | def shutdown(self): 63 | pass 64 | -------------------------------------------------------------------------------- /promptlayer/templates.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from promptlayer.types.prompt_template import GetPromptTemplate, PublishPromptTemplate 4 | from promptlayer.utils import ( 5 | aget_all_prompt_templates, 6 | aget_prompt_template, 7 | get_all_prompt_templates, 8 | get_prompt_template, 9 | publish_prompt_template, 10 | ) 11 | 12 | 13 | class TemplateManager: 14 | def __init__(self, api_key: str): 15 | self.api_key = api_key 16 | 17 | def get(self, prompt_name: str, params: Union[GetPromptTemplate, None] = None): 18 | return get_prompt_template(prompt_name, params, self.api_key) 19 | 20 | def publish(self, body: PublishPromptTemplate): 21 | return publish_prompt_template(body, self.api_key) 22 | 23 | def all(self, page: int = 1, per_page: int = 30): 24 | return get_all_prompt_templates(page, per_page, self.api_key) 25 | 26 | 27 | class AsyncTemplateManager: 28 | def __init__(self, api_key: str): 29 | self.api_key = api_key 30 | 31 | async def get(self, prompt_name: str, params: Union[GetPromptTemplate, None] = None): 32 | return await aget_prompt_template(prompt_name, params, self.api_key) 33 | 34 | async def all(self, page: int = 1, per_page: int = 30): 35 | return await aget_all_prompt_templates(page, per_page, self.api_key) 36 | -------------------------------------------------------------------------------- /promptlayer/track/__init__.py: -------------------------------------------------------------------------------- 1 | from promptlayer.track.track import ( 2 | agroup, 3 | ametadata, 4 | aprompt, 5 | ascore, 6 | group, 7 | metadata as metadata_, 8 | prompt, 9 | score as score_, 10 | ) 11 | 12 | # TODO(dmu) LOW: Move this code to another file 13 | 14 | 15 | class TrackManager: 16 | def __init__(self, api_key: str): 17 | self.api_key = api_key 18 | 19 | def group(self, request_id, group_id): 20 | return group(request_id, group_id, self.api_key) 21 | 22 | def metadata(self, request_id, metadata): 23 | return metadata_(request_id, metadata, self.api_key) 24 | 25 | def prompt(self, request_id, prompt_name, prompt_input_variables, version=None, label=None): 26 | return prompt( 27 | request_id, 28 | prompt_name, 29 | prompt_input_variables, 30 | version, 31 | label, 32 | self.api_key, 33 | ) 34 | 35 | def score(self, request_id, score, score_name=None): 36 | return score_(request_id, score, score_name, self.api_key) 37 | 38 | 39 | class AsyncTrackManager: 40 | def __init__(self, api_key: str): 41 | self.api_key = api_key 42 | 43 | async def group(self, request_id, group_id): 44 | return await agroup(request_id, group_id, self.api_key) 45 | 46 | async def metadata(self, request_id, metadata): 47 | return await ametadata(request_id, metadata, self.api_key) 48 | 49 | async def prompt(self, request_id, prompt_name, prompt_input_variables, version=None, label=None): 50 | return await aprompt( 51 | request_id, 52 | prompt_name, 53 | prompt_input_variables, 54 | version, 55 | label, 56 | self.api_key, 57 | ) 58 | 59 | async def score(self, request_id, score, score_name=None): 60 | return await ascore(request_id, score, score_name, self.api_key) 61 | 62 | 63 | __all__ = ["TrackManager"] 64 | -------------------------------------------------------------------------------- /promptlayer/track/track.py: -------------------------------------------------------------------------------- 1 | from promptlayer.utils import ( 2 | apromptlayer_track_group, 3 | apromptlayer_track_metadata, 4 | apromptlayer_track_prompt, 5 | apromptlayer_track_score, 6 | promptlayer_track_group, 7 | promptlayer_track_metadata, 8 | promptlayer_track_prompt, 9 | promptlayer_track_score, 10 | ) 11 | 12 | 13 | def prompt( 14 | request_id, 15 | prompt_name, 16 | prompt_input_variables, 17 | version=None, 18 | label=None, 19 | api_key: str = None, 20 | ): 21 | if not isinstance(prompt_input_variables, dict): 22 | raise Exception("Please provide a dictionary of input variables.") 23 | return promptlayer_track_prompt(request_id, prompt_name, prompt_input_variables, api_key, version, label) 24 | 25 | 26 | def metadata(request_id, metadata, api_key: str = None): 27 | if not isinstance(metadata, dict): 28 | raise Exception("Please provide a dictionary of metadata.") 29 | for key, value in metadata.items(): 30 | if not isinstance(key, str) or not isinstance(value, str): 31 | raise Exception("Please provide a dictionary of metadata with key value pair of strings.") 32 | return promptlayer_track_metadata(request_id, metadata, api_key) 33 | 34 | 35 | def score(request_id, score, score_name=None, api_key: str = None): 36 | if not isinstance(score, int): 37 | raise Exception("Please provide a int score.") 38 | if not isinstance(score_name, str) and score_name is not None: 39 | raise Exception("Please provide a string as score name.") 40 | if score < 0 or score > 100: 41 | raise Exception("Please provide a score between 0 and 100.") 42 | return promptlayer_track_score(request_id, score, score_name, api_key) 43 | 44 | 45 | def group(request_id, group_id, api_key: str = None): 46 | return promptlayer_track_group(request_id, group_id, api_key) 47 | 48 | 49 | async def aprompt( 50 | request_id, 51 | prompt_name, 52 | prompt_input_variables, 53 | version=None, 54 | label=None, 55 | api_key: str = None, 56 | ): 57 | if not isinstance(prompt_input_variables, dict): 58 | raise Exception("Please provide a dictionary of input variables.") 59 | return await apromptlayer_track_prompt(request_id, prompt_name, prompt_input_variables, api_key, version, label) 60 | 61 | 62 | async def ametadata(request_id, metadata, api_key: str = None): 63 | if not isinstance(metadata, dict): 64 | raise Exception("Please provide a dictionary of metadata.") 65 | for key, value in metadata.items(): 66 | if not isinstance(key, str) or not isinstance(value, str): 67 | raise Exception("Please provide a dictionary of metadata with key-value pairs of strings.") 68 | return await apromptlayer_track_metadata(request_id, metadata, api_key) 69 | 70 | 71 | async def ascore(request_id, score, score_name=None, api_key: str = None): 72 | if not isinstance(score, int): 73 | raise Exception("Please provide an integer score.") 74 | if not isinstance(score_name, str) and score_name is not None: 75 | raise Exception("Please provide a string as score name.") 76 | if score < 0 or score > 100: 77 | raise Exception("Please provide a score between 0 and 100.") 78 | return await apromptlayer_track_score(request_id, score, score_name, api_key) 79 | 80 | 81 | async def agroup(request_id, group_id, api_key: str = None): 82 | return await apromptlayer_track_group(request_id, group_id, api_key) 83 | -------------------------------------------------------------------------------- /promptlayer/types/__init__.py: -------------------------------------------------------------------------------- 1 | from . import prompt_template 2 | from .request_log import RequestLog 3 | 4 | __all__ = ["prompt_template", "RequestLog"] 5 | -------------------------------------------------------------------------------- /promptlayer/types/prompt_template.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Literal, Optional, Sequence, TypedDict, Union 2 | 3 | from typing_extensions import Required 4 | 5 | 6 | class GetPromptTemplate(TypedDict, total=False): 7 | version: int 8 | label: str 9 | provider: str 10 | input_variables: Dict[str, Any] 11 | metadata_filters: Dict[str, str] 12 | 13 | 14 | TemplateFormat = Literal["f-string", "jinja2"] 15 | 16 | 17 | class ImageUrl(TypedDict, total=False): 18 | url: str 19 | 20 | 21 | class TextContent(TypedDict, total=False): 22 | type: Literal["text"] 23 | text: str 24 | 25 | 26 | class ImageContent(TypedDict, total=False): 27 | type: Literal["image_url"] 28 | image_url: ImageUrl 29 | 30 | 31 | class Media(TypedDict, total=False): 32 | title: str 33 | type: str 34 | url: str 35 | 36 | 37 | class MediaContnt(TypedDict, total=False): 38 | type: Literal["media"] 39 | media: Media 40 | 41 | 42 | class MediaVariable(TypedDict, total=False): 43 | type: Literal["media_variable"] 44 | name: str 45 | 46 | 47 | Content = Union[TextContent, ImageContent, MediaContnt, MediaVariable] 48 | 49 | 50 | class Function(TypedDict, total=False): 51 | name: str 52 | description: str 53 | parameters: dict 54 | 55 | 56 | class Tool(TypedDict, total=False): 57 | type: Literal["function"] 58 | function: Function 59 | 60 | 61 | class FunctionCall(TypedDict, total=False): 62 | name: str 63 | arguments: str 64 | 65 | 66 | class SystemMessage(TypedDict, total=False): 67 | role: Literal["system"] 68 | input_variables: List[str] 69 | template_format: TemplateFormat 70 | content: Sequence[Content] 71 | name: str 72 | 73 | 74 | class UserMessage(TypedDict, total=False): 75 | role: Literal["user"] 76 | input_variables: List[str] 77 | template_format: TemplateFormat 78 | content: Sequence[Content] 79 | name: str 80 | 81 | 82 | class ToolCall(TypedDict, total=False): 83 | id: str 84 | type: Literal["function"] 85 | function: FunctionCall 86 | 87 | 88 | class AssistantMessage(TypedDict, total=False): 89 | role: Literal["assistant"] 90 | input_variables: List[str] 91 | template_format: TemplateFormat 92 | content: Sequence[Content] 93 | function_call: FunctionCall 94 | name: str 95 | tool_calls: List[ToolCall] 96 | 97 | 98 | class FunctionMessage(TypedDict, total=False): 99 | role: Literal["function"] 100 | input_variables: List[str] 101 | template_format: TemplateFormat 102 | content: Sequence[Content] 103 | name: str 104 | 105 | 106 | class ToolMessage(TypedDict, total=False): 107 | role: Literal["tool"] 108 | input_variables: List[str] 109 | template_format: TemplateFormat 110 | content: Sequence[Content] 111 | tool_call_id: str 112 | name: str 113 | 114 | 115 | class PlaceholderMessage(TypedDict, total=False): 116 | role: Literal["placeholder"] 117 | name: str 118 | 119 | 120 | class DeveloperMessage(TypedDict, total=False): 121 | role: Literal["developer"] 122 | input_variables: List[str] 123 | template_format: TemplateFormat 124 | content: Sequence[Content] 125 | 126 | 127 | class ChatFunctionCall(TypedDict, total=False): 128 | name: str 129 | 130 | 131 | class ChatToolChoice(TypedDict, total=False): 132 | type: Literal["function"] 133 | function: ChatFunctionCall 134 | 135 | 136 | ToolChoice = Union[str, ChatToolChoice] 137 | 138 | Message = Union[ 139 | SystemMessage, 140 | UserMessage, 141 | AssistantMessage, 142 | FunctionMessage, 143 | ToolMessage, 144 | PlaceholderMessage, 145 | DeveloperMessage, 146 | ] 147 | 148 | 149 | class CompletionPromptTemplate(TypedDict, total=False): 150 | type: Required[Literal["completion"]] 151 | template_format: TemplateFormat 152 | content: Sequence[Content] 153 | input_variables: List[str] 154 | 155 | 156 | class ChatPromptTemplate(TypedDict, total=False): 157 | type: Required[Literal["chat"]] 158 | messages: Required[Sequence[Message]] 159 | functions: Sequence[Function] 160 | function_call: Union[Literal["auto", "none"], ChatFunctionCall] 161 | input_variables: List[str] 162 | tools: Sequence[Tool] 163 | tool_choice: ToolChoice 164 | 165 | 166 | PromptTemplate = Union[CompletionPromptTemplate, ChatPromptTemplate] 167 | 168 | 169 | class Model(TypedDict, total=False): 170 | provider: Required[str] 171 | name: Required[str] 172 | parameters: Required[Dict[str, object]] 173 | 174 | 175 | class Metadata(TypedDict, total=False): 176 | model: Model 177 | 178 | 179 | class BasePromptTemplate(TypedDict, total=False): 180 | prompt_name: str 181 | tags: List[str] 182 | 183 | 184 | class PromptBlueprint(TypedDict, total=False): 185 | prompt_template: PromptTemplate 186 | commit_message: str 187 | metadata: Metadata 188 | 189 | 190 | class PublishPromptTemplate(BasePromptTemplate, PromptBlueprint, total=False): 191 | release_labels: Optional[List[str]] = None 192 | 193 | 194 | class BaseProviderBaseURL(TypedDict): 195 | name: Required[str] 196 | provider: Required[str] 197 | url: Required[str] 198 | 199 | 200 | class ProviderBaseURL(BaseProviderBaseURL): 201 | id: Required[int] 202 | 203 | 204 | class BasePromptTemplateResponse(TypedDict, total=False): 205 | id: Required[int] 206 | prompt_name: Required[str] 207 | tags: List[str] 208 | prompt_template: Required[PromptTemplate] 209 | commit_message: str 210 | metadata: Metadata 211 | provider_base_url: ProviderBaseURL 212 | 213 | 214 | a: BasePromptTemplateResponse = {"provider_base_url": {"url": ""}} 215 | 216 | 217 | class PublishPromptTemplateResponse(BasePromptTemplateResponse): 218 | pass 219 | 220 | 221 | class GetPromptTemplateResponse(BasePromptTemplateResponse): 222 | llm_kwargs: Union[Dict[str, object], None] 223 | version: int 224 | 225 | 226 | class ListPromptTemplateResponse(BasePromptTemplateResponse, total=False): 227 | version: int 228 | -------------------------------------------------------------------------------- /promptlayer/types/request_log.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict, Union 2 | 3 | from .prompt_template import PromptBlueprint 4 | 5 | 6 | class RequestLog(TypedDict): 7 | id: int 8 | prompt_version: Union[PromptBlueprint, None] 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "promptlayer" 3 | version = "1.0.50" 4 | description = "PromptLayer is a platform for prompt engineering and tracks your LLM requests." 5 | authors = ["Magniv "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.9,<4.0" 11 | requests = "^2.31.0" 12 | opentelemetry-api = "^1.26.0" 13 | opentelemetry-sdk = "^1.26.0" 14 | ably = "^2.0.11" 15 | aiohttp = "^3.10.10" 16 | httpx = "^0.28.1" 17 | nest-asyncio = "^1.6.0" 18 | 19 | [tool.poetry.group.dev.dependencies] 20 | behave = "^1.2.6" 21 | pytest = "^8.2.0" 22 | pytest-asyncio = "^0.23.6" 23 | openai = "^1.60.1" 24 | google-genai = "^1.5.0" 25 | anthropic = "0.49.0" 26 | # TODO(dmu) MEDIUM: Upgrade to vcrpy >= 7 once it supports urllib3 >= 2.2.2 27 | vcrpy = "<7.0.0" 28 | pytest-network = "^0.0.1" 29 | pytest-parametrize-cases = "^0.1.2" 30 | 31 | [build-system] 32 | requires = ["poetry-core"] 33 | build-backend = "poetry.core.masonry.api" 34 | 35 | [tool.ruff] 36 | line-length = 120 37 | indent-width = 4 # mimic Black 38 | target-version = "py38" 39 | 40 | [tool.ruff.lint] 41 | ignore = ["E501", "E711", "E712"] 42 | 43 | [tool.ruff.lint.isort] 44 | combine-as-imports = true 45 | relative-imports-order = "closest-to-furthest" 46 | known-first-party = ["promptlayer", "tests"] 47 | 48 | [tool.ruff.format] 49 | quote-style = "double" # mimic Black 50 | indent-style = "space" # also mimic Black 51 | skip-magic-trailing-comma = false # also mimic Black 52 | line-ending = "auto" # mimic Black 53 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import anthropic_api_key, headers, openai_api_key, promptlayer_api_key # noqa: F401 2 | from .clients import ( # noqa: F401 3 | anthropic_async_client, 4 | anthropic_client, 5 | openai_async_client, 6 | openai_client, 7 | promptlayer_async_client, 8 | promptlayer_client, 9 | ) 10 | from .setup import autouse_disable_network, setup # noqa: F401 11 | from .templates import sample_template_content, sample_template_name # noqa: F401 12 | from .workflow_update_messages import ( 13 | workflow_update_data_exceeds_size_limit, # noqa: F401 14 | workflow_update_data_no_result_code, # noqa: F401 15 | workflow_update_data_ok, # noqa: F401 16 | ) 17 | -------------------------------------------------------------------------------- /tests/fixtures/auth.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def promptlayer_api_key(): 8 | return os.environ.get("PROMPTLAYER_API_KEY", "pl_sanitized") 9 | 10 | 11 | @pytest.fixture 12 | def anthropic_api_key(): 13 | return os.environ.get("ANTHROPIC_API_KEY", "sk-ant-api03-sanitized") 14 | 15 | 16 | @pytest.fixture 17 | def openai_api_key(): 18 | return os.environ.get("OPENAI_API_KEY", "sk-sanitized") 19 | 20 | 21 | @pytest.fixture 22 | def headers(promptlayer_api_key): 23 | return {"X-API-KEY": promptlayer_api_key} 24 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_anthropic_chat_completion.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"max_tokens": 1024, "messages": [{"role": "user", "content": "What is 4 | the capital of the United States?"}], "model": "claude-3-haiku-20240307"}' 5 | headers: 6 | accept: 7 | - application/json 8 | accept-encoding: 9 | - gzip, deflate 10 | anthropic-version: 11 | - '2023-06-01' 12 | connection: 13 | - keep-alive 14 | content-length: 15 | - '136' 16 | content-type: 17 | - application/json 18 | host: 19 | - api.anthropic.com 20 | user-agent: 21 | - Anthropic/Python 0.49.0 22 | x-api-key: 23 | - sanitized 24 | x-stainless-arch: 25 | - x64 26 | x-stainless-async: 27 | - 'false' 28 | x-stainless-lang: 29 | - python 30 | x-stainless-os: 31 | - Linux 32 | x-stainless-package-version: 33 | - 0.49.0 34 | x-stainless-read-timeout: 35 | - '600' 36 | x-stainless-retry-count: 37 | - '0' 38 | x-stainless-runtime: 39 | - CPython 40 | x-stainless-runtime-version: 41 | - 3.9.21 42 | x-stainless-timeout: 43 | - '600' 44 | method: POST 45 | uri: https://api.anthropic.com/v1/messages 46 | response: 47 | body: 48 | string: '{"id":"msg_01TYd9en4fxUtB11teUYmYqv","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[{"type":"text","text":"The 49 | capital of the United States is Washington, D.C."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":16,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":16}}' 50 | headers: 51 | CF-RAY: 52 | - 9302be57b990f136-DME 53 | Connection: 54 | - keep-alive 55 | Content-Type: 56 | - application/json 57 | Date: 58 | - Mon, 14 Apr 2025 11:08:08 GMT 59 | Server: 60 | - cloudflare 61 | Transfer-Encoding: 62 | - chunked 63 | X-Robots-Tag: 64 | - none 65 | anthropic-organization-id: 66 | - ec8f80fa-3527-4d15-8ab4-526e6a1f4899 67 | cf-cache-status: 68 | - DYNAMIC 69 | content-length: 70 | - '350' 71 | request-id: 72 | - req_014E9E6P3Z2CNv265MwqWKmQ 73 | via: 74 | - 1.1 google 75 | status: 76 | code: 200 77 | message: OK 78 | - request: 79 | body: '{"function_name": "anthropic.Anthropic.messages.create", "provider_type": 80 | "anthropic", "args": [], "kwargs": {"max_tokens": 1024, "messages": [{"role": 81 | "user", "content": "What is the capital of the United States?"}], "model": "claude-3-haiku-20240307"}, 82 | "tags": null, "request_response": {"id": "msg_01TYd9en4fxUtB11teUYmYqv", "content": 83 | [{"citations": null, "text": "The capital of the United States is Washington, 84 | D.C.", "type": "text"}], "model": "claude-3-haiku-20240307", "role": "assistant", 85 | "stop_reason": "end_turn", "stop_sequence": null, "type": "message", "usage": 86 | {"cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "input_tokens": 87 | 16, "output_tokens": 16}}, "request_start_time": 1744628887.82317, "request_end_time": 88 | 1744628888.853475, "metadata": null, "span_id": null, "api_key": "sanitized"}' 89 | headers: 90 | Accept: 91 | - '*/*' 92 | Accept-Encoding: 93 | - gzip, deflate 94 | Connection: 95 | - keep-alive 96 | Content-Length: 97 | - '848' 98 | Content-Type: 99 | - application/json 100 | User-Agent: 101 | - python-requests/2.31.0 102 | method: POST 103 | uri: http://localhost:8000/track-request 104 | response: 105 | body: 106 | string: '{"success":true,"request_id":133,"prompt_blueprint":null,"message":"Request 107 | tracked successfully"} 108 | 109 | ' 110 | headers: 111 | Connection: 112 | - close 113 | Content-Length: 114 | - '99' 115 | Content-Type: 116 | - application/json 117 | Date: 118 | - Mon, 14 Apr 2025 11:08:08 GMT 119 | Server: 120 | - gunicorn 121 | status: 122 | code: 200 123 | message: OK 124 | version: 1 125 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_anthropic_chat_completion_async.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"max_tokens": 1024, "messages": [{"role": "user", "content": "What is 4 | the capital of Spain?"}], "model": "claude-3-haiku-20240307"}' 5 | headers: 6 | accept: 7 | - application/json 8 | accept-encoding: 9 | - gzip, deflate 10 | anthropic-version: 11 | - '2023-06-01' 12 | connection: 13 | - keep-alive 14 | content-length: 15 | - '124' 16 | content-type: 17 | - application/json 18 | host: 19 | - api.anthropic.com 20 | user-agent: 21 | - AsyncAnthropic/Python 0.49.0 22 | x-api-key: 23 | - sanitized 24 | x-stainless-arch: 25 | - x64 26 | x-stainless-async: 27 | - async:asyncio 28 | x-stainless-lang: 29 | - python 30 | x-stainless-os: 31 | - Linux 32 | x-stainless-package-version: 33 | - 0.49.0 34 | x-stainless-read-timeout: 35 | - '600' 36 | x-stainless-retry-count: 37 | - '0' 38 | x-stainless-runtime: 39 | - CPython 40 | x-stainless-runtime-version: 41 | - 3.9.21 42 | x-stainless-timeout: 43 | - '600' 44 | method: POST 45 | uri: https://api.anthropic.com/v1/messages 46 | response: 47 | body: 48 | string: '{"id":"msg_0196dGvnCAKhToSuS6jPSoNb","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[{"type":"text","text":"The 49 | capital of Spain is Madrid."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}}' 50 | headers: 51 | CF-RAY: 52 | - 9302cb474e6cec4b-DME 53 | Connection: 54 | - keep-alive 55 | Content-Type: 56 | - application/json 57 | Date: 58 | - Mon, 14 Apr 2025 11:16:58 GMT 59 | Server: 60 | - cloudflare 61 | Transfer-Encoding: 62 | - chunked 63 | X-Robots-Tag: 64 | - none 65 | anthropic-organization-id: 66 | - ec8f80fa-3527-4d15-8ab4-526e6a1f4899 67 | cf-cache-status: 68 | - DYNAMIC 69 | content-length: 70 | - '329' 71 | request-id: 72 | - req_018YSc9VH7zK3pRPS7rfNcN9 73 | via: 74 | - 1.1 google 75 | status: 76 | code: 200 77 | message: OK 78 | - request: 79 | body: '{"function_name": "anthropic.AsyncAnthropic.messages.create", "provider_type": 80 | "anthropic", "args": [], "kwargs": {"max_tokens": 1024, "messages": [{"role": 81 | "user", "content": "What is the capital of Spain?"}], "model": "claude-3-haiku-20240307"}, 82 | "tags": null, "request_response": {"id": "msg_0196dGvnCAKhToSuS6jPSoNb", "content": 83 | [{"citations": null, "text": "The capital of Spain is Madrid.", "type": "text"}], 84 | "model": "claude-3-haiku-20240307", "role": "assistant", "stop_reason": "end_turn", 85 | "stop_sequence": null, "type": "message", "usage": {"cache_creation_input_tokens": 86 | 0, "cache_read_input_tokens": 0, "input_tokens": 14, "output_tokens": 10}}, 87 | "request_start_time": 1744629417.539952, "request_end_time": 1744629418.695542, 88 | "metadata": null, "span_id": null, "api_key": "sanitized"}' 89 | headers: 90 | Accept: 91 | - '*/*' 92 | Accept-Encoding: 93 | - gzip, deflate 94 | Connection: 95 | - keep-alive 96 | Content-Length: 97 | - '821' 98 | Content-Type: 99 | - application/json 100 | User-Agent: 101 | - python-requests/2.31.0 102 | method: POST 103 | uri: http://localhost:8000/track-request 104 | response: 105 | body: 106 | string: '{"success":true,"request_id":137,"prompt_blueprint":null,"message":"Request 107 | tracked successfully"} 108 | 109 | ' 110 | headers: 111 | Connection: 112 | - close 113 | Content-Length: 114 | - '99' 115 | Content-Type: 116 | - application/json 117 | Date: 118 | - Mon, 14 Apr 2025 11:16:58 GMT 119 | Server: 120 | - gunicorn 121 | status: 122 | code: 200 123 | message: OK 124 | version: 1 125 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_anthropic_chat_completion_async_stream_with_pl_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"max_tokens": 1024, "messages": [{"role": "user", "content": "What is 4 | the capital of Japan?"}], "model": "claude-3-haiku-20240307", "stream": true}' 5 | headers: 6 | accept: 7 | - application/json 8 | accept-encoding: 9 | - gzip, deflate 10 | anthropic-version: 11 | - '2023-06-01' 12 | connection: 13 | - keep-alive 14 | content-length: 15 | - '138' 16 | content-type: 17 | - application/json 18 | host: 19 | - api.anthropic.com 20 | user-agent: 21 | - AsyncAnthropic/Python 0.49.0 22 | x-api-key: 23 | - sanitized 24 | x-stainless-arch: 25 | - x64 26 | x-stainless-async: 27 | - async:asyncio 28 | x-stainless-lang: 29 | - python 30 | x-stainless-os: 31 | - Linux 32 | x-stainless-package-version: 33 | - 0.49.0 34 | x-stainless-read-timeout: 35 | - '600' 36 | x-stainless-retry-count: 37 | - '0' 38 | x-stainless-runtime: 39 | - CPython 40 | x-stainless-runtime-version: 41 | - 3.9.21 42 | x-stainless-timeout: 43 | - NOT_GIVEN 44 | method: POST 45 | uri: https://api.anthropic.com/v1/messages 46 | response: 47 | body: 48 | string: 'event: message_start 49 | 50 | data: {"type":"message_start","message":{"id":"msg_01Bi6S5crUgtL7PUCuYc8Vy6","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4}} } 51 | 52 | 53 | event: content_block_start 54 | 55 | data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } 56 | 57 | 58 | event: ping 59 | 60 | data: {"type": "ping"} 61 | 62 | 63 | event: content_block_delta 64 | 65 | data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The 66 | capital of Japan"} } 67 | 68 | 69 | event: content_block_delta 70 | 71 | data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" 72 | is Tokyo."} } 73 | 74 | 75 | event: content_block_stop 76 | 77 | data: {"type":"content_block_stop","index":0 } 78 | 79 | 80 | event: message_delta 81 | 82 | data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10} } 83 | 84 | 85 | event: message_stop 86 | 87 | data: {"type":"message_stop" } 88 | 89 | 90 | ' 91 | headers: 92 | CF-RAY: 93 | - 930ce0917db19dc1-DME 94 | Cache-Control: 95 | - no-cache 96 | Connection: 97 | - keep-alive 98 | Content-Type: 99 | - text/event-stream; charset=utf-8 100 | Date: 101 | - Tue, 15 Apr 2025 16:39:08 GMT 102 | Server: 103 | - cloudflare 104 | Transfer-Encoding: 105 | - chunked 106 | X-Robots-Tag: 107 | - none 108 | anthropic-organization-id: 109 | - ec8f80fa-3527-4d15-8ab4-526e6a1f4899 110 | cf-cache-status: 111 | - DYNAMIC 112 | request-id: 113 | - req_01QoFNrnhsQbRcsukasRMqQ7 114 | via: 115 | - 1.1 google 116 | status: 117 | code: 200 118 | message: OK 119 | - request: 120 | body: '{"function_name": "anthropic.AsyncAnthropic.messages.create", "provider_type": 121 | "anthropic", "args": [], "kwargs": {"max_tokens": 1024, "messages": [{"role": 122 | "user", "content": "What is the capital of Japan?"}], "model": "claude-3-haiku-20240307", 123 | "stream": true}, "tags": null, "request_response": {"id": "msg_01Bi6S5crUgtL7PUCuYc8Vy6", 124 | "content": [{"citations": null, "text": "The capital of Japan is Tokyo.", "type": 125 | "text"}], "model": "claude-3-haiku-20240307", "role": "assistant", "stop_reason": 126 | null, "stop_sequence": null, "type": "message", "usage": null}, "request_start_time": 127 | 1744735147.208988, "request_end_time": 1744735148.237783, "metadata": null, 128 | "span_id": null, "api_key": "sanitized"}' 129 | headers: 130 | Accept: 131 | - '*/*' 132 | Accept-Encoding: 133 | - gzip, deflate 134 | Connection: 135 | - keep-alive 136 | Content-Length: 137 | - '729' 138 | Content-Type: 139 | - application/json 140 | User-Agent: 141 | - python-requests/2.31.0 142 | method: POST 143 | uri: http://localhost:8000/track-request 144 | response: 145 | body: 146 | string: '{"success":true,"request_id":191,"prompt_blueprint":null,"message":"Request 147 | tracked successfully"} 148 | 149 | ' 150 | headers: 151 | Connection: 152 | - close 153 | Content-Length: 154 | - '99' 155 | Content-Type: 156 | - application/json 157 | Date: 158 | - Tue, 15 Apr 2025 16:39:08 GMT 159 | Server: 160 | - gunicorn 161 | status: 162 | code: 200 163 | message: OK 164 | version: 1 165 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_anthropic_chat_completion_with_pl_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"max_tokens": 1024, "messages": [{"role": "user", "content": "What is 4 | the capital of France?"}], "model": "claude-3-haiku-20240307"}' 5 | headers: 6 | accept: 7 | - application/json 8 | accept-encoding: 9 | - gzip, deflate 10 | anthropic-version: 11 | - '2023-06-01' 12 | connection: 13 | - keep-alive 14 | content-length: 15 | - '125' 16 | content-type: 17 | - application/json 18 | host: 19 | - api.anthropic.com 20 | user-agent: 21 | - Anthropic/Python 0.49.0 22 | x-api-key: 23 | - sanitized 24 | x-stainless-arch: 25 | - x64 26 | x-stainless-async: 27 | - 'false' 28 | x-stainless-lang: 29 | - python 30 | x-stainless-os: 31 | - Linux 32 | x-stainless-package-version: 33 | - 0.49.0 34 | x-stainless-read-timeout: 35 | - '600' 36 | x-stainless-retry-count: 37 | - '0' 38 | x-stainless-runtime: 39 | - CPython 40 | x-stainless-runtime-version: 41 | - 3.9.21 42 | x-stainless-timeout: 43 | - '600' 44 | method: POST 45 | uri: https://api.anthropic.com/v1/messages 46 | response: 47 | body: 48 | string: '{"id":"msg_01BTdpWGHBkDbTHb7MS95DLy","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[{"type":"text","text":"The 49 | capital of France is Paris."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}}' 50 | headers: 51 | CF-RAY: 52 | - 9302ca74ec41e317-DME 53 | Connection: 54 | - keep-alive 55 | Content-Type: 56 | - application/json 57 | Date: 58 | - Mon, 14 Apr 2025 11:16:24 GMT 59 | Server: 60 | - cloudflare 61 | Transfer-Encoding: 62 | - chunked 63 | X-Robots-Tag: 64 | - none 65 | anthropic-organization-id: 66 | - ec8f80fa-3527-4d15-8ab4-526e6a1f4899 67 | cf-cache-status: 68 | - DYNAMIC 69 | content-length: 70 | - '329' 71 | request-id: 72 | - req_01YYd3cXehJ6bHEGH5ne1AJZ 73 | via: 74 | - 1.1 google 75 | status: 76 | code: 200 77 | message: OK 78 | - request: 79 | body: '{"function_name": "anthropic.Anthropic.messages.create", "provider_type": 80 | "anthropic", "args": [], "kwargs": {"max_tokens": 1024, "messages": [{"role": 81 | "user", "content": "What is the capital of France?"}], "model": "claude-3-haiku-20240307"}, 82 | "tags": null, "request_response": {"id": "msg_01BTdpWGHBkDbTHb7MS95DLy", "content": 83 | [{"citations": null, "text": "The capital of France is Paris.", "type": "text"}], 84 | "model": "claude-3-haiku-20240307", "role": "assistant", "stop_reason": "end_turn", 85 | "stop_sequence": null, "type": "message", "usage": {"cache_creation_input_tokens": 86 | 0, "cache_read_input_tokens": 0, "input_tokens": 14, "output_tokens": 10}}, 87 | "request_start_time": 1744629384.010934, "request_end_time": 1744629384.909616, 88 | "metadata": null, "span_id": null, "api_key": "sanitized"}' 89 | headers: 90 | Accept: 91 | - '*/*' 92 | Accept-Encoding: 93 | - gzip, deflate 94 | Connection: 95 | - keep-alive 96 | Content-Length: 97 | - '817' 98 | Content-Type: 99 | - application/json 100 | User-Agent: 101 | - python-requests/2.31.0 102 | method: POST 103 | uri: http://localhost:8000/track-request 104 | response: 105 | body: 106 | string: '{"success":true,"request_id":134,"prompt_blueprint":null,"message":"Request 107 | tracked successfully"} 108 | 109 | ' 110 | headers: 111 | Connection: 112 | - close 113 | Content-Length: 114 | - '99' 115 | Content-Type: 116 | - application/json 117 | Date: 118 | - Mon, 14 Apr 2025 11:16:24 GMT 119 | Server: 120 | - gunicorn 121 | status: 122 | code: 200 123 | message: OK 124 | version: 1 125 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_anthropic_chat_completion_with_stream.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"max_tokens": 1024, "messages": [{"role": "user", "content": "What is 4 | the capital of Germany?"}], "model": "claude-3-haiku-20240307", "stream": true}' 5 | headers: 6 | accept: 7 | - application/json 8 | accept-encoding: 9 | - gzip, deflate 10 | anthropic-version: 11 | - '2023-06-01' 12 | connection: 13 | - keep-alive 14 | content-length: 15 | - '140' 16 | content-type: 17 | - application/json 18 | host: 19 | - api.anthropic.com 20 | user-agent: 21 | - Anthropic/Python 0.49.0 22 | x-api-key: 23 | - sanitized 24 | x-stainless-arch: 25 | - x64 26 | x-stainless-async: 27 | - 'false' 28 | x-stainless-lang: 29 | - python 30 | x-stainless-os: 31 | - Linux 32 | x-stainless-package-version: 33 | - 0.49.0 34 | x-stainless-read-timeout: 35 | - '600' 36 | x-stainless-retry-count: 37 | - '0' 38 | x-stainless-runtime: 39 | - CPython 40 | x-stainless-runtime-version: 41 | - 3.9.21 42 | x-stainless-timeout: 43 | - NOT_GIVEN 44 | method: POST 45 | uri: https://api.anthropic.com/v1/messages 46 | response: 47 | body: 48 | string: 'event: message_start 49 | 50 | data: {"type":"message_start","message":{"id":"msg_01PP15qoPAWehXLzXosCnnVP","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4}} } 51 | 52 | 53 | event: content_block_start 54 | 55 | data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } 56 | 57 | 58 | event: ping 59 | 60 | data: {"type": "ping"} 61 | 62 | 63 | event: content_block_delta 64 | 65 | data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The 66 | capital of Germany"} } 67 | 68 | 69 | event: content_block_delta 70 | 71 | data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" 72 | is Berlin."} } 73 | 74 | 75 | event: content_block_stop 76 | 77 | data: {"type":"content_block_stop","index":0 } 78 | 79 | 80 | event: message_delta 81 | 82 | data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10} } 83 | 84 | 85 | event: message_stop 86 | 87 | data: {"type":"message_stop" } 88 | 89 | 90 | ' 91 | headers: 92 | CF-RAY: 93 | - 930cabf0da58f112-DME 94 | Cache-Control: 95 | - no-cache 96 | Connection: 97 | - keep-alive 98 | Content-Type: 99 | - text/event-stream; charset=utf-8 100 | Date: 101 | - Tue, 15 Apr 2025 16:03:12 GMT 102 | Server: 103 | - cloudflare 104 | Transfer-Encoding: 105 | - chunked 106 | X-Robots-Tag: 107 | - none 108 | anthropic-organization-id: 109 | - ec8f80fa-3527-4d15-8ab4-526e6a1f4899 110 | cf-cache-status: 111 | - DYNAMIC 112 | request-id: 113 | - req_01KZGzAirTUtQ7vqZk3uEFB9 114 | via: 115 | - 1.1 google 116 | status: 117 | code: 200 118 | message: OK 119 | - request: 120 | body: '{"function_name": "anthropic.Anthropic.messages.create", "provider_type": 121 | "anthropic", "args": [], "kwargs": {"max_tokens": 1024, "messages": [{"role": 122 | "user", "content": "What is the capital of Germany?"}], "model": "claude-3-haiku-20240307", 123 | "stream": true}, "tags": null, "request_response": {"id": "msg_01PP15qoPAWehXLzXosCnnVP", 124 | "content": [{"citations": null, "text": "The capital of Germany is Berlin.", 125 | "type": "text"}], "model": "claude-3-haiku-20240307", "role": "assistant", "stop_reason": 126 | null, "stop_sequence": null, "type": "message", "usage": null}, "request_start_time": 127 | 1744732991.599045, "request_end_time": 1744732992.605533, "metadata": null, 128 | "span_id": null, "api_key": "sanitized"}' 129 | headers: 130 | Accept: 131 | - '*/*' 132 | Accept-Encoding: 133 | - gzip, deflate 134 | Connection: 135 | - keep-alive 136 | Content-Length: 137 | - '729' 138 | Content-Type: 139 | - application/json 140 | User-Agent: 141 | - python-requests/2.31.0 142 | method: POST 143 | uri: http://localhost:8000/track-request 144 | response: 145 | body: 146 | string: '{"success":true,"request_id":176,"prompt_blueprint":null,"message":"Request 147 | tracked successfully"} 148 | 149 | ' 150 | headers: 151 | Connection: 152 | - close 153 | Content-Length: 154 | - '99' 155 | Content-Type: 156 | - application/json 157 | Date: 158 | - Tue, 15 Apr 2025 16:03:12 GMT 159 | Server: 160 | - gunicorn 161 | status: 162 | code: 200 163 | message: OK 164 | version: 1 165 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_anthropic_chat_completion_with_stream_and_pl_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"max_tokens": 1024, "messages": [{"role": "user", "content": "What is 4 | the capital of Italy?"}], "model": "claude-3-haiku-20240307", "stream": true}' 5 | headers: 6 | accept: 7 | - application/json 8 | accept-encoding: 9 | - gzip, deflate 10 | anthropic-version: 11 | - '2023-06-01' 12 | connection: 13 | - keep-alive 14 | content-length: 15 | - '138' 16 | content-type: 17 | - application/json 18 | host: 19 | - api.anthropic.com 20 | user-agent: 21 | - Anthropic/Python 0.49.0 22 | x-api-key: 23 | - sanitized 24 | x-stainless-arch: 25 | - x64 26 | x-stainless-async: 27 | - 'false' 28 | x-stainless-lang: 29 | - python 30 | x-stainless-os: 31 | - Linux 32 | x-stainless-package-version: 33 | - 0.49.0 34 | x-stainless-read-timeout: 35 | - '600' 36 | x-stainless-retry-count: 37 | - '0' 38 | x-stainless-runtime: 39 | - CPython 40 | x-stainless-runtime-version: 41 | - 3.9.21 42 | x-stainless-timeout: 43 | - NOT_GIVEN 44 | method: POST 45 | uri: https://api.anthropic.com/v1/messages 46 | response: 47 | body: 48 | string: 'event: message_start 49 | 50 | data: {"type":"message_start","message":{"id":"msg_016q2kSZ82qtDP2CNUAKSfLV","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4}} } 51 | 52 | 53 | event: content_block_start 54 | 55 | data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } 56 | 57 | 58 | event: ping 59 | 60 | data: {"type": "ping"} 61 | 62 | 63 | event: content_block_delta 64 | 65 | data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The 66 | capital of Italy"} } 67 | 68 | 69 | event: content_block_delta 70 | 71 | data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" 72 | is Rome."} } 73 | 74 | 75 | event: content_block_stop 76 | 77 | data: {"type":"content_block_stop","index":0 } 78 | 79 | 80 | event: message_delta 81 | 82 | data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10} } 83 | 84 | 85 | event: message_stop 86 | 87 | data: {"type":"message_stop" } 88 | 89 | 90 | ' 91 | headers: 92 | CF-RAY: 93 | - 930ca449f9c29dd6-DME 94 | Cache-Control: 95 | - no-cache 96 | Connection: 97 | - keep-alive 98 | Content-Type: 99 | - text/event-stream; charset=utf-8 100 | Date: 101 | - Tue, 15 Apr 2025 15:57:59 GMT 102 | Server: 103 | - cloudflare 104 | Transfer-Encoding: 105 | - chunked 106 | X-Robots-Tag: 107 | - none 108 | anthropic-organization-id: 109 | - ec8f80fa-3527-4d15-8ab4-526e6a1f4899 110 | cf-cache-status: 111 | - DYNAMIC 112 | request-id: 113 | - req_01MhQVkzE2Dj5LJ9hwtn94A2 114 | via: 115 | - 1.1 google 116 | status: 117 | code: 200 118 | message: OK 119 | - request: 120 | body: '{"function_name": "anthropic.Anthropic.messages.create", "provider_type": 121 | "anthropic", "args": [], "kwargs": {"max_tokens": 1024, "messages": [{"role": 122 | "user", "content": "What is the capital of Italy?"}], "model": "claude-3-haiku-20240307", 123 | "stream": true}, "tags": null, "request_response": {"id": "msg_016q2kSZ82qtDP2CNUAKSfLV", 124 | "content": [{"citations": null, "text": "The capital of Italy is Rome.", "type": 125 | "text"}], "model": "claude-3-haiku-20240307", "role": "assistant", "stop_reason": 126 | null, "stop_sequence": null, "type": "message", "usage": null}, "request_start_time": 127 | 1744732678.298123, "request_end_time": 1744732679.148348, "metadata": null, 128 | "span_id": null, "api_key": "sanitized"}' 129 | headers: 130 | Accept: 131 | - '*/*' 132 | Accept-Encoding: 133 | - gzip, deflate 134 | Connection: 135 | - keep-alive 136 | Content-Length: 137 | - '723' 138 | Content-Type: 139 | - application/json 140 | User-Agent: 141 | - python-requests/2.31.0 142 | method: POST 143 | uri: http://localhost:8000/track-request 144 | response: 145 | body: 146 | string: '{"success":true,"request_id":175,"prompt_blueprint":null,"message":"Request 147 | tracked successfully"} 148 | 149 | ' 150 | headers: 151 | Connection: 152 | - close 153 | Content-Length: 154 | - '99' 155 | Content-Type: 156 | - application/json 157 | Date: 158 | - Tue, 15 Apr 2025 15:57:59 GMT 159 | Server: 160 | - gunicorn 161 | status: 162 | code: 200 163 | message: OK 164 | version: 1 165 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_arun_workflow_request.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '' 4 | headers: 5 | accept: 6 | - '*/*' 7 | accept-encoding: 8 | - gzip, deflate 9 | connection: 10 | - keep-alive 11 | host: 12 | - localhost:8000 13 | user-agent: 14 | - python-httpx/0.28.1 15 | x-api-key: 16 | - sanitized 17 | method: GET 18 | uri: http://localhost:8000/workflows/analyze_1 19 | response: 20 | body: 21 | string: '{"success":true,"workflow":{"id":3,"workspace_id":1,"user_id":null,"name":"analyze_1","is_draft":false,"is_deleted":false}} 22 | 23 | ' 24 | headers: 25 | Connection: 26 | - close 27 | Content-Length: 28 | - '124' 29 | Content-Type: 30 | - application/json 31 | Date: 32 | - Thu, 01 May 2025 16:10:30 GMT 33 | Server: 34 | - gunicorn 35 | status: 36 | code: 200 37 | message: OK 38 | - request: 39 | body: '' 40 | headers: 41 | accept: 42 | - '*/*' 43 | accept-encoding: 44 | - gzip, deflate 45 | connection: 46 | - keep-alive 47 | content-length: 48 | - '0' 49 | host: 50 | - localhost:8000 51 | user-agent: 52 | - python-httpx/0.28.1 53 | x-api-key: 54 | - sanitized 55 | method: POST 56 | uri: http://localhost:8000/ws-token-request-library?capability=workflows%3A3%3Arun%3A8dd7e4d404754c60a50e78f70f74aade 57 | response: 58 | body: 59 | string: '{"success":true,"token_details":{"expires":1746119430452,"token":"uV0vXQ.JAcrUQfVqw-T7-Rlm000FrnEV3UAXYba0goN5h9XPQ7OrNfb5uq1juDZjNTFUWP9T6Gvvt3a0zzVfyuud8s9_uUFWB5Xu5pFLkbrAq1Le9BVvUhhCDdmOPzZVwKZRxJgRBQw7A7o6AABMcItiaennjFKUltTGmawIsX3kCEyBRiNluPC0k5SRhXTPfIC1gidZ3PZGHNB-PN1IDxqZVBlld5NwU2iLg-UQcvMR3fzRz-8SQQ9cDFp1HlEq38JNGvxA","issued":1746115830452,"capability":{"user:1":["presence","subscribe"],"workflows:3:run:8dd7e4d404754c60a50e78f70f74aade":["presence","subscribe"]},"clientId":"1"}} 60 | 61 | ' 62 | headers: 63 | Connection: 64 | - close 65 | Content-Length: 66 | - '497' 67 | Content-Type: 68 | - application/json 69 | Date: 70 | - Thu, 01 May 2025 16:10:31 GMT 71 | Server: 72 | - gunicorn 73 | status: 74 | code: 201 75 | message: CREATED 76 | - request: 77 | body: '{"input_variables": {"var1": "value1"}, "metadata": null, "workflow_label_name": 78 | null, "workflow_version_number": null, "return_all_outputs": false, "channel_name_suffix": 79 | "8dd7e4d404754c60a50e78f70f74aade"}' 80 | headers: 81 | accept: 82 | - '*/*' 83 | accept-encoding: 84 | - gzip, deflate 85 | connection: 86 | - keep-alive 87 | content-length: 88 | - '195' 89 | content-type: 90 | - application/json 91 | host: 92 | - localhost:8000 93 | user-agent: 94 | - python-httpx/0.28.1 95 | x-api-key: 96 | - sanitized 97 | method: POST 98 | uri: http://localhost:8000/workflows/3/run 99 | response: 100 | body: 101 | string: '{"success":true,"message":"Workflow execution created successfully","warning":null,"workflow_version_execution_id":787} 102 | 103 | ' 104 | headers: 105 | Connection: 106 | - close 107 | Content-Length: 108 | - '120' 109 | Content-Type: 110 | - application/json 111 | Date: 112 | - Thu, 01 May 2025 16:10:32 GMT 113 | Server: 114 | - gunicorn 115 | status: 116 | code: 201 117 | message: CREATED 118 | version: 1 119 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_get_all_templates.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | User-Agent: 12 | - python-requests/2.31.0 13 | x-api-key: 14 | - sanitized 15 | method: GET 16 | uri: http://localhost:8000/prompt-templates?page=1&per_page=30 17 | response: 18 | body: 19 | string: '{"items":[{"id":4,"prompt_name":"sample_template","tags":["test"],"folder_id":null,"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 20 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"version":1,"commit_message":"test","metadata":{"model":{"provider":"openai","name":"gpt-4o-mini","parameters":{"frequency_penalty":0,"max_tokens":256,"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","presence_penalty":0,"seed":0,"temperature":1,"top_p":1}}},"parent_folder_name":null,"full_folder_path":null},{"id":3,"prompt_name":"simple","tags":[],"folder_id":null,"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"You 21 | are helpful assistant"}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"Reply 22 | with Hey"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":null,"tools":null,"function_call":null,"tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"version":1,"commit_message":null,"metadata":null,"parent_folder_name":null,"full_folder_path":null},{"id":1,"prompt_name":"Example: 23 | page_title","tags":[],"folder_id":null,"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"You 24 | are a helpful AI assistant. You work for an online media organization called 25 | BuzzFeed.\n\nYour task is to create catchy article titles based on a summary. 26 | The titles should be no more than 10 words. The titles should have no emojis.\n\nThe 27 | goal is to write catchy & easy to write blog post titles. These titles should 28 | be \"clickbait\" and encourage users to click on the blog.\n\nThe user will 29 | provide a blog post abstract summary as well as a topic keyword. You will 30 | respond with a title. Don''t use quotes in the title."}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"# 31 | Topic\nTravel\n\n# Summary\nWanderlust calling? Check out our curated list 32 | of 10 awe-inspiring travel destinations that should be on every adventurer''s 33 | bucket list. From tropical paradises to historic cities, these places offer 34 | unforgettable experiences. Get ready to fuel your wanderlust and start planning 35 | your next getaway!"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"The 36 | Ultimate Travel Bucket List: 10 Breathtaking Destinations to Visit"}],"raw_request_display_role":"","dataset_examples":[],"role":"assistant","function_call":null,"name":null,"tool_calls":null},{"input_variables":["article","topic"],"template_format":"f-string","content":[{"type":"text","text":"# 37 | Topic\n{topic}\n\n# Summary\n{article}"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":null,"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":["article","topic"],"dataset_examples":[]},"version":6,"commit_message":"No 38 | quotes, higher temp","metadata":{"model":{"provider":"openai","name":"gpt-3.5-turbo","parameters":{"best_of":1,"frequency_penalty":0,"max_tokens":256,"presence_penalty":0,"temperature":1.2,"top_p":1}}},"parent_folder_name":null,"full_folder_path":null}],"has_prev":false,"has_next":false,"prev_num":null,"next_num":null,"page":1,"pages":1,"per_page":30,"total":3} 39 | 40 | ' 41 | headers: 42 | Connection: 43 | - close 44 | Content-Length: 45 | - '3977' 46 | Content-Type: 47 | - application/json 48 | Date: 49 | - Mon, 14 Apr 2025 10:53:02 GMT 50 | Server: 51 | - gunicorn 52 | status: 53 | code: 200 54 | message: OK 55 | version: 1 56 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_get_final_output_1.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '' 4 | headers: 5 | accept: 6 | - '*/*' 7 | accept-encoding: 8 | - gzip, deflate 9 | connection: 10 | - keep-alive 11 | host: 12 | - localhost:8000 13 | user-agent: 14 | - python-httpx/0.28.1 15 | x-api-key: 16 | - sanitized 17 | method: GET 18 | uri: http://localhost:8000/workflow-version-execution-results?workflow_version_execution_id=717&return_all_outputs=true 19 | response: 20 | body: 21 | string: '{"Node 1":{"status":"SUCCESS","value":"AAA","error_message":null,"raw_error_message":null,"is_output_node":true}} 22 | 23 | ' 24 | headers: 25 | Connection: 26 | - close 27 | Content-Length: 28 | - '114' 29 | Content-Type: 30 | - application/json 31 | Date: 32 | - Fri, 11 Apr 2025 17:44:48 GMT 33 | Server: 34 | - gunicorn 35 | status: 36 | code: 200 37 | message: OK 38 | version: 1 39 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_get_final_output_2.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '' 4 | headers: 5 | accept: 6 | - '*/*' 7 | accept-encoding: 8 | - gzip, deflate 9 | connection: 10 | - keep-alive 11 | host: 12 | - localhost:8000 13 | user-agent: 14 | - python-httpx/0.28.1 15 | x-api-key: 16 | - sanitized 17 | method: GET 18 | uri: http://localhost:8000/workflow-version-execution-results?workflow_version_execution_id=717&return_all_outputs=false 19 | response: 20 | body: 21 | string: '"AAA" 22 | 23 | ' 24 | headers: 25 | Connection: 26 | - close 27 | Content-Length: 28 | - '6' 29 | Content-Type: 30 | - application/json 31 | Date: 32 | - Fri, 11 Apr 2025 17:50:28 GMT 33 | Server: 34 | - gunicorn 35 | status: 36 | code: 200 37 | message: OK 38 | version: 1 39 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_get_prompt_template_provider_base_url_name.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"prompt_template": {"provider_base_url_name": "does_not_exist", "prompt_name": 4 | "test_template:test", "prompt_template": {"type": "chat", "provider_base_url_name": 5 | "does_not_exist", "messages": [{"content": [{"text": "You are an AI.", "type": 6 | "text"}], "input_variables": [], "name": null, "raw_request_display_role": "", 7 | "role": "system", "template_format": "f-string"}, {"content": [{"text": "What 8 | is the capital of Japan?", "type": "text"}], "input_variables": [], "name": 9 | null, "raw_request_display_role": "", "role": "user", "template_format": "f-string"}]}}, 10 | "prompt_version": {"provider_base_url_name": "does_not_exist", "prompt_name": 11 | "test_template:test", "prompt_template": {"type": "chat", "provider_base_url_name": 12 | "does_not_exist", "messages": [{"content": [{"text": "You are an AI.", "type": 13 | "text"}], "input_variables": [], "name": null, "raw_request_display_role": "", 14 | "role": "system", "template_format": "f-string"}, {"content": [{"text": "What 15 | is the capital of Japan?", "type": "text"}], "input_variables": [], "name": 16 | null, "raw_request_display_role": "", "role": "user", "template_format": "f-string"}]}}, 17 | "release_labels": null}' 18 | headers: 19 | Accept: 20 | - '*/*' 21 | Accept-Encoding: 22 | - gzip, deflate 23 | Connection: 24 | - keep-alive 25 | Content-Length: 26 | - '1151' 27 | Content-Type: 28 | - application/json 29 | User-Agent: 30 | - python-requests/2.31.0 31 | x-api-key: 32 | - sanitized 33 | method: POST 34 | uri: http://localhost:8000/rest/prompt-templates 35 | response: 36 | body: 37 | string: '{"id":5,"prompt_name":"test_template:test","tags":[],"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"You 38 | are an AI."}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 39 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":null,"tools":null,"function_call":null,"tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"commit_message":null,"metadata":null,"release_labels":null} 40 | 41 | ' 42 | headers: 43 | Connection: 44 | - close 45 | Content-Length: 46 | - '655' 47 | Content-Type: 48 | - application/json 49 | Date: 50 | - Mon, 14 Apr 2025 11:27:33 GMT 51 | Server: 52 | - gunicorn 53 | status: 54 | code: 201 55 | message: CREATED 56 | - request: 57 | body: '{"provider": "openai", "model": "gpt-3.5-turbo", "api_key": "sanitized"}' 58 | headers: 59 | Accept: 60 | - '*/*' 61 | Accept-Encoding: 62 | - gzip, deflate 63 | Connection: 64 | - keep-alive 65 | Content-Length: 66 | - '98' 67 | Content-Type: 68 | - application/json 69 | User-Agent: 70 | - python-requests/2.31.0 71 | x-api-key: 72 | - sanitized 73 | method: POST 74 | uri: http://localhost:8000/prompt-templates/test_template:test 75 | response: 76 | body: 77 | string: '{"id":5,"prompt_name":"test_template:test","tags":[],"workspace_id":1,"commit_message":null,"metadata":null,"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"You 78 | are an AI."}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 79 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":null,"tools":null,"function_call":null,"tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"llm_kwargs":{"messages":[{"role":"system","content":"You 80 | are an AI."},{"role":"user","content":[{"type":"text","text":"What is the 81 | capital of Japan?"}]}]},"provider_base_url":null,"version":1,"snippets":[],"warning":null} 82 | 83 | ' 84 | headers: 85 | Connection: 86 | - close 87 | Content-Length: 88 | - '872' 89 | Content-Type: 90 | - application/json 91 | Date: 92 | - Mon, 14 Apr 2025 11:27:33 GMT 93 | Server: 94 | - gunicorn 95 | status: 96 | code: 200 97 | message: OK 98 | version: 1 99 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_get_template_async.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"provider": "openai", "model": "gpt-3.5-turbo", "api_key": "sanitized"}' 4 | headers: 5 | accept: 6 | - '*/*' 7 | accept-encoding: 8 | - gzip, deflate 9 | connection: 10 | - keep-alive 11 | content-length: 12 | - '93' 13 | content-type: 14 | - application/json 15 | host: 16 | - localhost:8000 17 | user-agent: 18 | - python-httpx/0.28.1 19 | x-api-key: 20 | - sanitized 21 | method: POST 22 | uri: http://localhost:8000/prompt-templates/sample_template 23 | response: 24 | body: 25 | string: '{"id":4,"prompt_name":"sample_template","tags":["test"],"workspace_id":1,"commit_message":"test","metadata":{"model":{"provider":"openai","name":"gpt-4o-mini","parameters":{"frequency_penalty":0,"max_tokens":256,"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","presence_penalty":0,"seed":0,"temperature":1,"top_p":1}}},"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 26 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"llm_kwargs":{"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","frequency_penalty":0,"max_tokens":256,"presence_penalty":0,"seed":0,"temperature":1,"top_p":1},"provider_base_url":null,"version":1,"snippets":[],"warning":null} 27 | 28 | ' 29 | headers: 30 | Connection: 31 | - close 32 | Content-Length: 33 | - '1107' 34 | Content-Type: 35 | - application/json 36 | Date: 37 | - Mon, 14 Apr 2025 09:33:23 GMT 38 | Server: 39 | - gunicorn 40 | status: 41 | code: 200 42 | message: OK 43 | version: 1 44 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_log_request_async.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"api_key": "sanitized"}' 4 | headers: 5 | accept: 6 | - '*/*' 7 | accept-encoding: 8 | - gzip, deflate 9 | connection: 10 | - keep-alive 11 | content-length: 12 | - '49' 13 | content-type: 14 | - application/json 15 | host: 16 | - localhost:8000 17 | user-agent: 18 | - python-httpx/0.28.1 19 | x-api-key: 20 | - sanitized 21 | method: POST 22 | uri: http://localhost:8000/prompt-templates/sample_template 23 | response: 24 | body: 25 | string: '{"id":4,"prompt_name":"sample_template","tags":["test"],"workspace_id":1,"commit_message":"test","metadata":{"model":{"provider":"openai","name":"gpt-4o-mini","parameters":{"frequency_penalty":0,"max_tokens":256,"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","presence_penalty":0,"seed":0,"temperature":1,"top_p":1}}},"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 26 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"llm_kwargs":{"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","frequency_penalty":0,"max_tokens":256,"presence_penalty":0,"seed":0,"temperature":1,"top_p":1},"provider_base_url":null,"version":1,"snippets":[],"warning":null} 27 | 28 | ' 29 | headers: 30 | Connection: 31 | - close 32 | Content-Length: 33 | - '1107' 34 | Content-Type: 35 | - application/json 36 | Date: 37 | - Mon, 14 Apr 2025 10:21:52 GMT 38 | Server: 39 | - gunicorn 40 | status: 41 | code: 200 42 | message: OK 43 | - request: 44 | body: '{"provider": "openai", "model": "gpt-4-mini", "input": {"messages": [{"input_variables": 45 | [], "template_format": "f-string", "content": [{"type": "text", "text": ""}], 46 | "raw_request_display_role": "", "dataset_examples": [], "role": "system", "name": 47 | null}, {"input_variables": [], "template_format": "f-string", "content": [{"type": 48 | "text", "text": "What is the capital of Japan?"}], "raw_request_display_role": 49 | "", "dataset_examples": [], "role": "user", "name": null}], "functions": [], 50 | "tools": null, "function_call": "none", "tool_choice": null, "type": "chat", 51 | "input_variables": [], "dataset_examples": []}, "output": {"messages": [{"input_variables": 52 | [], "template_format": "f-string", "content": [{"type": "text", "text": ""}], 53 | "raw_request_display_role": "", "dataset_examples": [], "role": "system", "name": 54 | null}, {"input_variables": [], "template_format": "f-string", "content": [{"type": 55 | "text", "text": "What is the capital of Japan?"}], "raw_request_display_role": 56 | "", "dataset_examples": [], "role": "user", "name": null}], "functions": [], 57 | "tools": null, "function_call": "none", "tool_choice": null, "type": "chat", 58 | "input_variables": [], "dataset_examples": []}, "request_start_time": 1744626112.7601638, 59 | "request_end_time": 1744626112.7601643, "parameters": {}, "tags": [], "metadata": 60 | {}, "prompt_name": null, "prompt_version_number": null, "prompt_input_variables": 61 | {}, "input_tokens": 0, "output_tokens": 0, "price": 0.0, "function_name": "", 62 | "score": 0}' 63 | headers: 64 | accept: 65 | - '*/*' 66 | accept-encoding: 67 | - gzip, deflate 68 | connection: 69 | - keep-alive 70 | content-length: 71 | - '1347' 72 | content-type: 73 | - application/json 74 | host: 75 | - localhost:8000 76 | user-agent: 77 | - python-httpx/0.28.1 78 | x-api-key: 79 | - sanitized 80 | method: POST 81 | uri: http://localhost:8000/log-request 82 | response: 83 | body: 84 | string: '{"id":130,"user_id":1,"prompt_id":null,"prompt_version_number":null,"prompt_input_variables":{},"provider_type":"openai","function_name":"","function_args":[],"function_kwargs":{"input":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 85 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"parameters":{}},"request_response":{"output":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 86 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]}},"request_start_time":"2025-04-14T10:21:52.760164","request_end_time":"2025-04-14T10:21:52.760164","is_sharable":false,"share_hash":"561672cb688836ec113a19469a75aa40","share_view_count":0,"is_starred":false,"price":0.0,"score":0,"engine":"gpt-4-mini","tokens":0,"input_tokens":0,"output_tokens":0,"workspace_id":1,"span_id":null,"created_at":"Mon, 87 | 14 Apr 2025 10:21:52 GMT","metadata":[],"scores":[],"type":"request","tags_array":[],"prompt_string":"","response_string":"What 88 | is the capital of Japan?","prompt_version":{"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 89 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 90 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"commit_message":null,"metadata":{"model":{"provider":"openai","name":"gpt-4-mini","parameters":{}}},"provider_base_url_name":null,"report_id":null,"inference_client_name":null}} 91 | 92 | ' 93 | headers: 94 | Connection: 95 | - close 96 | Content-Length: 97 | - '2843' 98 | Content-Type: 99 | - application/json 100 | Date: 101 | - Mon, 14 Apr 2025 10:21:52 GMT 102 | Server: 103 | - gunicorn 104 | status: 105 | code: 201 106 | message: CREATED 107 | version: 1 108 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_make_message_listener_1.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '' 4 | headers: 5 | accept: 6 | - '*/*' 7 | accept-encoding: 8 | - gzip, deflate 9 | connection: 10 | - keep-alive 11 | host: 12 | - localhost:8000 13 | user-agent: 14 | - python-httpx/0.28.1 15 | x-api-key: 16 | - sanitized 17 | method: GET 18 | uri: http://localhost:8000/workflow-version-execution-results?workflow_version_execution_id=717&return_all_outputs=true 19 | response: 20 | body: 21 | string: '{"Node 1":{"status":"SUCCESS","value":"AAA","error_message":null,"raw_error_message":null,"is_output_node":true}} 22 | 23 | ' 24 | headers: 25 | Connection: 26 | - close 27 | Content-Length: 28 | - '114' 29 | Content-Type: 30 | - application/json 31 | Date: 32 | - Fri, 11 Apr 2025 17:44:48 GMT 33 | Server: 34 | - gunicorn 35 | status: 36 | code: 200 37 | message: OK 38 | version: 1 39 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_make_message_listener_2.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '' 4 | headers: 5 | accept: 6 | - '*/*' 7 | accept-encoding: 8 | - gzip, deflate 9 | connection: 10 | - keep-alive 11 | host: 12 | - localhost:8000 13 | user-agent: 14 | - python-httpx/0.28.1 15 | x-api-key: 16 | - sanitized 17 | method: GET 18 | uri: http://localhost:8000/workflow-version-execution-results?workflow_version_execution_id=717&return_all_outputs=false 19 | response: 20 | body: 21 | string: '"AAA" 22 | 23 | ' 24 | headers: 25 | Connection: 26 | - close 27 | Content-Length: 28 | - '6' 29 | Content-Type: 30 | - application/json 31 | Date: 32 | - Fri, 11 Apr 2025 17:50:28 GMT 33 | Server: 34 | - gunicorn 35 | status: 36 | code: 200 37 | message: OK 38 | version: 1 39 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_openai_chat_completion.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, 4 | {"role": "user", "content": "What is the capital of the United States?"}], "model": 5 | "gpt-3.5-turbo"}' 6 | headers: 7 | Authorization: 8 | - sanitized 9 | accept: 10 | - application/json 11 | accept-encoding: 12 | - gzip, deflate 13 | connection: 14 | - keep-alive 15 | content-length: 16 | - '167' 17 | content-type: 18 | - application/json 19 | host: 20 | - api.openai.com 21 | user-agent: 22 | - OpenAI/Python 1.60.1 23 | x-stainless-arch: 24 | - x64 25 | x-stainless-async: 26 | - 'false' 27 | x-stainless-lang: 28 | - python 29 | x-stainless-os: 30 | - Linux 31 | x-stainless-package-version: 32 | - 1.60.1 33 | x-stainless-retry-count: 34 | - '0' 35 | x-stainless-runtime: 36 | - CPython 37 | x-stainless-runtime-version: 38 | - 3.9.21 39 | method: POST 40 | uri: https://api.openai.com/v1/chat/completions 41 | response: 42 | body: 43 | string: "{\n \"id\": \"chatcmpl-BMF5JYzBZVfT2ZlGYxqkF28JzXY40\",\n \"object\": 44 | \"chat.completion\",\n \"created\": 1744640901,\n \"model\": \"gpt-3.5-turbo-0125\",\n 45 | \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": 46 | \"assistant\",\n \"content\": \"The capital of the United States is 47 | Washington, D.C.\",\n \"refusal\": null,\n \"annotations\": 48 | []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n 49 | \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 26,\n \"completion_tokens\": 50 | 13,\n \"total_tokens\": 39,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 51 | 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": 52 | {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 53 | 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": 54 | \"default\",\n \"system_fingerprint\": null\n}\n" 55 | headers: 56 | CF-RAY: 57 | - 9303e3a389b7b2ae-WAW 58 | Connection: 59 | - keep-alive 60 | Content-Type: 61 | - application/json 62 | Date: 63 | - Mon, 14 Apr 2025 14:28:22 GMT 64 | Server: 65 | - cloudflare 66 | Set-Cookie: 67 | - __cf_bm=asAq5.fAzn4GN3d3Clf7SMYh5fKPdxOfRtkg19TS.9s-1744640902-1.0.1.1-6GeWxM_pNOewi6QQm3ZMnQpdGNSsbKSpsCLUsOWp5SSzQFdqAO6AS2uDvRfde4.0faNtH5S88bAhahhTEA7rtCZ4TUXC0dKKy3Q2Uzbz5s8; 68 | path=/; expires=Mon, 14-Apr-25 14:58:22 GMT; domain=.api.openai.com; HttpOnly; 69 | Secure; SameSite=None 70 | - _cfuvid=ssFq5AKYbNGAntlXKXazI26GSxiccoFDtBHzwfl2eSk-1744640902280-0.0.1.1-604800000; 71 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 72 | Transfer-Encoding: 73 | - chunked 74 | X-Content-Type-Options: 75 | - nosniff 76 | access-control-expose-headers: 77 | - X-Request-ID 78 | alt-svc: 79 | - h3=":443"; ma=86400 80 | cf-cache-status: 81 | - DYNAMIC 82 | content-length: 83 | - '844' 84 | openai-organization: 85 | - promptlayer-qcpdch 86 | openai-processing-ms: 87 | - '378' 88 | openai-version: 89 | - '2020-10-01' 90 | strict-transport-security: 91 | - max-age=31536000; includeSubDomains; preload 92 | x-ratelimit-limit-requests: 93 | - '10000' 94 | x-ratelimit-limit-tokens: 95 | - '50000000' 96 | x-ratelimit-remaining-requests: 97 | - '9999' 98 | x-ratelimit-remaining-tokens: 99 | - '49999979' 100 | x-ratelimit-reset-requests: 101 | - 6ms 102 | x-ratelimit-reset-tokens: 103 | - 0s 104 | x-request-id: 105 | - req_06c8c965862225f458b19e76bf93ddad 106 | status: 107 | code: 200 108 | message: OK 109 | - request: 110 | body: '{"function_name": "openai.OpenAI.chat.completions.create", "provider_type": 111 | "openai", "args": [], "kwargs": {"model": "gpt-3.5-turbo", "messages": [{"role": 112 | "system", "content": "You are a helpful assistant."}, {"role": "user", "content": 113 | "What is the capital of the United States?"}]}, "tags": null, "request_response": 114 | {"id": "chatcmpl-BMF5JYzBZVfT2ZlGYxqkF28JzXY40", "choices": [{"finish_reason": 115 | "stop", "index": 0, "logprobs": null, "message": {"content": "The capital of 116 | the United States is Washington, D.C.", "refusal": null, "role": "assistant", 117 | "audio": null, "function_call": null, "tool_calls": null, "annotations": []}}], 118 | "created": 1744640901, "model": "gpt-3.5-turbo-0125", "object": "chat.completion", 119 | "service_tier": "default", "system_fingerprint": null, "usage": {"completion_tokens": 120 | 13, "prompt_tokens": 26, "total_tokens": 39, "completion_tokens_details": {"accepted_prediction_tokens": 121 | 0, "audio_tokens": 0, "reasoning_tokens": 0, "rejected_prediction_tokens": 0}, 122 | "prompt_tokens_details": {"audio_tokens": 0, "cached_tokens": 0}}}, "request_start_time": 123 | 1744640901.32603, "request_end_time": 1744640902.440252, "metadata": null, "span_id": 124 | null, "api_key": "sanitized"}' 125 | headers: 126 | Accept: 127 | - '*/*' 128 | Accept-Encoding: 129 | - gzip, deflate 130 | Connection: 131 | - keep-alive 132 | Content-Length: 133 | - '1219' 134 | Content-Type: 135 | - application/json 136 | User-Agent: 137 | - python-requests/2.31.0 138 | method: POST 139 | uri: http://localhost:8000/track-request 140 | response: 141 | body: 142 | string: '{"success":true,"request_id":142,"prompt_blueprint":null,"message":"Request 143 | tracked successfully"} 144 | 145 | ' 146 | headers: 147 | Connection: 148 | - close 149 | Content-Length: 150 | - '99' 151 | Content-Type: 152 | - application/json 153 | Date: 154 | - Mon, 14 Apr 2025 14:28:23 GMT 155 | Server: 156 | - gunicorn 157 | status: 158 | code: 200 159 | message: OK 160 | version: 1 161 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_openai_chat_completion_async.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, 4 | {"role": "user", "content": "What is the capital of Spain?"}], "model": "gpt-3.5-turbo"}' 5 | headers: 6 | Authorization: 7 | - sanitized 8 | accept: 9 | - application/json 10 | accept-encoding: 11 | - gzip, deflate 12 | connection: 13 | - keep-alive 14 | content-length: 15 | - '155' 16 | content-type: 17 | - application/json 18 | host: 19 | - api.openai.com 20 | user-agent: 21 | - AsyncOpenAI/Python 1.60.1 22 | x-stainless-arch: 23 | - x64 24 | x-stainless-async: 25 | - async:asyncio 26 | x-stainless-lang: 27 | - python 28 | x-stainless-os: 29 | - Linux 30 | x-stainless-package-version: 31 | - 1.60.1 32 | x-stainless-retry-count: 33 | - '0' 34 | x-stainless-runtime: 35 | - CPython 36 | x-stainless-runtime-version: 37 | - 3.9.21 38 | method: POST 39 | uri: https://api.openai.com/v1/chat/completions 40 | response: 41 | body: 42 | string: "{\n \"id\": \"chatcmpl-BMF5PSareXOsQYsC6m0ZnUiZe9zRF\",\n \"object\": 43 | \"chat.completion\",\n \"created\": 1744640907,\n \"model\": \"gpt-3.5-turbo-0125\",\n 44 | \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": 45 | \"assistant\",\n \"content\": \"The capital of Spain is Madrid.\",\n 46 | \ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": 47 | null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 48 | 24,\n \"completion_tokens\": 8,\n \"total_tokens\": 32,\n \"prompt_tokens_details\": 49 | {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": 50 | {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 51 | 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": 52 | \"default\",\n \"system_fingerprint\": null\n}\n" 53 | headers: 54 | CF-RAY: 55 | - 9303e3c84eaf028d-WAW 56 | Connection: 57 | - keep-alive 58 | Content-Type: 59 | - application/json 60 | Date: 61 | - Mon, 14 Apr 2025 14:28:28 GMT 62 | Server: 63 | - cloudflare 64 | Set-Cookie: 65 | - __cf_bm=yGQsBej6ZdooDKD3nbgyNAc7HEQhihhK43OTMSdmmUw-1744640908-1.0.1.1-arrO5aZwzfXaxJ0G7s6zelVqazVdgZArOdj67sqyAGAv2ksu.9g46pt.mjl37VojQV8bx0mkAYirbBsKKyvDAhRBmy2yQXK7C5AMLSv.skE; 66 | path=/; expires=Mon, 14-Apr-25 14:58:28 GMT; domain=.api.openai.com; HttpOnly; 67 | Secure; SameSite=None 68 | - _cfuvid=o9lJW29lcd4Vmp0bHZ190SPyYzO4etEkW68VLJZynO8-1744640908222-0.0.1.1-604800000; 69 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 70 | Transfer-Encoding: 71 | - chunked 72 | X-Content-Type-Options: 73 | - nosniff 74 | access-control-expose-headers: 75 | - X-Request-ID 76 | alt-svc: 77 | - h3=":443"; ma=86400 78 | cf-cache-status: 79 | - DYNAMIC 80 | content-length: 81 | - '822' 82 | openai-organization: 83 | - promptlayer-qcpdch 84 | openai-processing-ms: 85 | - '490' 86 | openai-version: 87 | - '2020-10-01' 88 | strict-transport-security: 89 | - max-age=31536000; includeSubDomains; preload 90 | x-ratelimit-limit-requests: 91 | - '10000' 92 | x-ratelimit-limit-tokens: 93 | - '50000000' 94 | x-ratelimit-remaining-requests: 95 | - '9999' 96 | x-ratelimit-remaining-tokens: 97 | - '49999982' 98 | x-ratelimit-reset-requests: 99 | - 6ms 100 | x-ratelimit-reset-tokens: 101 | - 0s 102 | x-request-id: 103 | - req_49da8b49147700184eec1d3835fff43f 104 | status: 105 | code: 200 106 | message: OK 107 | - request: 108 | body: '{"function_name": "openai.AsyncOpenAI.chat.completions.create", "provider_type": 109 | "openai", "args": [], "kwargs": {"model": "gpt-3.5-turbo", "messages": [{"role": 110 | "system", "content": "You are a helpful assistant."}, {"role": "user", "content": 111 | "What is the capital of Spain?"}]}, "tags": null, "request_response": {"id": 112 | "chatcmpl-BMF5PSareXOsQYsC6m0ZnUiZe9zRF", "choices": [{"finish_reason": "stop", 113 | "index": 0, "logprobs": null, "message": {"content": "The capital of Spain is 114 | Madrid.", "refusal": null, "role": "assistant", "audio": null, "function_call": 115 | null, "tool_calls": null, "annotations": []}}], "created": 1744640907, "model": 116 | "gpt-3.5-turbo-0125", "object": "chat.completion", "service_tier": "default", 117 | "system_fingerprint": null, "usage": {"completion_tokens": 8, "prompt_tokens": 118 | 24, "total_tokens": 32, "completion_tokens_details": {"accepted_prediction_tokens": 119 | 0, "audio_tokens": 0, "reasoning_tokens": 0, "rejected_prediction_tokens": 0}, 120 | "prompt_tokens_details": {"audio_tokens": 0, "cached_tokens": 0}}}, "request_start_time": 121 | 1744640907.141194, "request_end_time": 1744640908.310717, "metadata": null, 122 | "span_id": null, "api_key": "sanitized"}' 123 | headers: 124 | Accept: 125 | - '*/*' 126 | Accept-Encoding: 127 | - gzip, deflate 128 | Connection: 129 | - keep-alive 130 | Content-Length: 131 | - '1191' 132 | Content-Type: 133 | - application/json 134 | User-Agent: 135 | - python-requests/2.31.0 136 | method: POST 137 | uri: http://localhost:8000/track-request 138 | response: 139 | body: 140 | string: '{"success":true,"request_id":146,"prompt_blueprint":null,"message":"Request 141 | tracked successfully"} 142 | 143 | ' 144 | headers: 145 | Connection: 146 | - close 147 | Content-Length: 148 | - '99' 149 | Content-Type: 150 | - application/json 151 | Date: 152 | - Mon, 14 Apr 2025 14:28:28 GMT 153 | Server: 154 | - gunicorn 155 | status: 156 | code: 200 157 | message: OK 158 | version: 1 159 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_openai_chat_completion_async_stream_with_pl_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, 4 | {"role": "user", "content": "What is the capital of Japan?"}], "model": "gpt-3.5-turbo", 5 | "stream": true}' 6 | headers: 7 | Authorization: 8 | - sanitized 9 | accept: 10 | - application/json 11 | accept-encoding: 12 | - gzip, deflate 13 | connection: 14 | - keep-alive 15 | content-length: 16 | - '169' 17 | content-type: 18 | - application/json 19 | host: 20 | - api.openai.com 21 | user-agent: 22 | - AsyncOpenAI/Python 1.60.1 23 | x-stainless-arch: 24 | - x64 25 | x-stainless-async: 26 | - async:asyncio 27 | x-stainless-lang: 28 | - python 29 | x-stainless-os: 30 | - Linux 31 | x-stainless-package-version: 32 | - 1.60.1 33 | x-stainless-retry-count: 34 | - '0' 35 | x-stainless-runtime: 36 | - CPython 37 | x-stainless-runtime-version: 38 | - 3.9.21 39 | method: POST 40 | uri: https://api.openai.com/v1/chat/completions 41 | response: 42 | body: 43 | string: 'data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} 44 | 45 | 46 | data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}]} 47 | 48 | 49 | data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 50 | capital"},"logprobs":null,"finish_reason":null}]} 51 | 52 | 53 | data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 54 | of"},"logprobs":null,"finish_reason":null}]} 55 | 56 | 57 | data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 58 | Japan"},"logprobs":null,"finish_reason":null}]} 59 | 60 | 61 | data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 62 | is"},"logprobs":null,"finish_reason":null}]} 63 | 64 | 65 | data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 66 | Tokyo"},"logprobs":null,"finish_reason":null}]} 67 | 68 | 69 | data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} 70 | 71 | 72 | data: {"id":"chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS","object":"chat.completion.chunk","created":1744735150,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} 73 | 74 | 75 | data: [DONE] 76 | 77 | 78 | ' 79 | headers: 80 | CF-RAY: 81 | - 930ce0a26ad0bfdc-WAW 82 | Connection: 83 | - keep-alive 84 | Content-Type: 85 | - text/event-stream; charset=utf-8 86 | Date: 87 | - Tue, 15 Apr 2025 16:39:10 GMT 88 | Server: 89 | - cloudflare 90 | Set-Cookie: 91 | - __cf_bm=owHlo75MxP3eFr.9TfCGxn7wO87C61Ng3NpOFCTHSag-1744735150-1.0.1.1-1GZYNGbsUwqT.moq0QiTPuJNJRDY47FyALqZQOLJq8bUjaZaOunV3w41kj32UUiTWxY4g5xjnYf2UtrpiIfMY2hLbtTs1TqqSSRHFFU9K3c; 92 | path=/; expires=Tue, 15-Apr-25 17:09:10 GMT; domain=.api.openai.com; HttpOnly; 93 | Secure; SameSite=None 94 | - _cfuvid=u1rLvIyukh.jiMK0dFiJV8fXDI_9tldUTgg3Rtd870U-1744735150802-0.0.1.1-604800000; 95 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 96 | Transfer-Encoding: 97 | - chunked 98 | X-Content-Type-Options: 99 | - nosniff 100 | access-control-expose-headers: 101 | - X-Request-ID 102 | alt-svc: 103 | - h3=":443"; ma=86400 104 | cf-cache-status: 105 | - DYNAMIC 106 | openai-organization: 107 | - promptlayer-qcpdch 108 | openai-processing-ms: 109 | - '157' 110 | openai-version: 111 | - '2020-10-01' 112 | strict-transport-security: 113 | - max-age=31536000; includeSubDomains; preload 114 | x-ratelimit-limit-requests: 115 | - '10000' 116 | x-ratelimit-limit-tokens: 117 | - '50000000' 118 | x-ratelimit-remaining-requests: 119 | - '9999' 120 | x-ratelimit-remaining-tokens: 121 | - '49999982' 122 | x-ratelimit-reset-requests: 123 | - 6ms 124 | x-ratelimit-reset-tokens: 125 | - 0s 126 | x-request-id: 127 | - req_4894347b8de2d2c55d62b68f3115d720 128 | status: 129 | code: 200 130 | message: OK 131 | - request: 132 | body: '{"function_name": "openai.AsyncOpenAI.chat.completions.create", "provider_type": 133 | "openai", "args": [], "kwargs": {"model": "gpt-3.5-turbo", "messages": [{"role": 134 | "system", "content": "You are a helpful assistant."}, {"role": "user", "content": 135 | "What is the capital of Japan?"}], "stream": true}, "tags": null, "request_response": 136 | {"id": "chatcmpl-BMdbS6FFVXc0L0Lff4PNpdD4bBiHS", "choices": [{"role": "assistant", 137 | "content": "The capital of Japan is Tokyo."}], "created": 1744735150, "model": 138 | "gpt-3.5-turbo-0125", "object": "chat.completion.chunk", "service_tier": "default", 139 | "system_fingerprint": null, "usage": null}, "request_start_time": 1744735149.915745, 140 | "request_end_time": 1744735150.952056, "metadata": null, "span_id": null, "api_key": 141 | "sanitized"}' 142 | headers: 143 | Accept: 144 | - '*/*' 145 | Accept-Encoding: 146 | - gzip, deflate 147 | Connection: 148 | - keep-alive 149 | Content-Length: 150 | - '784' 151 | Content-Type: 152 | - application/json 153 | User-Agent: 154 | - python-requests/2.31.0 155 | method: POST 156 | uri: http://localhost:8000/track-request 157 | response: 158 | body: 159 | string: '{"success":true,"request_id":194,"prompt_blueprint":null,"message":"Request 160 | tracked successfully"} 161 | 162 | ' 163 | headers: 164 | Connection: 165 | - close 166 | Content-Length: 167 | - '99' 168 | Content-Type: 169 | - application/json 170 | Date: 171 | - Tue, 15 Apr 2025 16:39:10 GMT 172 | Server: 173 | - gunicorn 174 | status: 175 | code: 200 176 | message: OK 177 | version: 1 178 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_openai_chat_completion_with_pl_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, 4 | {"role": "user", "content": "What is the capital of France?"}], "model": "gpt-3.5-turbo"}' 5 | headers: 6 | Authorization: 7 | - sanitized 8 | accept: 9 | - application/json 10 | accept-encoding: 11 | - gzip, deflate 12 | connection: 13 | - keep-alive 14 | content-length: 15 | - '156' 16 | content-type: 17 | - application/json 18 | host: 19 | - api.openai.com 20 | user-agent: 21 | - OpenAI/Python 1.60.1 22 | x-stainless-arch: 23 | - x64 24 | x-stainless-async: 25 | - 'false' 26 | x-stainless-lang: 27 | - python 28 | x-stainless-os: 29 | - Linux 30 | x-stainless-package-version: 31 | - 1.60.1 32 | x-stainless-retry-count: 33 | - '0' 34 | x-stainless-runtime: 35 | - CPython 36 | x-stainless-runtime-version: 37 | - 3.9.21 38 | method: POST 39 | uri: https://api.openai.com/v1/chat/completions 40 | response: 41 | body: 42 | string: "{\n \"id\": \"chatcmpl-BMF5LwsMO72CHnWgcbAnjfEmQBNrD\",\n \"object\": 43 | \"chat.completion\",\n \"created\": 1744640903,\n \"model\": \"gpt-3.5-turbo-0125\",\n 44 | \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": 45 | \"assistant\",\n \"content\": \"The capital of France is Paris.\",\n 46 | \ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": 47 | null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 48 | 24,\n \"completion_tokens\": 8,\n \"total_tokens\": 32,\n \"prompt_tokens_details\": 49 | {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": 50 | {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 51 | 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": 52 | \"default\",\n \"system_fingerprint\": null\n}\n" 53 | headers: 54 | CF-RAY: 55 | - 9303e3af6cd8c400-WAW 56 | Connection: 57 | - keep-alive 58 | Content-Type: 59 | - application/json 60 | Date: 61 | - Mon, 14 Apr 2025 14:28:23 GMT 62 | Server: 63 | - cloudflare 64 | Set-Cookie: 65 | - __cf_bm=3wdOjuuM.rHpXdeK0QVNlO.JyHD39oAGuAxMasU_8rc-1744640903-1.0.1.1-NvlYqk5oPAdrFZJu1tWMvL4rOSpdI6WJPSWsurF81AfhH7_.ziZoBDXdqdkQOwWfUgiIg3N0LPq8fWmJReEi1UbFFf_yj8m_o.T.XP7SRUw; 66 | path=/; expires=Mon, 14-Apr-25 14:58:23 GMT; domain=.api.openai.com; HttpOnly; 67 | Secure; SameSite=None 68 | - _cfuvid=3H5YNE.WZQDluf1GCd9NICuuD7ok1n__xYmXczeaUr0-1744640903981-0.0.1.1-604800000; 69 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 70 | Transfer-Encoding: 71 | - chunked 72 | X-Content-Type-Options: 73 | - nosniff 74 | access-control-expose-headers: 75 | - X-Request-ID 76 | alt-svc: 77 | - h3=":443"; ma=86400 78 | cf-cache-status: 79 | - DYNAMIC 80 | content-length: 81 | - '822' 82 | openai-organization: 83 | - promptlayer-qcpdch 84 | openai-processing-ms: 85 | - '198' 86 | openai-version: 87 | - '2020-10-01' 88 | strict-transport-security: 89 | - max-age=31536000; includeSubDomains; preload 90 | x-ratelimit-limit-requests: 91 | - '10000' 92 | x-ratelimit-limit-tokens: 93 | - '50000000' 94 | x-ratelimit-remaining-requests: 95 | - '9999' 96 | x-ratelimit-remaining-tokens: 97 | - '49999981' 98 | x-ratelimit-reset-requests: 99 | - 6ms 100 | x-ratelimit-reset-tokens: 101 | - 0s 102 | x-request-id: 103 | - req_75c76b62b338f2dcbef3efa1fed3ad8f 104 | status: 105 | code: 200 106 | message: OK 107 | - request: 108 | body: '{"function_name": "openai.OpenAI.chat.completions.create", "provider_type": 109 | "openai", "args": [], "kwargs": {"model": "gpt-3.5-turbo", "messages": [{"role": 110 | "system", "content": "You are a helpful assistant."}, {"role": "user", "content": 111 | "What is the capital of France?"}]}, "tags": null, "request_response": {"id": 112 | "chatcmpl-BMF5LwsMO72CHnWgcbAnjfEmQBNrD", "choices": [{"finish_reason": "stop", 113 | "index": 0, "logprobs": null, "message": {"content": "The capital of France 114 | is Paris.", "refusal": null, "role": "assistant", "audio": null, "function_call": 115 | null, "tool_calls": null, "annotations": []}}], "created": 1744640903, "model": 116 | "gpt-3.5-turbo-0125", "object": "chat.completion", "service_tier": "default", 117 | "system_fingerprint": null, "usage": {"completion_tokens": 8, "prompt_tokens": 118 | 24, "total_tokens": 32, "completion_tokens_details": {"accepted_prediction_tokens": 119 | 0, "audio_tokens": 0, "reasoning_tokens": 0, "rejected_prediction_tokens": 0}, 120 | "prompt_tokens_details": {"audio_tokens": 0, "cached_tokens": 0}}}, "request_start_time": 121 | 1744640903.246829, "request_end_time": 1744640904.11391, "metadata": null, "span_id": 122 | null, "api_key": "sanitized"}' 123 | headers: 124 | Accept: 125 | - '*/*' 126 | Accept-Encoding: 127 | - gzip, deflate 128 | Connection: 129 | - keep-alive 130 | Content-Length: 131 | - '1186' 132 | Content-Type: 133 | - application/json 134 | User-Agent: 135 | - python-requests/2.31.0 136 | method: POST 137 | uri: http://localhost:8000/track-request 138 | response: 139 | body: 140 | string: '{"success":true,"request_id":143,"prompt_blueprint":null,"message":"Request 141 | tracked successfully"} 142 | 143 | ' 144 | headers: 145 | Connection: 146 | - close 147 | Content-Length: 148 | - '99' 149 | Content-Type: 150 | - application/json 151 | Date: 152 | - Mon, 14 Apr 2025 14:28:24 GMT 153 | Server: 154 | - gunicorn 155 | status: 156 | code: 200 157 | message: OK 158 | version: 1 159 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_openai_chat_completion_with_stream.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, 4 | {"role": "user", "content": "What is the capital of Germany?"}], "model": "gpt-3.5-turbo", 5 | "stream": true}' 6 | headers: 7 | Authorization: 8 | - sanitized 9 | accept: 10 | - application/json 11 | accept-encoding: 12 | - gzip, deflate 13 | connection: 14 | - keep-alive 15 | content-length: 16 | - '171' 17 | content-type: 18 | - application/json 19 | host: 20 | - api.openai.com 21 | user-agent: 22 | - OpenAI/Python 1.60.1 23 | x-stainless-arch: 24 | - x64 25 | x-stainless-async: 26 | - 'false' 27 | x-stainless-lang: 28 | - python 29 | x-stainless-os: 30 | - Linux 31 | x-stainless-package-version: 32 | - 1.60.1 33 | x-stainless-retry-count: 34 | - '0' 35 | x-stainless-runtime: 36 | - CPython 37 | x-stainless-runtime-version: 38 | - 3.9.21 39 | method: POST 40 | uri: https://api.openai.com/v1/chat/completions 41 | response: 42 | body: 43 | string: 'data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} 44 | 45 | 46 | data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}]} 47 | 48 | 49 | data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 50 | capital"},"logprobs":null,"finish_reason":null}]} 51 | 52 | 53 | data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 54 | of"},"logprobs":null,"finish_reason":null}]} 55 | 56 | 57 | data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 58 | Germany"},"logprobs":null,"finish_reason":null}]} 59 | 60 | 61 | data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 62 | is"},"logprobs":null,"finish_reason":null}]} 63 | 64 | 65 | data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 66 | Berlin"},"logprobs":null,"finish_reason":null}]} 67 | 68 | 69 | data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} 70 | 71 | 72 | data: {"id":"chatcmpl-BMF5NbakpXjwzGqWVrdgpWmPl3maf","object":"chat.completion.chunk","created":1744640905,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} 73 | 74 | 75 | data: [DONE] 76 | 77 | 78 | ' 79 | headers: 80 | CF-RAY: 81 | - 9303e3b97d9aee43-WAW 82 | Connection: 83 | - keep-alive 84 | Content-Type: 85 | - text/event-stream; charset=utf-8 86 | Date: 87 | - Mon, 14 Apr 2025 14:28:25 GMT 88 | Server: 89 | - cloudflare 90 | Set-Cookie: 91 | - __cf_bm=xMMqpDvXgNP3YoKXMISg5xOd1C3mTCpGqaiUMwsha6E-1744640905-1.0.1.1-LzdGOmaDopBp1uyC0uWC8N0Sj3zAX1fkvTd1HCGycCm2CcVQqVY.Rb.4TPW0XtqyfWQVb5Nu0SvVyWqTfMvN50tYEEApS_TDy6cK.fp2fBA; 92 | path=/; expires=Mon, 14-Apr-25 14:58:25 GMT; domain=.api.openai.com; HttpOnly; 93 | Secure; SameSite=None 94 | - _cfuvid=uYNVMf0snr9XwFkTi4vdpPBn6P4TsPZ4NvLIFmWzLxo-1744640905801-0.0.1.1-604800000; 95 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 96 | Transfer-Encoding: 97 | - chunked 98 | X-Content-Type-Options: 99 | - nosniff 100 | access-control-expose-headers: 101 | - X-Request-ID 102 | alt-svc: 103 | - h3=":443"; ma=86400 104 | cf-cache-status: 105 | - DYNAMIC 106 | openai-organization: 107 | - promptlayer-qcpdch 108 | openai-processing-ms: 109 | - '166' 110 | openai-version: 111 | - '2020-10-01' 112 | strict-transport-security: 113 | - max-age=31536000; includeSubDomains; preload 114 | x-ratelimit-limit-requests: 115 | - '10000' 116 | x-ratelimit-limit-tokens: 117 | - '50000000' 118 | x-ratelimit-remaining-requests: 119 | - '9999' 120 | x-ratelimit-remaining-tokens: 121 | - '49999982' 122 | x-ratelimit-reset-requests: 123 | - 6ms 124 | x-ratelimit-reset-tokens: 125 | - 0s 126 | x-request-id: 127 | - req_2dcac69f7e0b12ae13eeaf140154d64a 128 | status: 129 | code: 200 130 | message: OK 131 | version: 1 132 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_openai_chat_completion_with_stream_and_pl_id.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, 4 | {"role": "user", "content": "What is the capital of Italy?"}], "model": "gpt-3.5-turbo", 5 | "stream": true}' 6 | headers: 7 | Authorization: 8 | - sanitized 9 | accept: 10 | - application/json 11 | accept-encoding: 12 | - gzip, deflate 13 | connection: 14 | - keep-alive 15 | content-length: 16 | - '169' 17 | content-type: 18 | - application/json 19 | host: 20 | - api.openai.com 21 | user-agent: 22 | - OpenAI/Python 1.60.1 23 | x-stainless-arch: 24 | - x64 25 | x-stainless-async: 26 | - 'false' 27 | x-stainless-lang: 28 | - python 29 | x-stainless-os: 30 | - Linux 31 | x-stainless-package-version: 32 | - 1.60.1 33 | x-stainless-retry-count: 34 | - '0' 35 | x-stainless-runtime: 36 | - CPython 37 | x-stainless-runtime-version: 38 | - 3.9.21 39 | method: POST 40 | uri: https://api.openai.com/v1/chat/completions 41 | response: 42 | body: 43 | string: 'data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} 44 | 45 | 46 | data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}]} 47 | 48 | 49 | data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 50 | capital"},"logprobs":null,"finish_reason":null}]} 51 | 52 | 53 | data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 54 | of"},"logprobs":null,"finish_reason":null}]} 55 | 56 | 57 | data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 58 | Italy"},"logprobs":null,"finish_reason":null}]} 59 | 60 | 61 | data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 62 | is"},"logprobs":null,"finish_reason":null}]} 63 | 64 | 65 | data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" 66 | Rome"},"logprobs":null,"finish_reason":null}]} 67 | 68 | 69 | data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} 70 | 71 | 72 | data: {"id":"chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR","object":"chat.completion.chunk","created":1744735149,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} 73 | 74 | 75 | data: [DONE] 76 | 77 | 78 | ' 79 | headers: 80 | CF-RAY: 81 | - 930ce09aae13bfbc-WAW 82 | Connection: 83 | - keep-alive 84 | Content-Type: 85 | - text/event-stream; charset=utf-8 86 | Date: 87 | - Tue, 15 Apr 2025 16:39:09 GMT 88 | Server: 89 | - cloudflare 90 | Set-Cookie: 91 | - __cf_bm=iGtr_.vGSmOCmTF9hbdxNTjlcDfcQehHfMag920d_aQ-1744735149-1.0.1.1-wCMVcusdGOgqys.g9R5OpprsTBuGJMzZGe_BKMH1Or.Mb_mf11xbysmuTSlkmgQA0cuOtNbGm_3ORj2agRByRKu.QBLhMBd4NMOUlKYK.B4; 92 | path=/; expires=Tue, 15-Apr-25 17:09:09 GMT; domain=.api.openai.com; HttpOnly; 93 | Secure; SameSite=None 94 | - _cfuvid=8PlnNLIPphAcygLXAO0anovDq9JtTy5Ct6To3J2Vbwo-1744735149686-0.0.1.1-604800000; 95 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 96 | Transfer-Encoding: 97 | - chunked 98 | X-Content-Type-Options: 99 | - nosniff 100 | access-control-expose-headers: 101 | - X-Request-ID 102 | alt-svc: 103 | - h3=":443"; ma=86400 104 | cf-cache-status: 105 | - DYNAMIC 106 | openai-organization: 107 | - promptlayer-qcpdch 108 | openai-processing-ms: 109 | - '157' 110 | openai-version: 111 | - '2020-10-01' 112 | strict-transport-security: 113 | - max-age=31536000; includeSubDomains; preload 114 | x-ratelimit-limit-requests: 115 | - '10000' 116 | x-ratelimit-limit-tokens: 117 | - '50000000' 118 | x-ratelimit-remaining-requests: 119 | - '9999' 120 | x-ratelimit-remaining-tokens: 121 | - '49999982' 122 | x-ratelimit-reset-requests: 123 | - 6ms 124 | x-ratelimit-reset-tokens: 125 | - 0s 126 | x-request-id: 127 | - req_1b7e6816013de365ec635a31eeb2da14 128 | status: 129 | code: 200 130 | message: OK 131 | - request: 132 | body: '{"function_name": "openai.OpenAI.chat.completions.create", "provider_type": 133 | "openai", "args": [], "kwargs": {"model": "gpt-3.5-turbo", "messages": [{"role": 134 | "system", "content": "You are a helpful assistant."}, {"role": "user", "content": 135 | "What is the capital of Italy?"}], "stream": true}, "tags": null, "request_response": 136 | {"id": "chatcmpl-BMdbRwh3IezC3YV86Kga7wqbM1eTR", "choices": [{"role": "assistant", 137 | "content": "The capital of Italy is Rome."}], "created": 1744735149, "model": 138 | "gpt-3.5-turbo-0125", "object": "chat.completion.chunk", "service_tier": "default", 139 | "system_fingerprint": null, "usage": null}, "request_start_time": 1744735148.737197, 140 | "request_end_time": 1744735149.818011, "metadata": null, "span_id": null, "api_key": 141 | "sanitized"}' 142 | headers: 143 | Accept: 144 | - '*/*' 145 | Accept-Encoding: 146 | - gzip, deflate 147 | Connection: 148 | - keep-alive 149 | Content-Length: 150 | - '778' 151 | Content-Type: 152 | - application/json 153 | User-Agent: 154 | - python-requests/2.31.0 155 | method: POST 156 | uri: http://localhost:8000/track-request 157 | response: 158 | body: 159 | string: '{"success":true,"request_id":193,"prompt_blueprint":null,"message":"Request 160 | tracked successfully"} 161 | 162 | ' 163 | headers: 164 | Connection: 165 | - close 166 | Content-Length: 167 | - '99' 168 | Content-Type: 169 | - application/json 170 | Date: 171 | - Tue, 15 Apr 2025 16:39:09 GMT 172 | Server: 173 | - gunicorn 174 | status: 175 | code: 200 176 | message: OK 177 | version: 1 178 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_publish_template_async.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"prompt_template": {"prompt_name": "sample_template", "prompt_template": 4 | {"dataset_examples": [], "function_call": "none", "functions": [], "input_variables": 5 | [], "messages": [{"content": [{"text": "", "type": "text"}], "dataset_examples": 6 | [], "input_variables": [], "name": null, "raw_request_display_role": "", "role": 7 | "system", "template_format": "f-string"}, {"content": [{"text": "What is the 8 | capital of Japan?", "type": "text"}], "dataset_examples": [], "input_variables": 9 | [], "name": null, "raw_request_display_role": "", "role": "user", "template_format": 10 | "f-string"}], "tool_choice": null, "tools": null, "type": "chat"}, "tags": ["test"], 11 | "commit_message": "test", "metadata": {"model": {"name": "gpt-4o-mini", "provider": 12 | "openai", "parameters": {"frequency_penalty": 0, "max_tokens": 256, "messages": 13 | [{"content": "Hello", "role": "system"}], "model": "gpt-4o", "presence_penalty": 14 | 0, "seed": 0, "temperature": 1, "top_p": 1}}}}, "prompt_version": {"prompt_name": 15 | "sample_template", "prompt_template": {"dataset_examples": [], "function_call": 16 | "none", "functions": [], "input_variables": [], "messages": [{"content": [{"text": 17 | "", "type": "text"}], "dataset_examples": [], "input_variables": [], "name": 18 | null, "raw_request_display_role": "", "role": "system", "template_format": "f-string"}, 19 | {"content": [{"text": "What is the capital of Japan?", "type": "text"}], "dataset_examples": 20 | [], "input_variables": [], "name": null, "raw_request_display_role": "", "role": 21 | "user", "template_format": "f-string"}], "tool_choice": null, "tools": null, 22 | "type": "chat"}, "tags": ["test"], "commit_message": "test", "metadata": {"model": 23 | {"name": "gpt-4o-mini", "provider": "openai", "parameters": {"frequency_penalty": 24 | 0, "max_tokens": 256, "messages": [{"content": "Hello", "role": "system"}], 25 | "model": "gpt-4o", "presence_penalty": 0, "seed": 0, "temperature": 1, "top_p": 26 | 1}}}}, "release_labels": null}' 27 | headers: 28 | Accept: 29 | - '*/*' 30 | Accept-Encoding: 31 | - gzip, deflate 32 | Connection: 33 | - keep-alive 34 | Content-Length: 35 | - '1907' 36 | Content-Type: 37 | - application/json 38 | User-Agent: 39 | - python-requests/2.31.0 40 | x-api-key: 41 | - sanitized 42 | method: POST 43 | uri: http://localhost:8000/rest/prompt-templates 44 | response: 45 | body: 46 | string: '{"id":4,"prompt_name":"sample_template","tags":["test"],"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 47 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"commit_message":"test","metadata":{"model":{"provider":"openai","name":"gpt-4o-mini","parameters":{"frequency_penalty":0,"max_tokens":256,"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","presence_penalty":0,"seed":0,"temperature":1,"top_p":1}}},"release_labels":null} 48 | 49 | ' 50 | headers: 51 | Connection: 52 | - close 53 | Content-Length: 54 | - '870' 55 | Content-Type: 56 | - application/json 57 | Date: 58 | - Mon, 14 Apr 2025 08:48:13 GMT 59 | Server: 60 | - gunicorn 61 | status: 62 | code: 201 63 | message: CREATED 64 | version: 1 65 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_run_prompt_async.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"api_key": "sanitized"}' 4 | headers: 5 | accept: 6 | - '*/*' 7 | accept-encoding: 8 | - gzip, deflate 9 | connection: 10 | - keep-alive 11 | content-length: 12 | - '49' 13 | content-type: 14 | - application/json 15 | host: 16 | - localhost:8000 17 | user-agent: 18 | - python-httpx/0.28.1 19 | x-api-key: 20 | - sanitized 21 | method: POST 22 | uri: http://localhost:8000/prompt-templates/sample_template 23 | response: 24 | body: 25 | string: '{"id":4,"prompt_name":"sample_template","tags":["test"],"workspace_id":1,"commit_message":"test","metadata":{"model":{"provider":"openai","name":"gpt-4o-mini","parameters":{"frequency_penalty":0,"max_tokens":256,"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","presence_penalty":0,"seed":0,"temperature":1,"top_p":1}}},"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 26 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"llm_kwargs":{"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","frequency_penalty":0,"max_tokens":256,"presence_penalty":0,"seed":0,"temperature":1,"top_p":1},"provider_base_url":null,"version":1,"snippets":[],"warning":null} 27 | 28 | ' 29 | headers: 30 | Connection: 31 | - close 32 | Content-Length: 33 | - '1107' 34 | Content-Type: 35 | - application/json 36 | Date: 37 | - Mon, 14 Apr 2025 10:00:25 GMT 38 | Server: 39 | - gunicorn 40 | status: 41 | code: 200 42 | message: OK 43 | - request: 44 | body: '{"messages": [{"content": "Hello", "role": "system"}], "model": "gpt-4o", 45 | "frequency_penalty": 0, "max_tokens": 256, "presence_penalty": 0, "seed": 0, 46 | "stream": false, "temperature": 1, "top_p": 1}' 47 | headers: 48 | Authorization: 49 | - sanitized 50 | accept: 51 | - application/json 52 | accept-encoding: 53 | - gzip, deflate 54 | connection: 55 | - keep-alive 56 | content-length: 57 | - '177' 58 | content-type: 59 | - application/json 60 | host: 61 | - api.openai.com 62 | user-agent: 63 | - AsyncOpenAI/Python 1.60.1 64 | x-stainless-arch: 65 | - x64 66 | x-stainless-async: 67 | - async:asyncio 68 | x-stainless-lang: 69 | - python 70 | x-stainless-os: 71 | - Linux 72 | x-stainless-package-version: 73 | - 1.60.1 74 | x-stainless-retry-count: 75 | - '0' 76 | x-stainless-runtime: 77 | - CPython 78 | x-stainless-runtime-version: 79 | - 3.9.21 80 | method: POST 81 | uri: https://api.openai.com/v1/chat/completions 82 | response: 83 | body: 84 | string: "{\n \"id\": \"chatcmpl-BMAu3fBRyPYaswyFgBBnBQcB0YxUK\",\n \"object\": 85 | \"chat.completion\",\n \"created\": 1744624827,\n \"model\": \"gpt-4o-2024-08-06\",\n 86 | \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": 87 | \"assistant\",\n \"content\": \"Hello! Yes, I'm trained on data up 88 | to October 2023. How can I assist you today?\",\n \"refusal\": null,\n 89 | \ \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": 90 | \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 8,\n \"completion_tokens\": 91 | 23,\n \"total_tokens\": 31,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 92 | 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": 93 | {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 94 | 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": 95 | \"default\",\n \"system_fingerprint\": \"fp_92f14e8683\"\n}\n" 96 | headers: 97 | CF-RAY: 98 | - 93025b2f48841df9-WAW 99 | Connection: 100 | - keep-alive 101 | Content-Type: 102 | - application/json 103 | Date: 104 | - Mon, 14 Apr 2025 10:00:27 GMT 105 | Server: 106 | - cloudflare 107 | Set-Cookie: 108 | - __cf_bm=WoDMvktgogHkKC5dtVRbodUbJxV4Bc4sWuN0QjW1jm8-1744624827-1.0.1.1-9ZBIg_wk7E3fLa9s2.rAu96n.Ev9yOJCKpbuekBbDfGNAgqtPe9902bf5B61WTuP8z6zGS1vjqA8zsPJkBLnMwvq0JgYJ8Z9JTNzm_YjyJw; 109 | path=/; expires=Mon, 14-Apr-25 10:30:27 GMT; domain=.api.openai.com; HttpOnly; 110 | Secure; SameSite=None 111 | - _cfuvid=eHe50Lx9pdivakzkzleBOuxU1.KM94YO_a2dXQl8l_A-1744624827668-0.0.1.1-604800000; 112 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 113 | Transfer-Encoding: 114 | - chunked 115 | X-Content-Type-Options: 116 | - nosniff 117 | access-control-expose-headers: 118 | - X-Request-ID 119 | alt-svc: 120 | - h3=":443"; ma=86400 121 | cf-cache-status: 122 | - DYNAMIC 123 | content-length: 124 | - '880' 125 | openai-organization: 126 | - promptlayer-qcpdch 127 | openai-processing-ms: 128 | - '507' 129 | openai-version: 130 | - '2020-10-01' 131 | strict-transport-security: 132 | - max-age=31536000; includeSubDomains; preload 133 | x-ratelimit-limit-requests: 134 | - '50000' 135 | x-ratelimit-limit-tokens: 136 | - '150000000' 137 | x-ratelimit-remaining-requests: 138 | - '49999' 139 | x-ratelimit-remaining-tokens: 140 | - '149999995' 141 | x-ratelimit-reset-requests: 142 | - 1ms 143 | x-ratelimit-reset-tokens: 144 | - 0s 145 | x-request-id: 146 | - req_bd8a9e42286b527e321d668846c705a2 147 | status: 148 | code: 200 149 | message: OK 150 | - request: 151 | body: '{"function_name": "openai.chat.completions.create", "provider_type": "openai", 152 | "args": [], "kwargs": {"messages": [{"content": "Hello", "role": "system"}], 153 | "model": "gpt-4o", "frequency_penalty": 0, "max_tokens": 256, "presence_penalty": 154 | 0, "seed": 0, "temperature": 1, "top_p": 1, "stream": false}, "tags": null, 155 | "request_start_time": 1744624827.799539, "request_end_time": 1744624827.799546, 156 | "metadata": null, "prompt_id": 4, "prompt_version": 1, "prompt_input_variables": 157 | {}, "group_id": null, "return_prompt_blueprint": true, "span_id": null, "request_response": 158 | {"id": "chatcmpl-BMAu3fBRyPYaswyFgBBnBQcB0YxUK", "choices": [{"finish_reason": 159 | "stop", "index": 0, "logprobs": null, "message": {"content": "Hello! Yes, I''m 160 | trained on data up to October 2023. How can I assist you today?", "refusal": 161 | null, "role": "assistant", "audio": null, "function_call": null, "tool_calls": 162 | null, "annotations": []}}], "created": 1744624827, "model": "gpt-4o-2024-08-06", 163 | "object": "chat.completion", "service_tier": "default", "system_fingerprint": 164 | "fp_92f14e8683", "usage": {"completion_tokens": 23, "prompt_tokens": 8, "total_tokens": 165 | 31, "completion_tokens_details": {"accepted_prediction_tokens": 0, "audio_tokens": 166 | 0, "reasoning_tokens": 0, "rejected_prediction_tokens": 0}, "prompt_tokens_details": 167 | {"audio_tokens": 0, "cached_tokens": 0}}}, "api_key": "sanitized"}' 168 | headers: 169 | accept: 170 | - '*/*' 171 | accept-encoding: 172 | - gzip, deflate 173 | connection: 174 | - keep-alive 175 | content-length: 176 | - '1282' 177 | content-type: 178 | - application/json 179 | host: 180 | - localhost:8000 181 | user-agent: 182 | - python-httpx/0.28.1 183 | method: POST 184 | uri: http://localhost:8000/track-request 185 | response: 186 | body: 187 | string: '{"success":true,"request_id":129,"prompt_blueprint":{"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"Hello"}],"raw_request_display_role":"system","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"Hello! 188 | Yes, I''m trained on data up to October 2023. How can I assist you today?"}],"raw_request_display_role":"assistant","dataset_examples":[],"role":"assistant","function_call":null,"name":null,"tool_calls":null}],"functions":[],"tools":[],"function_call":null,"tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"commit_message":null,"metadata":{"model":{"provider":"openai","name":"gpt-4o","parameters":{"frequency_penalty":0,"max_tokens":256,"presence_penalty":0,"seed":0,"temperature":1,"top_p":1,"stream":false}}},"provider_base_url_name":null,"report_id":null,"inference_client_name":null},"message":"Request 189 | tracked successfully"} 190 | 191 | ' 192 | headers: 193 | Connection: 194 | - close 195 | Content-Length: 196 | - '1015' 197 | Content-Type: 198 | - application/json 199 | Date: 200 | - Mon, 14 Apr 2025 10:00:27 GMT 201 | Server: 202 | - gunicorn 203 | status: 204 | code: 200 205 | message: OK 206 | version: 1 207 | -------------------------------------------------------------------------------- /tests/fixtures/cassettes/test_track_and_templates.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"provider": "openai", "model": "gpt-3.5-turbo", "api_key": "sanitized"}' 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Connection: 10 | - keep-alive 11 | Content-Length: 12 | - '98' 13 | Content-Type: 14 | - application/json 15 | User-Agent: 16 | - python-requests/2.31.0 17 | x-api-key: 18 | - sanitized 19 | method: POST 20 | uri: http://localhost:8000/prompt-templates/sample_template 21 | response: 22 | body: 23 | string: '{"id":4,"prompt_name":"sample_template","tags":["test"],"workspace_id":1,"commit_message":"test","metadata":{"model":{"provider":"openai","name":"gpt-4o-mini","parameters":{"frequency_penalty":0,"max_tokens":256,"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","presence_penalty":0,"seed":0,"temperature":1,"top_p":1}}},"prompt_template":{"messages":[{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":""}],"raw_request_display_role":"","dataset_examples":[],"role":"system","name":null},{"input_variables":[],"template_format":"f-string","content":[{"type":"text","text":"What 24 | is the capital of Japan?"}],"raw_request_display_role":"","dataset_examples":[],"role":"user","name":null}],"functions":[],"tools":null,"function_call":"none","tool_choice":null,"type":"chat","input_variables":[],"dataset_examples":[]},"llm_kwargs":{"messages":[{"content":"Hello","role":"system"}],"model":"gpt-4o","frequency_penalty":0,"max_tokens":256,"presence_penalty":0,"seed":0,"temperature":1,"top_p":1},"provider_base_url":null,"version":1,"snippets":[],"warning":null} 25 | 26 | ' 27 | headers: 28 | Connection: 29 | - close 30 | Content-Length: 31 | - '1107' 32 | Content-Type: 33 | - application/json 34 | Date: 35 | - Mon, 14 Apr 2025 10:48:24 GMT 36 | Server: 37 | - gunicorn 38 | status: 39 | code: 200 40 | message: OK 41 | - request: 42 | body: '{"messages": [{"content": "Hello", "role": "system"}], "model": "gpt-3.5-turbo", 43 | "frequency_penalty": 0, "max_tokens": 256, "presence_penalty": 0, "seed": 0, 44 | "temperature": 1, "top_p": 1}' 45 | headers: 46 | Authorization: 47 | - sanitized 48 | accept: 49 | - application/json 50 | accept-encoding: 51 | - gzip, deflate 52 | connection: 53 | - keep-alive 54 | content-length: 55 | - '169' 56 | content-type: 57 | - application/json 58 | host: 59 | - api.openai.com 60 | user-agent: 61 | - OpenAI/Python 1.60.1 62 | x-stainless-arch: 63 | - x64 64 | x-stainless-async: 65 | - 'false' 66 | x-stainless-lang: 67 | - python 68 | x-stainless-os: 69 | - Linux 70 | x-stainless-package-version: 71 | - 1.60.1 72 | x-stainless-retry-count: 73 | - '0' 74 | x-stainless-runtime: 75 | - CPython 76 | x-stainless-runtime-version: 77 | - 3.9.21 78 | method: POST 79 | uri: https://api.openai.com/v1/chat/completions 80 | response: 81 | body: 82 | string: "{\n \"id\": \"chatcmpl-BMBeSHzYtFfWOaJukmxcQxflVDr5S\",\n \"object\": 83 | \"chat.completion\",\n \"created\": 1744627704,\n \"model\": \"gpt-3.5-turbo-0125\",\n 84 | \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": 85 | \"assistant\",\n \"content\": \"Hello! How can I assist you today?\",\n 86 | \ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": 87 | null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 88 | 8,\n \"completion_tokens\": 10,\n \"total_tokens\": 18,\n \"prompt_tokens_details\": 89 | {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": 90 | {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 91 | 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": 92 | \"default\",\n \"system_fingerprint\": null\n}\n" 93 | headers: 94 | CF-RAY: 95 | - 9302a1731995b243-WAW 96 | Connection: 97 | - keep-alive 98 | Content-Type: 99 | - application/json 100 | Date: 101 | - Mon, 14 Apr 2025 10:48:25 GMT 102 | Server: 103 | - cloudflare 104 | Set-Cookie: 105 | - __cf_bm=dkxNTdwzyxu8C_XBrYGCBAIev6t5Rb9LOD0RScWu3Hw-1744627705-1.0.1.1-2P3mi4WpCJJUE82kaE7nJw1gaDRRSj.wiDv9ZhaeWkxJerxuvAb.AWlvlLTJg41n3_56LUg_bKyWkJzRvMBL93JV1q.IZQ8Fg3Heo.DAHas; 106 | path=/; expires=Mon, 14-Apr-25 11:18:25 GMT; domain=.api.openai.com; HttpOnly; 107 | Secure; SameSite=None 108 | - _cfuvid=r5S0oTEM7vtkEpEEeOj.TuhQ5P0mhmRWZY87g0PtRXw-1744627705219-0.0.1.1-604800000; 109 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 110 | Transfer-Encoding: 111 | - chunked 112 | X-Content-Type-Options: 113 | - nosniff 114 | access-control-expose-headers: 115 | - X-Request-ID 116 | alt-svc: 117 | - h3=":443"; ma=86400 118 | cf-cache-status: 119 | - DYNAMIC 120 | content-length: 121 | - '825' 122 | openai-organization: 123 | - promptlayer-qcpdch 124 | openai-processing-ms: 125 | - '229' 126 | openai-version: 127 | - '2020-10-01' 128 | strict-transport-security: 129 | - max-age=31536000; includeSubDomains; preload 130 | x-ratelimit-limit-requests: 131 | - '10000' 132 | x-ratelimit-limit-tokens: 133 | - '50000000' 134 | x-ratelimit-remaining-requests: 135 | - '9999' 136 | x-ratelimit-remaining-tokens: 137 | - '49999995' 138 | x-ratelimit-reset-requests: 139 | - 6ms 140 | x-ratelimit-reset-tokens: 141 | - 0s 142 | x-request-id: 143 | - req_86424c52e8d7f558b64fce8ed9457451 144 | status: 145 | code: 200 146 | message: OK 147 | - request: 148 | body: '{"function_name": "openai.OpenAI.chat.completions.create", "provider_type": 149 | "openai", "args": [], "kwargs": {"model": "gpt-3.5-turbo", "messages": [{"content": 150 | "Hello", "role": "system"}], "frequency_penalty": 0, "max_tokens": 256, "presence_penalty": 151 | 0, "seed": 0, "temperature": 1, "top_p": 1}, "tags": null, "request_response": 152 | {"id": "chatcmpl-BMBeSHzYtFfWOaJukmxcQxflVDr5S", "choices": [{"finish_reason": 153 | "stop", "index": 0, "logprobs": null, "message": {"content": "Hello! How can 154 | I assist you today?", "refusal": null, "role": "assistant", "audio": null, "function_call": 155 | null, "tool_calls": null, "annotations": []}}], "created": 1744627704, "model": 156 | "gpt-3.5-turbo-0125", "object": "chat.completion", "service_tier": "default", 157 | "system_fingerprint": null, "usage": {"completion_tokens": 10, "prompt_tokens": 158 | 8, "total_tokens": 18, "completion_tokens_details": {"accepted_prediction_tokens": 159 | 0, "audio_tokens": 0, "reasoning_tokens": 0, "rejected_prediction_tokens": 0}, 160 | "prompt_tokens_details": {"audio_tokens": 0, "cached_tokens": 0}}}, "request_start_time": 161 | 1744627704.311881, "request_end_time": 1744627705.33816, "metadata": null, "span_id": 162 | null, "api_key": "sanitized"}' 163 | headers: 164 | Accept: 165 | - '*/*' 166 | Accept-Encoding: 167 | - gzip, deflate 168 | Connection: 169 | - keep-alive 170 | Content-Length: 171 | - '1210' 172 | Content-Type: 173 | - application/json 174 | User-Agent: 175 | - python-requests/2.31.0 176 | method: POST 177 | uri: http://localhost:8000/track-request 178 | response: 179 | body: 180 | string: '{"success":true,"request_id":132,"prompt_blueprint":null,"message":"Request 181 | tracked successfully"} 182 | 183 | ' 184 | headers: 185 | Connection: 186 | - close 187 | Content-Length: 188 | - '99' 189 | Content-Type: 190 | - application/json 191 | Date: 192 | - Mon, 14 Apr 2025 10:48:25 GMT 193 | Server: 194 | - gunicorn 195 | status: 196 | code: 200 197 | message: OK 198 | - request: 199 | body: '{"request_id": 132, "score": 10, "name": "accuracy", "api_key": "sanitized"}' 200 | headers: 201 | Accept: 202 | - '*/*' 203 | Accept-Encoding: 204 | - gzip, deflate 205 | Connection: 206 | - keep-alive 207 | Content-Length: 208 | - '102' 209 | Content-Type: 210 | - application/json 211 | User-Agent: 212 | - python-requests/2.31.0 213 | method: POST 214 | uri: http://localhost:8000/library-track-score 215 | response: 216 | body: 217 | string: '{"success":true} 218 | 219 | ' 220 | headers: 221 | Connection: 222 | - close 223 | Content-Length: 224 | - '17' 225 | Content-Type: 226 | - application/json 227 | Date: 228 | - Mon, 14 Apr 2025 10:48:25 GMT 229 | Server: 230 | - gunicorn 231 | status: 232 | code: 200 233 | message: OK 234 | - request: 235 | body: '{"request_id": 132, "metadata": {"test": "test"}, "api_key": "sanitized"}' 236 | headers: 237 | Accept: 238 | - '*/*' 239 | Accept-Encoding: 240 | - gzip, deflate 241 | Connection: 242 | - keep-alive 243 | Content-Length: 244 | - '99' 245 | Content-Type: 246 | - application/json 247 | User-Agent: 248 | - python-requests/2.31.0 249 | method: POST 250 | uri: http://localhost:8000/library-track-metadata 251 | response: 252 | body: 253 | string: '{"success":true} 254 | 255 | ' 256 | headers: 257 | Connection: 258 | - close 259 | Content-Length: 260 | - '17' 261 | Content-Type: 262 | - application/json 263 | Date: 264 | - Mon, 14 Apr 2025 10:48:25 GMT 265 | Server: 266 | - gunicorn 267 | status: 268 | code: 200 269 | message: OK 270 | - request: 271 | body: '{"api_key": "sanitized"}' 272 | headers: 273 | Accept: 274 | - '*/*' 275 | Accept-Encoding: 276 | - gzip, deflate 277 | Connection: 278 | - keep-alive 279 | Content-Length: 280 | - '50' 281 | Content-Type: 282 | - application/json 283 | User-Agent: 284 | - python-requests/2.31.0 285 | method: POST 286 | uri: http://localhost:8000/create-group 287 | response: 288 | body: 289 | string: '{"success":true,"id":1} 290 | 291 | ' 292 | headers: 293 | Connection: 294 | - close 295 | Content-Length: 296 | - '24' 297 | Content-Type: 298 | - application/json 299 | Date: 300 | - Mon, 14 Apr 2025 10:48:25 GMT 301 | Server: 302 | - gunicorn 303 | status: 304 | code: 200 305 | message: OK 306 | - request: 307 | body: '{"request_id": 132, "group_id": 1, "api_key": "sanitized"}' 308 | headers: 309 | Accept: 310 | - '*/*' 311 | Accept-Encoding: 312 | - gzip, deflate 313 | Connection: 314 | - keep-alive 315 | Content-Length: 316 | - '84' 317 | Content-Type: 318 | - application/json 319 | User-Agent: 320 | - python-requests/2.31.0 321 | method: POST 322 | uri: http://localhost:8000/track-group 323 | response: 324 | body: 325 | string: '{"success":true} 326 | 327 | ' 328 | headers: 329 | Connection: 330 | - close 331 | Content-Length: 332 | - '17' 333 | Content-Type: 334 | - application/json 335 | Date: 336 | - Mon, 14 Apr 2025 10:48:25 GMT 337 | Server: 338 | - gunicorn 339 | status: 340 | code: 200 341 | message: OK 342 | version: 1 343 | -------------------------------------------------------------------------------- /tests/fixtures/clients.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from promptlayer import AsyncPromptLayer, PromptLayer 4 | 5 | 6 | @pytest.fixture 7 | def promptlayer_client(promptlayer_api_key): 8 | return PromptLayer(api_key=promptlayer_api_key) 9 | 10 | 11 | @pytest.fixture 12 | def promptlayer_async_client(promptlayer_api_key): 13 | return AsyncPromptLayer(api_key=promptlayer_api_key) 14 | 15 | 16 | @pytest.fixture 17 | def anthropic_client(promptlayer_client, anthropic_api_key): 18 | return promptlayer_client.anthropic.Anthropic(api_key=anthropic_api_key) 19 | 20 | 21 | @pytest.fixture 22 | def anthropic_async_client(promptlayer_client, anthropic_api_key): 23 | return promptlayer_client.anthropic.AsyncAnthropic(api_key=anthropic_api_key) 24 | 25 | 26 | @pytest.fixture 27 | def openai_client(promptlayer_client, openai_api_key): 28 | return promptlayer_client.openai.OpenAI(api_key=openai_api_key) 29 | 30 | 31 | @pytest.fixture 32 | def openai_async_client(promptlayer_client, openai_api_key): 33 | return promptlayer_client.openai.AsyncOpenAI(api_key=openai_api_key) 34 | -------------------------------------------------------------------------------- /tests/fixtures/setup.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | 5 | from tests.utils.vcr import is_cassette_recording 6 | 7 | if is_cassette_recording(): 8 | 9 | @pytest.fixture 10 | def autouse_disable_network(): 11 | return 12 | else: 13 | 14 | @pytest.fixture(autouse=True) 15 | def autouse_disable_network(disable_network): 16 | yield 17 | 18 | 19 | @pytest.fixture(scope="session", autouse=True) 20 | def setup(): 21 | with patch("promptlayer.utils.URL_API_PROMPTLAYER", "http://localhost:8000"): 22 | yield 23 | -------------------------------------------------------------------------------- /tests/fixtures/templates.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def sample_template_name(): 6 | return "sample_template" 7 | 8 | 9 | @pytest.fixture 10 | def sample_template_content(): 11 | return { 12 | "dataset_examples": [], 13 | "function_call": "none", 14 | "functions": [], 15 | "input_variables": [], 16 | "messages": [ 17 | { 18 | "content": [{"text": "", "type": "text"}], 19 | "dataset_examples": [], 20 | "input_variables": [], 21 | "name": None, 22 | "raw_request_display_role": "", 23 | "role": "system", 24 | "template_format": "f-string", 25 | }, 26 | { 27 | "content": [{"text": "What is the capital of Japan?", "type": "text"}], 28 | "dataset_examples": [], 29 | "input_variables": [], 30 | "name": None, 31 | "raw_request_display_role": "", 32 | "role": "user", 33 | "template_format": "f-string", 34 | }, 35 | ], 36 | "tool_choice": None, 37 | "tools": None, 38 | "type": "chat", 39 | } 40 | -------------------------------------------------------------------------------- /tests/fixtures/workflow_update_messages.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def workflow_update_data_no_result_code(): 6 | return { 7 | "final_output": { 8 | "Node 1": { 9 | "status": "SUCCESS", 10 | "value": "no_result_code", 11 | "error_message": None, 12 | "raw_error_message": None, 13 | "is_output_node": None, 14 | } 15 | }, 16 | "workflow_version_execution_id": 717, 17 | } 18 | 19 | 20 | @pytest.fixture 21 | def workflow_update_data_ok(): 22 | return { 23 | "final_output": { 24 | "Node 1": { 25 | "status": "SUCCESS", 26 | "value": "ok_result_code", 27 | "error_message": None, 28 | "raw_error_message": None, 29 | "is_output_node": None, 30 | } 31 | }, 32 | "result_code": "OK", 33 | "workflow_version_execution_id": 717, 34 | } 35 | 36 | 37 | @pytest.fixture 38 | def workflow_update_data_exceeds_size_limit(): 39 | return { 40 | "final_output": ( 41 | "Final output (and associated metadata) exceeds the size limit of 1 bytes. " 42 | "Upgrade to the most recent SDK or use GET /workflow-version-execution-results " 43 | "to retrieve the final output." 44 | ), 45 | "result_code": "EXCEEDS_SIZE_LIMIT", 46 | "workflow_version_execution_id": 717, 47 | } 48 | -------------------------------------------------------------------------------- /tests/test_agents/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagnivOrg/prompt-layer-library/2ac9bc5d0a37d90e108d53647a602c84b7f9b58a/tests/test_agents/__init__.py -------------------------------------------------------------------------------- /tests/test_agents/test_arun_workflow_request.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import nullcontext 3 | from unittest.mock import AsyncMock, MagicMock, patch 4 | 5 | import pytest 6 | from ably.realtime.realtime_channel import RealtimeChannel 7 | from pytest_parametrize_cases import Case, parametrize_cases 8 | 9 | from promptlayer.utils import arun_workflow_request 10 | from tests.utils.mocks import Any 11 | from tests.utils.vcr import assert_played, is_cassette_recording 12 | 13 | 14 | @patch("promptlayer.utils.URL_API_PROMPTLAYER", "http://localhost:8000") 15 | @patch("promptlayer.utils.WS_TOKEN_REQUEST_LIBRARY_URL", "http://localhost:8000/ws-token-request-library") 16 | @parametrize_cases( 17 | Case("Regular call", kwargs={"workflow_id_or_name": "analyze_1", "input_variables": {"var1": "value1"}}), 18 | Case("Legacy call", kwargs={"workflow_name": "analyze_1", "input_variables": {"var1": "value1"}}), 19 | ) 20 | @pytest.mark.asyncio 21 | async def test_arun_workflow_request(promptlayer_api_key, kwargs): 22 | is_recording = is_cassette_recording() 23 | results_future = MagicMock() 24 | message_listener = MagicMock() 25 | with ( 26 | assert_played("test_arun_workflow_request.yaml") as cassette, 27 | patch( 28 | "promptlayer.utils._make_channel_name_suffix", return_value="8dd7e4d404754c60a50e78f70f74aade" 29 | ) as _make_channel_name_suffix_mock, 30 | nullcontext() 31 | if is_recording 32 | else patch( 33 | "promptlayer.utils._subscribe_to_workflow_completion_channel", 34 | return_value=(results_future, message_listener), 35 | ) as _subscribe_to_workflow_completion_channel_mock, 36 | nullcontext() 37 | if is_recording 38 | else patch( 39 | "promptlayer.utils._wait_for_workflow_completion", 40 | new_callable=AsyncMock, 41 | return_value={"Node 2": "False", "Node 3": "AAA"}, 42 | ) as _wait_for_workflow_completion_mock, 43 | ): 44 | assert await arun_workflow_request(api_key=promptlayer_api_key, **kwargs) == { 45 | "Node 2": "False", 46 | "Node 3": "AAA", 47 | } 48 | assert [(request.method, request.uri) for request in cassette.requests] == [ 49 | ("GET", "http://localhost:8000/workflows/analyze_1"), 50 | ( 51 | "POST", 52 | ( 53 | "http://localhost:8000/ws-token-request-library?" 54 | "capability=workflows%3A3%3Arun%3A8dd7e4d404754c60a50e78f70f74aade" 55 | ), 56 | ), 57 | ("POST", "http://localhost:8000/workflows/3/run"), 58 | ] 59 | 60 | _make_channel_name_suffix_mock.assert_called_once() 61 | if not is_recording: 62 | _subscribe_to_workflow_completion_channel_mock.assert_awaited_once_with( 63 | Any(type_=RealtimeChannel), Any(type_=asyncio.Future), False, {"X-API-KEY": promptlayer_api_key} 64 | ) 65 | _wait_for_workflow_completion_mock.assert_awaited_once_with( 66 | Any(type_=RealtimeChannel), results_future, message_listener, 3600 67 | ) 68 | -------------------------------------------------------------------------------- /tests/test_agents/test_misc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from unittest.mock import AsyncMock, MagicMock 4 | 5 | import pytest 6 | from ably.types.message import Message 7 | 8 | from promptlayer.utils import _get_final_output, _make_message_listener, _wait_for_workflow_completion 9 | from tests.utils.vcr import assert_played 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_get_final_output(headers): 14 | with assert_played("test_get_final_output_1.yaml"): 15 | assert (await _get_final_output(717, True, headers=headers)) == { 16 | "Node 1": { 17 | "status": "SUCCESS", 18 | "value": "AAA", 19 | "error_message": None, 20 | "raw_error_message": None, 21 | "is_output_node": True, 22 | } 23 | } 24 | 25 | with assert_played("test_get_final_output_2.yaml"): 26 | assert (await _get_final_output(717, False, headers=headers)) == "AAA" 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_make_message_listener( 31 | headers, workflow_update_data_no_result_code, workflow_update_data_ok, workflow_update_data_exceeds_size_limit 32 | ): 33 | results_future = asyncio.Future() 34 | execution_id_future = asyncio.Future() 35 | message_listener = _make_message_listener(results_future, execution_id_future, True, headers) 36 | execution_id_future.set_result(717) 37 | await message_listener(Message(name="INVALID")) 38 | assert not results_future.done() 39 | assert execution_id_future.done() 40 | 41 | # Final output is in the message 42 | for message_data in (workflow_update_data_no_result_code, workflow_update_data_ok): 43 | results_future = asyncio.Future() 44 | execution_id_future = asyncio.Future() 45 | execution_id_future.set_result(717) 46 | message_listener = _make_message_listener(results_future, execution_id_future, True, headers) 47 | await message_listener(Message(name="SET_WORKFLOW_COMPLETE", data=json.dumps(message_data))) 48 | assert results_future.done() 49 | assert (await asyncio.wait_for(results_future, 0.1)) == message_data["final_output"] 50 | 51 | # Final output is not in the message (return all outputs) 52 | with assert_played("test_make_message_listener_1.yaml"): 53 | results_future = asyncio.Future() 54 | execution_id_future = asyncio.Future() 55 | execution_id_future.set_result(717) 56 | message_listener = _make_message_listener(results_future, execution_id_future, True, headers) 57 | await message_listener( 58 | Message(name="SET_WORKFLOW_COMPLETE", data=json.dumps(workflow_update_data_exceeds_size_limit)) 59 | ) 60 | assert results_future.done() 61 | assert (await asyncio.wait_for(results_future, 0.1)) == { 62 | "Node 1": { 63 | "status": "SUCCESS", 64 | "value": "AAA", 65 | "error_message": None, 66 | "raw_error_message": None, 67 | "is_output_node": True, 68 | } 69 | } 70 | 71 | # Final output is not in the message (return final output) 72 | with assert_played("test_make_message_listener_2.yaml"): 73 | results_future = asyncio.Future() 74 | execution_id_future = asyncio.Future() 75 | execution_id_future.set_result(717) 76 | message_listener = _make_message_listener(results_future, execution_id_future, False, headers) 77 | await message_listener( 78 | Message(name="SET_WORKFLOW_COMPLETE", data=json.dumps(workflow_update_data_exceeds_size_limit)) 79 | ) 80 | assert results_future.done() 81 | assert (await asyncio.wait_for(results_future, 0.1)) == "AAA" 82 | 83 | 84 | @pytest.mark.asyncio 85 | async def test_wait_for_workflow_completion(workflow_update_data_ok): 86 | mock_channel = AsyncMock() 87 | mock_channel.unsubscribe = MagicMock() 88 | results_future = asyncio.Future() 89 | results_future.set_result(workflow_update_data_ok["final_output"]) 90 | message_listener = AsyncMock() 91 | actual_result = await _wait_for_workflow_completion(mock_channel, results_future, message_listener, 120) 92 | assert workflow_update_data_ok["final_output"] == actual_result 93 | mock_channel.unsubscribe.assert_called_once_with("SET_WORKFLOW_COMPLETE", message_listener) 94 | -------------------------------------------------------------------------------- /tests/test_anthropic_proxy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from anthropic.types import ( 3 | Message, 4 | MessageDeltaUsage, 5 | RawContentBlockDeltaEvent, 6 | RawContentBlockStartEvent, 7 | RawContentBlockStopEvent, 8 | RawMessageDeltaEvent, 9 | RawMessageStartEvent, 10 | RawMessageStopEvent, 11 | TextBlock, 12 | TextDelta, 13 | ) 14 | from anthropic.types.raw_message_delta_event import Delta 15 | from anthropic.types.usage import Usage 16 | 17 | from tests.utils.vcr import assert_played 18 | 19 | 20 | def test_anthropic_chat_completion(capsys, anthropic_client): 21 | with assert_played("test_anthropic_chat_completion.yaml"): 22 | completion = anthropic_client.messages.create( 23 | max_tokens=1024, 24 | messages=[{"role": "user", "content": "What is the capital of the United States?"}], 25 | model="claude-3-haiku-20240307", 26 | ) 27 | 28 | captured = capsys.readouterr() 29 | assert "WARNING: While" not in captured.err 30 | assert completion.content is not None 31 | 32 | 33 | def test_anthropic_chat_completion_with_pl_id(capsys, anthropic_client): 34 | with assert_played("test_anthropic_chat_completion_with_pl_id.yaml"): 35 | completion, pl_id = anthropic_client.messages.create( 36 | max_tokens=1024, 37 | messages=[{"role": "user", "content": "What is the capital of France?"}], 38 | model="claude-3-haiku-20240307", 39 | return_pl_id=True, 40 | ) 41 | 42 | assert "WARNING: While" not in capsys.readouterr().err 43 | assert completion.content is not None 44 | assert isinstance(pl_id, int) 45 | 46 | 47 | def test_anthropic_chat_completion_with_stream(capsys, anthropic_client): 48 | with assert_played("test_anthropic_chat_completion_with_stream.yaml"): 49 | completions_gen = anthropic_client.messages.create( 50 | max_tokens=1024, 51 | messages=[{"role": "user", "content": "What is the capital of Germany?"}], 52 | model="claude-3-haiku-20240307", 53 | stream=True, 54 | ) 55 | 56 | completions = [completion for completion in completions_gen] 57 | assert completions == [ 58 | RawMessageStartEvent( 59 | message=Message( 60 | id="msg_01PP15qoPAWehXLzXosCnnVP", 61 | content=[], 62 | model="claude-3-haiku-20240307", 63 | role="assistant", 64 | stop_reason=None, 65 | stop_sequence=None, 66 | type="message", 67 | usage=Usage( 68 | cache_creation_input_tokens=0, cache_read_input_tokens=0, input_tokens=14, output_tokens=4 69 | ), 70 | ), 71 | type="message_start", 72 | ), 73 | RawContentBlockStartEvent( 74 | content_block=TextBlock(citations=None, text="", type="text"), index=0, type="content_block_start" 75 | ), 76 | RawContentBlockDeltaEvent( 77 | delta=TextDelta(text="The capital of Germany", type="text_delta"), index=0, type="content_block_delta" 78 | ), 79 | RawContentBlockDeltaEvent( 80 | delta=TextDelta(text=" is Berlin.", type="text_delta"), index=0, type="content_block_delta" 81 | ), 82 | RawContentBlockStopEvent(index=0, type="content_block_stop"), 83 | RawMessageDeltaEvent( 84 | delta=Delta(stop_reason="end_turn", stop_sequence=None), 85 | type="message_delta", 86 | usage=MessageDeltaUsage(output_tokens=10), 87 | ), 88 | RawMessageStopEvent(type="message_stop"), 89 | ] 90 | 91 | assert "WARNING: While" not in capsys.readouterr().err 92 | 93 | 94 | def test_anthropic_chat_completion_with_stream_and_pl_id(anthropic_client): 95 | with assert_played("test_anthropic_chat_completion_with_stream_and_pl_id.yaml"): 96 | completions_gen = anthropic_client.messages.create( 97 | max_tokens=1024, 98 | messages=[{"role": "user", "content": "What is the capital of Italy?"}], 99 | model="claude-3-haiku-20240307", 100 | stream=True, 101 | return_pl_id=True, 102 | ) 103 | completions = [completion for completion, _ in completions_gen] 104 | assert completions == [ 105 | RawMessageStartEvent( 106 | message=Message( 107 | id="msg_016q2kSZ82qtDP2CNUAKSfLV", 108 | content=[], 109 | model="claude-3-haiku-20240307", 110 | role="assistant", 111 | stop_reason=None, 112 | stop_sequence=None, 113 | type="message", 114 | usage=Usage( 115 | cache_creation_input_tokens=0, cache_read_input_tokens=0, input_tokens=14, output_tokens=4 116 | ), 117 | ), 118 | type="message_start", 119 | ), 120 | RawContentBlockStartEvent( 121 | content_block=TextBlock(citations=None, text="", type="text"), index=0, type="content_block_start" 122 | ), 123 | RawContentBlockDeltaEvent( 124 | delta=TextDelta(text="The capital of Italy", type="text_delta"), index=0, type="content_block_delta" 125 | ), 126 | RawContentBlockDeltaEvent( 127 | delta=TextDelta(text=" is Rome.", type="text_delta"), index=0, type="content_block_delta" 128 | ), 129 | RawContentBlockStopEvent(index=0, type="content_block_stop"), 130 | RawMessageDeltaEvent( 131 | delta=Delta(stop_reason="end_turn", stop_sequence=None), 132 | type="message_delta", 133 | usage=MessageDeltaUsage(output_tokens=10), 134 | ), 135 | RawMessageStopEvent(type="message_stop"), 136 | ] 137 | 138 | 139 | @pytest.mark.asyncio 140 | async def test_anthropic_chat_completion_async(capsys, anthropic_async_client): 141 | with assert_played("test_anthropic_chat_completion_async.yaml"): 142 | completion = await anthropic_async_client.messages.create( 143 | max_tokens=1024, 144 | messages=[{"role": "user", "content": "What is the capital of Spain?"}], 145 | model="claude-3-haiku-20240307", 146 | ) 147 | 148 | captured = capsys.readouterr() 149 | assert "WARNING: While" not in captured.err 150 | assert completion.content is not None 151 | 152 | 153 | @pytest.mark.asyncio 154 | async def test_anthropic_chat_completion_async_stream_with_pl_id(anthropic_async_client): 155 | with assert_played("test_anthropic_chat_completion_async_stream_with_pl_id.yaml"): 156 | completions_gen = await anthropic_async_client.messages.create( 157 | max_tokens=1024, 158 | messages=[{"role": "user", "content": "What is the capital of Japan?"}], 159 | model="claude-3-haiku-20240307", 160 | stream=True, 161 | return_pl_id=True, 162 | ) 163 | 164 | completions = [completion async for completion, _ in completions_gen] 165 | assert completions == [ 166 | RawMessageStartEvent( 167 | message=Message( 168 | id="msg_01Bi6S5crUgtL7PUCuYc8Vy6", 169 | content=[], 170 | model="claude-3-haiku-20240307", 171 | role="assistant", 172 | stop_reason=None, 173 | stop_sequence=None, 174 | type="message", 175 | usage=Usage( 176 | cache_creation_input_tokens=0, cache_read_input_tokens=0, input_tokens=14, output_tokens=4 177 | ), 178 | ), 179 | type="message_start", 180 | ), 181 | RawContentBlockStartEvent( 182 | content_block=TextBlock(citations=None, text="", type="text"), index=0, type="content_block_start" 183 | ), 184 | RawContentBlockDeltaEvent( 185 | delta=TextDelta(text="The capital of Japan", type="text_delta"), index=0, type="content_block_delta" 186 | ), 187 | RawContentBlockDeltaEvent( 188 | delta=TextDelta(text=" is Tokyo.", type="text_delta"), index=0, type="content_block_delta" 189 | ), 190 | RawContentBlockStopEvent(index=0, type="content_block_stop"), 191 | RawMessageDeltaEvent( 192 | delta=Delta(stop_reason="end_turn", stop_sequence=None), 193 | type="message_delta", 194 | usage=MessageDeltaUsage(output_tokens=10), 195 | ), 196 | RawMessageStopEvent(type="message_stop"), 197 | ] 198 | -------------------------------------------------------------------------------- /tests/test_get_prompt_template.py: -------------------------------------------------------------------------------- 1 | from tests.utils.vcr import assert_played 2 | 3 | 4 | def test_get_prompt_template_provider_base_url_name(capsys, promptlayer_client): 5 | # TODO(dmu) HIGH: Improve assertions for this test 6 | provider_base_url_name = "does_not_exist" 7 | prompt_template = { 8 | "type": "chat", 9 | "provider_base_url_name": provider_base_url_name, 10 | "messages": [ 11 | { 12 | "content": [{"text": "You are an AI.", "type": "text"}], 13 | "input_variables": [], 14 | "name": None, 15 | "raw_request_display_role": "", 16 | "role": "system", 17 | "template_format": "f-string", 18 | }, 19 | { 20 | "content": [{"text": "What is the capital of Japan?", "type": "text"}], 21 | "input_variables": [], 22 | "name": None, 23 | "raw_request_display_role": "", 24 | "role": "user", 25 | "template_format": "f-string", 26 | }, 27 | ], 28 | } 29 | 30 | prompt_registry_name = "test_template:test" 31 | with assert_played("test_get_prompt_template_provider_base_url_name.yaml"): 32 | promptlayer_client.templates.publish( 33 | { 34 | "provider_base_url_name": provider_base_url_name, 35 | "prompt_name": prompt_registry_name, 36 | "prompt_template": prompt_template, 37 | } 38 | ) 39 | response = promptlayer_client.templates.get( 40 | prompt_registry_name, {"provider": "openai", "model": "gpt-3.5-turbo"} 41 | ) 42 | assert response["provider_base_url"] is None 43 | -------------------------------------------------------------------------------- /tests/test_openai_proxy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.utils.vcr import assert_played 4 | 5 | 6 | def test_openai_chat_completion(capsys, openai_client): 7 | with assert_played("test_openai_chat_completion.yaml"): 8 | completion = openai_client.chat.completions.create( 9 | model="gpt-3.5-turbo", 10 | messages=[ 11 | {"role": "system", "content": "You are a helpful assistant."}, 12 | {"role": "user", "content": "What is the capital of the United States?"}, 13 | ], 14 | ) 15 | 16 | captured = capsys.readouterr() 17 | assert "WARNING: While" not in captured.err 18 | assert completion.choices[0].message.content is not None 19 | 20 | 21 | def test_openai_chat_completion_with_pl_id(capsys, openai_client): 22 | with assert_played("test_openai_chat_completion_with_pl_id.yaml"): 23 | completion, pl_id = openai_client.chat.completions.create( 24 | model="gpt-3.5-turbo", 25 | messages=[ 26 | {"role": "system", "content": "You are a helpful assistant."}, 27 | {"role": "user", "content": "What is the capital of France?"}, 28 | ], 29 | return_pl_id=True, 30 | ) 31 | 32 | captured = capsys.readouterr() 33 | assert "WARNING: While" not in captured.err 34 | assert completion.choices[0].message.content is not None 35 | assert isinstance(pl_id, int) 36 | 37 | 38 | def test_openai_chat_completion_with_stream(capsys, openai_client): 39 | with assert_played("test_openai_chat_completion_with_stream.yaml"): 40 | completion = openai_client.chat.completions.create( 41 | model="gpt-3.5-turbo", 42 | messages=[ 43 | {"role": "system", "content": "You are a helpful assistant."}, 44 | {"role": "user", "content": "What is the capital of Germany?"}, 45 | ], 46 | stream=True, 47 | ) 48 | 49 | captured = capsys.readouterr() 50 | for chunk in completion: 51 | assert chunk.choices[0].delta != {} 52 | assert "WARNING: While" not in captured.err 53 | 54 | 55 | def test_openai_chat_completion_with_stream_and_pl_id(capsys, openai_client): 56 | with assert_played("test_openai_chat_completion_with_stream_and_pl_id.yaml"): 57 | completion = openai_client.chat.completions.create( 58 | model="gpt-3.5-turbo", 59 | messages=[ 60 | {"role": "system", "content": "You are a helpful assistant."}, 61 | {"role": "user", "content": "What is the capital of Italy?"}, 62 | ], 63 | stream=True, 64 | return_pl_id=True, 65 | ) 66 | 67 | pl_id = None 68 | for _, pl_id in completion: 69 | assert pl_id is None or isinstance(pl_id, int) 70 | assert isinstance(pl_id, int) 71 | 72 | assert "WARNING: While" not in capsys.readouterr().err 73 | 74 | 75 | @pytest.mark.asyncio 76 | async def test_openai_chat_completion_async(capsys, openai_async_client): 77 | with assert_played("test_openai_chat_completion_async.yaml"): 78 | completion = await openai_async_client.chat.completions.create( 79 | model="gpt-3.5-turbo", 80 | messages=[ 81 | {"role": "system", "content": "You are a helpful assistant."}, 82 | {"role": "user", "content": "What is the capital of Spain?"}, 83 | ], 84 | ) 85 | 86 | captured = capsys.readouterr() 87 | assert "WARNING: While" not in captured.err 88 | assert completion.choices[0].message.content is not None 89 | 90 | 91 | @pytest.mark.asyncio 92 | async def test_openai_chat_completion_async_stream_with_pl_id(capsys, openai_async_client): 93 | with assert_played("test_openai_chat_completion_async_stream_with_pl_id.yaml"): 94 | completion = await openai_async_client.chat.completions.create( 95 | model="gpt-3.5-turbo", 96 | messages=[ 97 | {"role": "system", "content": "You are a helpful assistant."}, 98 | {"role": "user", "content": "What is the capital of Japan?"}, 99 | ], 100 | stream=True, 101 | return_pl_id=True, 102 | ) 103 | 104 | assert "WARNING: While" not in capsys.readouterr().err 105 | pl_id = None 106 | async for _, pl_id in completion: 107 | assert pl_id is None or isinstance(pl_id, int) 108 | assert isinstance(pl_id, int) 109 | -------------------------------------------------------------------------------- /tests/test_templates_groups_track.py: -------------------------------------------------------------------------------- 1 | from tests.utils.vcr import assert_played 2 | 3 | 4 | def test_track_and_templates(sample_template_name, promptlayer_client, openai_client): 5 | # TODO(dmu) HIGH: Improve asserts in this test 6 | with assert_played("test_track_and_templates.yaml"): 7 | response = promptlayer_client.templates.get( 8 | sample_template_name, {"provider": "openai", "model": "gpt-3.5-turbo"} 9 | ) 10 | assert response == { 11 | "id": 4, 12 | "prompt_name": "sample_template", 13 | "tags": ["test"], 14 | "workspace_id": 1, 15 | "commit_message": "test", 16 | "metadata": { 17 | "model": { 18 | "provider": "openai", 19 | "name": "gpt-4o-mini", 20 | "parameters": { 21 | "frequency_penalty": 0, 22 | "max_tokens": 256, 23 | "messages": [{"content": "Hello", "role": "system"}], 24 | "model": "gpt-4o", 25 | "presence_penalty": 0, 26 | "seed": 0, 27 | "temperature": 1, 28 | "top_p": 1, 29 | }, 30 | } 31 | }, 32 | "prompt_template": { 33 | "messages": [ 34 | { 35 | "input_variables": [], 36 | "template_format": "f-string", 37 | "content": [{"type": "text", "text": ""}], 38 | "raw_request_display_role": "", 39 | "dataset_examples": [], 40 | "role": "system", 41 | "name": None, 42 | }, 43 | { 44 | "input_variables": [], 45 | "template_format": "f-string", 46 | "content": [{"type": "text", "text": "What is the capital of Japan?"}], 47 | "raw_request_display_role": "", 48 | "dataset_examples": [], 49 | "role": "user", 50 | "name": None, 51 | }, 52 | ], 53 | "functions": [], 54 | "tools": None, 55 | "function_call": "none", 56 | "tool_choice": None, 57 | "type": "chat", 58 | "input_variables": [], 59 | "dataset_examples": [], 60 | }, 61 | "llm_kwargs": { 62 | "messages": [{"content": "Hello", "role": "system"}], 63 | "model": "gpt-4o", 64 | "frequency_penalty": 0, 65 | "max_tokens": 256, 66 | "presence_penalty": 0, 67 | "seed": 0, 68 | "temperature": 1, 69 | "top_p": 1, 70 | }, 71 | "provider_base_url": None, 72 | "version": 1, 73 | "snippets": [], 74 | "warning": None, 75 | } 76 | 77 | llm_kwargs = response["llm_kwargs"].copy() 78 | llm_kwargs.pop("model", None) 79 | _, pl_id = openai_client.chat.completions.create(return_pl_id=True, model="gpt-3.5-turbo", **llm_kwargs) 80 | assert promptlayer_client.track.score(request_id=pl_id, score_name="accuracy", score=10) is not None 81 | assert promptlayer_client.track.metadata(request_id=pl_id, metadata={"test": "test"}) 82 | 83 | group_id = promptlayer_client.group.create() 84 | assert isinstance(group_id, int) 85 | assert promptlayer_client.track.group(request_id=pl_id, group_id=group_id) 86 | 87 | 88 | def test_get_all_templates(promptlayer_client): 89 | with assert_played("test_get_all_templates.yaml"): 90 | all_templates = promptlayer_client.templates.all() 91 | assert isinstance(all_templates, list) 92 | assert len(all_templates) > 0 93 | -------------------------------------------------------------------------------- /tests/utils/mocks.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime 3 | from uuid import UUID 4 | 5 | 6 | class Any: 7 | def __init__(self, *, type_=None, regex=None): 8 | self.type = type_ 9 | self.regex = regex if isinstance(regex, (re.Pattern, type(None))) else re.compile(regex) 10 | 11 | def __eq__(self, other): 12 | result = True 13 | if (type_ := self.type) is not None: 14 | result &= isinstance(other, type_) 15 | if regex := self.regex: 16 | result &= bool(regex.match(str(other))) 17 | 18 | return result 19 | 20 | def __ne__(self, other): 21 | return self != other 22 | 23 | def __repr__(self): 24 | return ( 25 | f"Any(type_={'None' if self.type is None else self.type.__name__}, " 26 | f"regex={'None' if self.regex is None else self.regex.pattern})" 27 | ) 28 | 29 | def __str__(self): 30 | return repr(self) 31 | 32 | 33 | ANY_INT = Any(type_=int) 34 | ANY_BOOL = Any(type_=bool) 35 | ANY_STR = Any(type_=str) 36 | ANY_DATETIME = Any(type_=datetime) 37 | ANY_UUID = Any(type_=UUID) 38 | -------------------------------------------------------------------------------- /tests/utils/vcr.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from contextlib import contextmanager 4 | from pathlib import Path 5 | 6 | import vcr 7 | from vcr.record_mode import RecordMode 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | CASSETTES_PATH = Path(__file__).resolve().parent.parent / "fixtures/cassettes" 12 | VCR_DEFAULT_KWARGS = { 13 | "allow_playback_repeats": False, 14 | "decode_compressed_response": True, 15 | "filter_headers": [ 16 | ("Authorization", "sanitized"), 17 | ("x-api-key", "sanitized"), 18 | ], 19 | "filter_post_data_parameters": [("api_key", "sanitized")], 20 | } 21 | 22 | 23 | def is_cassette_recording(): 24 | return os.getenv("PROMPTLAYER_IS_CASSETTE_RECORDING", "false").lower() == "true" 25 | 26 | 27 | @contextmanager 28 | def assert_played(cassette_name, should_assert_played=True, play_count=None, **kwargs): 29 | combined_kwargs = VCR_DEFAULT_KWARGS | kwargs 30 | is_cassette_recording_ = is_cassette_recording() 31 | combined_kwargs.setdefault( 32 | "record_mode", RecordMode.ONCE.value if is_cassette_recording_ else RecordMode.NONE.value 33 | ) 34 | 35 | vcr_instance = vcr.VCR() 36 | with vcr_instance.use_cassette(str(CASSETTES_PATH / cassette_name), **combined_kwargs) as cassette: 37 | yield cassette 38 | if should_assert_played and not is_cassette_recording_: 39 | if play_count is None: 40 | assert cassette.all_played, "Not all requests have played" 41 | else: 42 | actual_play_count = cassette.play_count 43 | if cassette.play_count != play_count: 44 | play_counts = cassette.play_counts 45 | for index, request in enumerate(cassette.requests): 46 | logger.debug("%s played %s time(s)", request, play_counts[index]) 47 | raise AssertionError(f"Expected {play_count}, actually played {actual_play_count}") 48 | --------------------------------------------------------------------------------