├── tests ├── test_agents │ ├── __init__.py │ ├── test_arun_workflow_request.py │ └── test_misc.py ├── fixtures │ ├── setup.py │ ├── auth.py │ ├── __init__.py │ ├── cassettes │ │ ├── test_get_final_output_2.yaml │ │ ├── test_make_message_listener_2.yaml │ │ ├── test_get_final_output_1.yaml │ │ ├── test_make_message_listener_1.yaml │ │ ├── test_get_template_async.yaml │ │ ├── test_arun_workflow_request.yaml │ │ ├── test_publish_template_async.yaml │ │ ├── test_anthropic_chat_completion_with_pl_id.yaml │ │ ├── test_anthropic_chat_completion_async.yaml │ │ ├── test_anthropic_chat_completion.yaml │ │ ├── test_get_prompt_template_provider_base_url_name.yaml │ │ ├── test_get_all_templates.yaml │ │ ├── test_anthropic_chat_completion_with_stream.yaml │ │ ├── test_anthropic_chat_completion_with_stream_and_pl_id.yaml │ │ ├── test_anthropic_chat_completion_async_stream_with_pl_id.yaml │ │ ├── test_openai_chat_completion_with_stream.yaml │ │ ├── test_openai_chat_completion_with_pl_id.yaml │ │ ├── test_openai_chat_completion_async.yaml │ │ ├── test_openai_chat_completion.yaml │ │ ├── test_log_request_async.yaml │ │ ├── test_openai_chat_completion_with_stream_and_pl_id.yaml │ │ ├── test_openai_chat_completion_async_stream_with_pl_id.yaml │ │ ├── test_run_prompt_async.yaml │ │ └── test_track_and_templates.yaml │ ├── clients.py │ ├── templates.py │ └── workflow_update_messages.py ├── utils │ ├── mocks.py │ └── vcr.py ├── test_get_prompt_template.py ├── test_templates_groups_track.py ├── test_openai_proxy.py └── test_anthropic_proxy.py ├── __init__.py ├── promptlayer ├── types │ ├── __init__.py │ ├── request_log.py │ └── prompt_template.py ├── groups │ ├── groups.py │ └── __init__.py ├── __init__.py ├── templates.py ├── streaming │ ├── __init__.py │ └── stream_processor.py ├── track │ ├── __init__.py │ └── track.py ├── span_exporter.py ├── exceptions.py └── promptlayer_base.py ├── .editorconfig ├── Makefile ├── conftest.py ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── pre-commit.yaml │ ├── release.yml │ └── integration-tests.yml ├── .devcontainer └── devcontainer.json ├── pyproject.toml ├── .gitignore ├── README.md └── LICENSE /tests/test_agents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # TODO(dmu) LOW: This file seems unnecessary. Consider removal 2 | -------------------------------------------------------------------------------- /promptlayer/types/__init__.py: -------------------------------------------------------------------------------- 1 | from . import prompt_template 2 | from .request_log import RequestLog 3 | 4 | __all__ = ["prompt_template", "RequestLog"] 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/fixtures/setup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.utils.vcr import is_cassette_recording 4 | 5 | if is_cassette_recording(): 6 | 7 | @pytest.fixture 8 | def autouse_disable_network(): 9 | return 10 | else: 11 | 12 | @pytest.fixture(autouse=True) 13 | def autouse_disable_network(disable_network): 14 | yield 15 | -------------------------------------------------------------------------------- /promptlayer/groups/groups.py: -------------------------------------------------------------------------------- 1 | from promptlayer.utils import apromptlayer_create_group, promptlayer_create_group 2 | 3 | 4 | def create(api_key: str, base_url: str, throw_on_error: bool): 5 | return promptlayer_create_group(api_key, base_url, throw_on_error) 6 | 7 | 8 | async def acreate(api_key: str, base_url: str, throw_on_error: bool): 9 | return await apromptlayer_create_group(api_key, base_url, throw_on_error) 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 base_url(): 23 | return "http://localhost:8000" 24 | 25 | 26 | @pytest.fixture 27 | def throw_on_error(): 28 | return True 29 | 30 | 31 | @pytest.fixture 32 | def headers(promptlayer_api_key): 33 | return {"X-API-KEY": promptlayer_api_key} 34 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import ( # noqa: F401 2 | anthropic_api_key, 3 | base_url, 4 | headers, 5 | openai_api_key, 6 | promptlayer_api_key, 7 | throw_on_error, 8 | ) 9 | from .clients import ( # noqa: F401 10 | anthropic_async_client, 11 | anthropic_client, 12 | openai_async_client, 13 | openai_client, 14 | promptlayer_async_client, 15 | promptlayer_client, 16 | ) 17 | from .setup import autouse_disable_network # noqa: F401 18 | from .templates import sample_template_content, sample_template_name # noqa: F401 19 | from .workflow_update_messages import ( 20 | workflow_update_data_exceeds_size_limit, # noqa: F401 21 | workflow_update_data_no_result_code, # noqa: F401 22 | workflow_update_data_ok, # noqa: F401 23 | ) 24 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | pull_request: 5 | branches: ["master"] 6 | 7 | env: 8 | POETRY_VIRTUALENVS_CREATE: "false" 9 | PYTHON_VERSION: "3.9" 10 | POETRY_VERSION: "2.2.1" 11 | PIPX_DEFAULT_PYTHON: "python3.9" 12 | 13 | jobs: 14 | integration-tests: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v5 19 | - name: Set up Python 20 | uses: actions/setup-python@v6 21 | with: 22 | python-version: ${{ env.PYTHON_VERSION }} 23 | 24 | - name: Install Poetry 25 | run: pipx install poetry==${{ env.POETRY_VERSION }} 26 | 27 | - name: Install dependencies 28 | run: poetry install 29 | 30 | - name: Run integration tests 31 | run: poetry run pytest 32 | -------------------------------------------------------------------------------- /promptlayer/groups/__init__.py: -------------------------------------------------------------------------------- 1 | from promptlayer.groups.groups import acreate, create 2 | 3 | 4 | class GroupManager: 5 | def __init__(self, api_key: str, base_url: str, throw_on_error: bool): 6 | self.api_key = api_key 7 | self.base_url = base_url 8 | self.throw_on_error = throw_on_error 9 | 10 | def create(self): 11 | return create(self.api_key, self.base_url, self.throw_on_error) 12 | 13 | 14 | class AsyncGroupManager: 15 | def __init__(self, api_key: str, base_url: str, throw_on_error: bool): 16 | self.api_key = api_key 17 | self.base_url = base_url 18 | self.throw_on_error = throw_on_error 19 | 20 | async def create(self): 21 | return await acreate(self.api_key, self.base_url, self.throw_on_error) 22 | 23 | 24 | __all__ = ["GroupManager", "AsyncGroupManager"] 25 | -------------------------------------------------------------------------------- /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_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_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_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/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/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, base_url: str): 8 | return PromptLayer(api_key=promptlayer_api_key, base_url=base_url) 9 | 10 | 11 | @pytest.fixture 12 | def promptlayer_async_client(promptlayer_api_key, base_url: str): 13 | return AsyncPromptLayer(api_key=promptlayer_api_key, base_url=base_url) 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/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 | -------------------------------------------------------------------------------- /promptlayer/__init__.py: -------------------------------------------------------------------------------- 1 | from .exceptions import ( 2 | PromptLayerAPIConnectionError, 3 | PromptLayerAPIError, 4 | PromptLayerAPIStatusError, 5 | PromptLayerAPITimeoutError, 6 | PromptLayerAuthenticationError, 7 | PromptLayerBadRequestError, 8 | PromptLayerConflictError, 9 | PromptLayerError, 10 | PromptLayerInternalServerError, 11 | PromptLayerNotFoundError, 12 | PromptLayerPermissionDeniedError, 13 | PromptLayerRateLimitError, 14 | PromptLayerUnprocessableEntityError, 15 | PromptLayerValidationError, 16 | ) 17 | from .promptlayer import AsyncPromptLayer, PromptLayer 18 | 19 | __version__ = "1.0.78" 20 | __all__ = [ 21 | "PromptLayer", 22 | "AsyncPromptLayer", 23 | "__version__", 24 | # Exceptions 25 | "PromptLayerError", 26 | "PromptLayerAPIError", 27 | "PromptLayerBadRequestError", 28 | "PromptLayerAuthenticationError", 29 | "PromptLayerPermissionDeniedError", 30 | "PromptLayerNotFoundError", 31 | "PromptLayerConflictError", 32 | "PromptLayerUnprocessableEntityError", 33 | "PromptLayerRateLimitError", 34 | "PromptLayerInternalServerError", 35 | "PromptLayerAPIStatusError", 36 | "PromptLayerAPIConnectionError", 37 | "PromptLayerAPITimeoutError", 38 | "PromptLayerValidationError", 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 | -------------------------------------------------------------------------------- /.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-extra/features/poetry:2": { 9 | "version": "2.2.1" 10 | } 11 | }, 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "ms-python.python", 16 | "charliermarsh.ruff" 17 | ], 18 | "settings": { 19 | "editor.formatOnSave": true, 20 | "python.analysis.autoImportCompletions": true, 21 | "[python]": { 22 | "editor.defaultFormatter": "charliermarsh.ruff" 23 | }, 24 | // TODO(dmu) HIGH: Make linter configuration consistent with .pre-commit-config.yaml 25 | "ruff.organizeImports": true 26 | } 27 | } 28 | }, 29 | // Features to add to the dev container. More info: https://containers.dev/features. 30 | // "features": {}, 31 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 32 | // "forwardPorts": [], 33 | // Use 'postCreateCommand' to run commands after the container is created. 34 | "postCreateCommand": "poetry install" 35 | // Configure tool-specific properties. 36 | // "customizations": {}, 37 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 38 | // "remoteUser": "root", 39 | } 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "promptlayer" 3 | version = "1.0.78" 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 | centrifuge-python = "^0.4.1" 19 | tenacity = "^9.1.2" 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | pytest = "^8.2.0" 23 | pytest-asyncio = "^0.23.6" 24 | openai = "^1.60.1" 25 | google-genai = "^1.5.0" 26 | anthropic = {extras = ["vertex"], version = "^0.57.1"} 27 | # TODO(dmu) MEDIUM: Upgrade to vcrpy >= 7 once it supports urllib3 >= 2.2.2 28 | vcrpy = "<7.0.0" 29 | pytest-network = "^0.0.1" 30 | pytest-parametrize-cases = "^0.1.2" 31 | pydantic = "^2.11.7" 32 | pydantic-settings = "^2.10.1" 33 | boto3 = "^1.35.0" 34 | aioboto3 = "^13.0.0" 35 | 36 | [build-system] 37 | requires = ["poetry-core"] 38 | build-backend = "poetry.core.masonry.api" 39 | 40 | [tool.ruff] 41 | line-length = 120 42 | indent-width = 4 # mimic Black 43 | target-version = "py38" 44 | 45 | [tool.ruff.lint] 46 | ignore = ["E501", "E711", "E712"] 47 | 48 | [tool.ruff.lint.isort] 49 | combine-as-imports = true 50 | relative-imports-order = "closest-to-furthest" 51 | known-first-party = ["promptlayer", "tests"] 52 | 53 | [tool.ruff.format] 54 | quote-style = "double" # mimic Black 55 | indent-style = "space" # also mimic Black 56 | skip-magic-trailing-comma = false # also mimic Black 57 | line-ending = "auto" # mimic Black 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, base_url: str, throw_on_error: bool): 15 | self.api_key = api_key 16 | self.base_url = base_url 17 | self.throw_on_error = throw_on_error 18 | 19 | def get(self, prompt_name: str, params: Union[GetPromptTemplate, None] = None): 20 | return get_prompt_template(self.api_key, self.base_url, self.throw_on_error, prompt_name, params) 21 | 22 | def publish(self, body: PublishPromptTemplate): 23 | return publish_prompt_template(self.api_key, self.base_url, self.throw_on_error, body) 24 | 25 | def all(self, page: int = 1, per_page: int = 30, label: str = None): 26 | return get_all_prompt_templates(self.api_key, self.base_url, self.throw_on_error, page, per_page, label) 27 | 28 | 29 | class AsyncTemplateManager: 30 | def __init__(self, api_key: str, base_url: str, throw_on_error: bool): 31 | self.api_key = api_key 32 | self.base_url = base_url 33 | self.throw_on_error = throw_on_error 34 | 35 | async def get(self, prompt_name: str, params: Union[GetPromptTemplate, None] = None): 36 | return await aget_prompt_template(self.api_key, self.base_url, self.throw_on_error, prompt_name, params) 37 | 38 | async def all(self, page: int = 1, per_page: int = 30, label: str = None): 39 | return await aget_all_prompt_templates(self.api_key, self.base_url, self.throw_on_error, page, per_page, label) 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /promptlayer/streaming/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streaming prompt blueprint support for PromptLayer 3 | 4 | This module provides comprehensive streaming support for building prompt blueprints 5 | from various LLM providers during streaming responses. 6 | """ 7 | 8 | from .blueprint_builder import ( 9 | build_prompt_blueprint_from_anthropic_event, 10 | build_prompt_blueprint_from_google_event, 11 | build_prompt_blueprint_from_openai_chunk, 12 | build_prompt_blueprint_from_openai_responses_event, 13 | ) 14 | from .response_handlers import ( 15 | aanthropic_stream_completion, 16 | aanthropic_stream_message, 17 | abedrock_stream_message, 18 | agoogle_stream_chat, 19 | agoogle_stream_completion, 20 | amistral_stream_chat, 21 | anthropic_stream_completion, 22 | anthropic_stream_message, 23 | aopenai_responses_stream_chat, 24 | aopenai_stream_chat, 25 | aopenai_stream_completion, 26 | bedrock_stream_message, 27 | google_stream_chat, 28 | google_stream_completion, 29 | mistral_stream_chat, 30 | openai_responses_stream_chat, 31 | openai_stream_chat, 32 | openai_stream_completion, 33 | ) 34 | from .stream_processor import ( 35 | astream_response, 36 | stream_response, 37 | ) 38 | 39 | __all__ = [ 40 | "build_prompt_blueprint_from_anthropic_event", 41 | "build_prompt_blueprint_from_google_event", 42 | "build_prompt_blueprint_from_openai_chunk", 43 | "build_prompt_blueprint_from_openai_responses_event", 44 | "stream_response", 45 | "astream_response", 46 | "openai_stream_chat", 47 | "aopenai_stream_chat", 48 | "openai_responses_stream_chat", 49 | "aopenai_responses_stream_chat", 50 | "anthropic_stream_message", 51 | "aanthropic_stream_message", 52 | "openai_stream_completion", 53 | "aopenai_stream_completion", 54 | "anthropic_stream_completion", 55 | "aanthropic_stream_completion", 56 | "bedrock_stream_message", 57 | "abedrock_stream_message", 58 | "google_stream_chat", 59 | "google_stream_completion", 60 | "agoogle_stream_chat", 61 | "agoogle_stream_completion", 62 | "mistral_stream_chat", 63 | "amistral_stream_chat", 64 | ] 65 | -------------------------------------------------------------------------------- /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, base_url: str, throw_on_error: bool): 17 | self.api_key = api_key 18 | self.base_url = base_url 19 | self.throw_on_error = throw_on_error 20 | 21 | def group(self, request_id, group_id): 22 | return group(self.api_key, self.base_url, self.throw_on_error, request_id, group_id) 23 | 24 | def metadata(self, request_id, metadata): 25 | return metadata_(self.api_key, self.base_url, self.throw_on_error, request_id, metadata) 26 | 27 | def prompt(self, request_id, prompt_name, prompt_input_variables, version=None, label=None): 28 | return prompt( 29 | self.api_key, 30 | self.base_url, 31 | self.throw_on_error, 32 | request_id, 33 | prompt_name, 34 | prompt_input_variables, 35 | version, 36 | label, 37 | ) 38 | 39 | def score(self, request_id, score, score_name=None): 40 | return score_(self.api_key, self.base_url, self.throw_on_error, request_id, score, score_name) 41 | 42 | 43 | class AsyncTrackManager: 44 | def __init__(self, api_key: str, base_url: str, throw_on_error: bool): 45 | self.api_key = api_key 46 | self.base_url = base_url 47 | self.throw_on_error = throw_on_error 48 | 49 | async def group(self, request_id, group_id): 50 | return await agroup(self.api_key, self.base_url, self.throw_on_error, request_id, group_id) 51 | 52 | async def metadata(self, request_id, metadata): 53 | return await ametadata(self.api_key, self.base_url, self.throw_on_error, request_id, metadata) 54 | 55 | async def prompt(self, request_id, prompt_name, prompt_input_variables, version=None, label=None): 56 | return await aprompt( 57 | self.api_key, 58 | self.base_url, 59 | self.throw_on_error, 60 | request_id, 61 | prompt_name, 62 | prompt_input_variables, 63 | version, 64 | label, 65 | ) 66 | 67 | async def score(self, request_id, score, score_name=None): 68 | return await ascore(self.api_key, self.base_url, self.throw_on_error, request_id, score, score_name) 69 | 70 | 71 | __all__ = ["TrackManager"] 72 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 raise_on_bad_response, retry_on_api_error 8 | 9 | 10 | class PromptLayerSpanExporter(SpanExporter): 11 | def __init__(self, api_key: str, base_url: str, throw_on_error: bool): 12 | self.api_key = api_key 13 | self.url = f"{base_url}/spans-bulk" 14 | self.throw_on_error = throw_on_error 15 | 16 | @retry_on_api_error 17 | def _post_spans(self, request_data): 18 | response = requests.post( 19 | self.url, 20 | headers={"X-Api-Key": self.api_key, "Content-Type": "application/json"}, 21 | json={"spans": request_data}, 22 | ) 23 | if response.status_code not in (200, 201): 24 | raise_on_bad_response(response, "PromptLayer had the following error while exporting spans") 25 | return response 26 | 27 | def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: 28 | request_data = [] 29 | 30 | for span in spans: 31 | span_info = { 32 | "name": span.name, 33 | "context": { 34 | "trace_id": hex(span.context.trace_id)[2:].zfill(32), # Ensure 32 characters 35 | "span_id": hex(span.context.span_id)[2:].zfill(16), # Ensure 16 characters 36 | "trace_state": str(span.context.trace_state), 37 | }, 38 | "kind": str(span.kind), 39 | "parent_id": hex(span.parent.span_id)[2:] if span.parent else None, 40 | "start_time": span.start_time, 41 | "end_time": span.end_time, 42 | "status": { 43 | "status_code": str(span.status.status_code), 44 | "description": span.status.description, 45 | }, 46 | "attributes": dict(span.attributes), 47 | "events": [ 48 | { 49 | "name": event.name, 50 | "timestamp": event.timestamp, 51 | "attributes": dict(event.attributes), 52 | } 53 | for event in span.events 54 | ], 55 | "links": [{"context": link.context, "attributes": dict(link.attributes)} for link in span.links], 56 | "resource": { 57 | "attributes": dict(span.resource.attributes), 58 | "schema_url": span.resource.schema_url, 59 | }, 60 | } 61 | request_data.append(span_info) 62 | 63 | try: 64 | self._post_spans(request_data) 65 | return SpanExportResult.SUCCESS 66 | except requests.RequestException: 67 | return SpanExportResult.FAILURE 68 | 69 | def shutdown(self): 70 | pass 71 | -------------------------------------------------------------------------------- /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.WS_TOKEN_REQUEST_LIBRARY_URL", "http://localhost:8000/ws-token-request-library") 15 | @parametrize_cases( 16 | Case("Regular call", kwargs={"workflow_id_or_name": "analyze_1", "input_variables": {"var1": "value1"}}), 17 | Case("Legacy call", kwargs={"workflow_name": "analyze_1", "input_variables": {"var1": "value1"}}), 18 | ) 19 | @pytest.mark.asyncio 20 | async def test_arun_workflow_request(base_url: str, throw_on_error: bool, promptlayer_api_key, kwargs): 21 | is_recording = is_cassette_recording() 22 | results_future = MagicMock() 23 | message_listener = MagicMock() 24 | with ( 25 | assert_played("test_arun_workflow_request.yaml") as cassette, 26 | patch( 27 | "promptlayer.utils._make_channel_name_suffix", return_value="8dd7e4d404754c60a50e78f70f74aade" 28 | ) as _make_channel_name_suffix_mock, 29 | nullcontext() 30 | if is_recording 31 | else patch( 32 | "promptlayer.utils._subscribe_to_workflow_completion_channel", 33 | return_value=(results_future, message_listener), 34 | ) as _subscribe_to_workflow_completion_channel_mock, 35 | nullcontext() 36 | if is_recording 37 | else patch( 38 | "promptlayer.utils._wait_for_workflow_completion", 39 | new_callable=AsyncMock, 40 | return_value={"Node 2": "False", "Node 3": "AAA"}, 41 | ) as _wait_for_workflow_completion_mock, 42 | ): 43 | assert await arun_workflow_request( 44 | api_key=promptlayer_api_key, base_url=base_url, throw_on_error=throw_on_error, **kwargs 45 | ) == { 46 | "Node 2": "False", 47 | "Node 3": "AAA", 48 | } 49 | assert [(request.method, request.uri) for request in cassette.requests] == [ 50 | ("GET", "http://localhost:8000/workflows/analyze_1"), 51 | ( 52 | "POST", 53 | ( 54 | "http://localhost:8000/ws-token-request-library?" 55 | "capability=workflows%3A3%3Arun%3A8dd7e4d404754c60a50e78f70f74aade" 56 | ), 57 | ), 58 | ("POST", "http://localhost:8000/workflows/3/run"), 59 | ] 60 | 61 | _make_channel_name_suffix_mock.assert_called_once() 62 | if not is_recording: 63 | _subscribe_to_workflow_completion_channel_mock.assert_awaited_once_with( 64 | base_url, Any(type_=RealtimeChannel), Any(type_=asyncio.Future), False, {"X-API-KEY": promptlayer_api_key} 65 | ) 66 | _wait_for_workflow_completion_mock.assert_awaited_once_with( 67 | Any(type_=RealtimeChannel), results_future, message_listener, 3600 68 | ) 69 | -------------------------------------------------------------------------------- /promptlayer/exceptions.py: -------------------------------------------------------------------------------- 1 | class PromptLayerError(Exception): 2 | """Base exception for all PromptLayer SDK errors.""" 3 | 4 | def __init__(self, message: str, response=None, body=None): 5 | super().__init__(message) 6 | self.message = message 7 | self.response = response 8 | self.body = body 9 | 10 | def __str__(self): 11 | return self.message 12 | 13 | 14 | class PromptLayerAPIError(PromptLayerError): 15 | """Base exception for API-related errors.""" 16 | 17 | pass 18 | 19 | 20 | class PromptLayerBadRequestError(PromptLayerAPIError): 21 | """Exception raised for 400 Bad Request errors. 22 | 23 | Indicates that the request was malformed or contained invalid parameters. 24 | """ 25 | 26 | pass 27 | 28 | 29 | class PromptLayerAuthenticationError(PromptLayerAPIError): 30 | """Exception raised for 401 Unauthorized errors. 31 | 32 | Indicates that the API key is missing, invalid, or expired. 33 | """ 34 | 35 | pass 36 | 37 | 38 | class PromptLayerPermissionDeniedError(PromptLayerAPIError): 39 | """Exception raised for 403 Forbidden errors. 40 | 41 | Indicates that the API key doesn't have permission to perform the requested operation. 42 | """ 43 | 44 | pass 45 | 46 | 47 | class PromptLayerNotFoundError(PromptLayerAPIError): 48 | """Exception raised for 404 Not Found errors. 49 | 50 | Indicates that the requested resource (e.g., prompt template) was not found. 51 | """ 52 | 53 | pass 54 | 55 | 56 | class PromptLayerConflictError(PromptLayerAPIError): 57 | """Exception raised for 409 Conflict errors. 58 | 59 | Indicates that the request conflicts with the current state of the resource. 60 | """ 61 | 62 | pass 63 | 64 | 65 | class PromptLayerUnprocessableEntityError(PromptLayerAPIError): 66 | """Exception raised for 422 Unprocessable Entity errors. 67 | 68 | Indicates that the request was well-formed but contains semantic errors. 69 | """ 70 | 71 | pass 72 | 73 | 74 | class PromptLayerRateLimitError(PromptLayerAPIError): 75 | """Exception raised for 429 Too Many Requests errors. 76 | 77 | Indicates that the API rate limit has been exceeded. 78 | """ 79 | 80 | pass 81 | 82 | 83 | class PromptLayerInternalServerError(PromptLayerAPIError): 84 | """Exception raised for 500+ Internal Server errors. 85 | 86 | Indicates that the PromptLayer API encountered an internal error. 87 | """ 88 | 89 | pass 90 | 91 | 92 | class PromptLayerAPIStatusError(PromptLayerAPIError): 93 | """Exception raised for other API errors not covered by specific exception classes.""" 94 | 95 | pass 96 | 97 | 98 | class PromptLayerAPIConnectionError(PromptLayerError): 99 | """Exception raised when unable to connect to the API. 100 | 101 | This can be due to network issues, timeouts, or connection errors. 102 | """ 103 | 104 | pass 105 | 106 | 107 | class PromptLayerAPITimeoutError(PromptLayerError): 108 | """Exception raised when an API request times out.""" 109 | 110 | pass 111 | 112 | 113 | class PromptLayerValidationError(PromptLayerError): 114 | """Exception raised when input validation fails. 115 | 116 | This can be due to invalid types, out of range values, or malformed data. 117 | """ 118 | 119 | pass 120 | -------------------------------------------------------------------------------- /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_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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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.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/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(base_url: str, headers): 14 | with assert_played("test_get_final_output_1.yaml"): 15 | assert (await _get_final_output(base_url, 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(base_url, 717, False, headers=headers)) == "AAA" 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_make_message_listener( 31 | base_url: str, 32 | headers, 33 | workflow_update_data_no_result_code, 34 | workflow_update_data_ok, 35 | workflow_update_data_exceeds_size_limit, 36 | ): 37 | results_future = asyncio.Future() 38 | execution_id_future = asyncio.Future() 39 | message_listener = _make_message_listener(base_url, results_future, execution_id_future, True, headers) 40 | execution_id_future.set_result(717) 41 | await message_listener(Message(name="INVALID")) 42 | assert not results_future.done() 43 | assert execution_id_future.done() 44 | 45 | # Final output is in the message 46 | for message_data in (workflow_update_data_no_result_code, workflow_update_data_ok): 47 | results_future = asyncio.Future() 48 | execution_id_future = asyncio.Future() 49 | execution_id_future.set_result(717) 50 | message_listener = _make_message_listener(base_url, results_future, execution_id_future, True, headers) 51 | await message_listener(Message(name="SET_WORKFLOW_COMPLETE", data=json.dumps(message_data))) 52 | assert results_future.done() 53 | assert (await asyncio.wait_for(results_future, 0.1)) == message_data["final_output"] 54 | 55 | # Final output is not in the message (return all outputs) 56 | with assert_played("test_make_message_listener_1.yaml"): 57 | results_future = asyncio.Future() 58 | execution_id_future = asyncio.Future() 59 | execution_id_future.set_result(717) 60 | message_listener = _make_message_listener(base_url, results_future, execution_id_future, True, headers) 61 | await message_listener( 62 | Message(name="SET_WORKFLOW_COMPLETE", data=json.dumps(workflow_update_data_exceeds_size_limit)) 63 | ) 64 | assert results_future.done() 65 | assert (await asyncio.wait_for(results_future, 0.1)) == { 66 | "Node 1": { 67 | "status": "SUCCESS", 68 | "value": "AAA", 69 | "error_message": None, 70 | "raw_error_message": None, 71 | "is_output_node": True, 72 | } 73 | } 74 | 75 | # Final output is not in the message (return final output) 76 | with assert_played("test_make_message_listener_2.yaml"): 77 | results_future = asyncio.Future() 78 | execution_id_future = asyncio.Future() 79 | execution_id_future.set_result(717) 80 | message_listener = _make_message_listener(base_url, results_future, execution_id_future, False, headers) 81 | await message_listener( 82 | Message(name="SET_WORKFLOW_COMPLETE", data=json.dumps(workflow_update_data_exceeds_size_limit)) 83 | ) 84 | assert results_future.done() 85 | assert (await asyncio.wait_for(results_future, 0.1)) == "AAA" 86 | 87 | 88 | @pytest.mark.asyncio 89 | async def test_wait_for_workflow_completion(workflow_update_data_ok): 90 | mock_channel = AsyncMock() 91 | mock_channel.unsubscribe = MagicMock() 92 | results_future = asyncio.Future() 93 | results_future.set_result(workflow_update_data_ok["final_output"]) 94 | message_listener = AsyncMock() 95 | actual_result = await _wait_for_workflow_completion(mock_channel, results_future, message_listener, 120) 96 | assert workflow_update_data_ok["final_output"] == actual_result 97 | mock_channel.unsubscribe.assert_called_once_with("SET_WORKFLOW_COMPLETE", message_listener) 98 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /promptlayer/streaming/stream_processor.py: -------------------------------------------------------------------------------- 1 | from typing import Any, AsyncGenerator, AsyncIterable, Callable, Dict, Generator 2 | 3 | from .blueprint_builder import ( 4 | build_prompt_blueprint_from_anthropic_event, 5 | build_prompt_blueprint_from_bedrock_event, 6 | build_prompt_blueprint_from_google_event, 7 | build_prompt_blueprint_from_openai_chunk, 8 | build_prompt_blueprint_from_openai_responses_event, 9 | ) 10 | 11 | 12 | def _build_stream_blueprint(result: Any, metadata: Dict) -> Any: 13 | model_info = metadata.get("model", {}) if metadata else {} 14 | provider = model_info.get("provider", "") 15 | model_name = model_info.get("name", "") 16 | 17 | if provider == "openai" or provider == "openai.azure": 18 | api_type = model_info.get("api_type", "chat-completions") if metadata else "chat-completions" 19 | if api_type == "chat-completions": 20 | return build_prompt_blueprint_from_openai_chunk(result, metadata) 21 | elif api_type == "responses": 22 | return build_prompt_blueprint_from_openai_responses_event(result, metadata) 23 | 24 | elif provider == "google" or (provider == "vertexai" and model_name.startswith("gemini")): 25 | return build_prompt_blueprint_from_google_event(result, metadata) 26 | 27 | elif provider in ("anthropic", "anthropic.bedrock") or (provider == "vertexai" and model_name.startswith("claude")): 28 | return build_prompt_blueprint_from_anthropic_event(result, metadata) 29 | 30 | elif provider == "mistral": 31 | return build_prompt_blueprint_from_openai_chunk(result.data, metadata) 32 | 33 | elif provider == "amazon.bedrock": 34 | return build_prompt_blueprint_from_bedrock_event(result, metadata) 35 | 36 | return None 37 | 38 | 39 | def _build_stream_data(result: Any, stream_blueprint: Any, request_id: Any = None) -> Dict[str, Any]: 40 | return { 41 | "request_id": request_id, 42 | "raw_response": result, 43 | "prompt_blueprint": stream_blueprint, 44 | } 45 | 46 | 47 | def stream_response(*, generator: Generator, after_stream: Callable, map_results: Callable, metadata: Dict): 48 | results = [] 49 | provider = metadata.get("model", {}).get("provider", "") 50 | if provider == "amazon.bedrock": 51 | response_metadata = generator.get("ResponseMetadata", {}) 52 | generator = generator.get("stream", generator) 53 | 54 | for result in generator: 55 | results.append(result) 56 | 57 | stream_blueprint = _build_stream_blueprint(result, metadata) 58 | data = _build_stream_data(result, stream_blueprint) 59 | yield data 60 | 61 | request_response = map_results(results) 62 | if provider == "amazon.bedrock": 63 | request_response["ResponseMetadata"] = response_metadata 64 | else: 65 | request_response = request_response.model_dump(mode="json") 66 | 67 | response = after_stream(request_response=request_response) 68 | data["request_id"] = response.get("request_id") 69 | data["prompt_blueprint"] = response.get("prompt_blueprint") 70 | yield data 71 | 72 | 73 | async def astream_response( 74 | generator: AsyncIterable[Any], 75 | after_stream: Callable[..., Any], 76 | map_results: Callable[[Any], Any], 77 | metadata: Dict[str, Any] = None, 78 | ) -> AsyncGenerator[Dict[str, Any], None]: 79 | results = [] 80 | provider = metadata.get("model", {}).get("provider", "") 81 | if provider == "amazon.bedrock": 82 | response_metadata = generator.get("ResponseMetadata", {}) 83 | generator = generator.get("stream", generator) 84 | 85 | async for result in generator: 86 | results.append(result) 87 | 88 | stream_blueprint = _build_stream_blueprint(result, metadata) 89 | data = _build_stream_data(result, stream_blueprint) 90 | yield data 91 | 92 | async def async_generator_from_list(lst): 93 | for item in lst: 94 | yield item 95 | 96 | request_response = await map_results(async_generator_from_list(results)) 97 | 98 | if provider == "amazon.bedrock": 99 | request_response["ResponseMetadata"] = response_metadata 100 | else: 101 | request_response = request_response.model_dump(mode="json") 102 | 103 | after_stream_response = await after_stream(request_response=request_response) 104 | data["request_id"] = after_stream_response.get("request_id") 105 | data["prompt_blueprint"] = after_stream_response.get("prompt_blueprint") 106 | yield data 107 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /promptlayer/track/track.py: -------------------------------------------------------------------------------- 1 | from promptlayer import exceptions as _exceptions 2 | from promptlayer.utils import ( 3 | apromptlayer_track_group, 4 | apromptlayer_track_metadata, 5 | apromptlayer_track_prompt, 6 | apromptlayer_track_score, 7 | promptlayer_track_group, 8 | promptlayer_track_metadata, 9 | promptlayer_track_prompt, 10 | promptlayer_track_score, 11 | ) 12 | 13 | 14 | def prompt( 15 | api_key: str, 16 | base_url: str, 17 | throw_on_error: bool, 18 | request_id, 19 | prompt_name, 20 | prompt_input_variables, 21 | version=None, 22 | label=None, 23 | ): 24 | if not isinstance(prompt_input_variables, dict): 25 | raise _exceptions.PromptLayerValidationError( 26 | "Please provide a dictionary of input variables.", response=None, body=None 27 | ) 28 | return promptlayer_track_prompt( 29 | api_key, base_url, throw_on_error, request_id, prompt_name, prompt_input_variables, api_key, version, label 30 | ) 31 | 32 | 33 | def metadata(api_key: str, base_url: str, throw_on_error: bool, request_id, metadata): 34 | if not isinstance(metadata, dict): 35 | raise _exceptions.PromptLayerValidationError( 36 | "Please provide a dictionary of metadata.", response=None, body=None 37 | ) 38 | for key, value in metadata.items(): 39 | if not isinstance(key, str) or not isinstance(value, str): 40 | raise _exceptions.PromptLayerValidationError( 41 | "Please provide a dictionary of metadata with key value pair of strings.", response=None, body=None 42 | ) 43 | return promptlayer_track_metadata(api_key, base_url, throw_on_error, request_id, metadata) 44 | 45 | 46 | def score(api_key: str, base_url: str, throw_on_error: bool, request_id, score, score_name=None): 47 | if not isinstance(score, int): 48 | raise _exceptions.PromptLayerValidationError("Please provide a int score.", response=None, body=None) 49 | if not isinstance(score_name, str) and score_name is not None: 50 | raise _exceptions.PromptLayerValidationError("Please provide a string as score name.", response=None, body=None) 51 | if score < 0 or score > 100: 52 | raise _exceptions.PromptLayerValidationError( 53 | "Please provide a score between 0 and 100.", response=None, body=None 54 | ) 55 | return promptlayer_track_score(api_key, base_url, throw_on_error, request_id, score, score_name) 56 | 57 | 58 | def group(api_key: str, base_url: str, throw_on_error: bool, request_id, group_id): 59 | return promptlayer_track_group(api_key, base_url, throw_on_error, request_id, group_id) 60 | 61 | 62 | async def aprompt( 63 | api_key: str, 64 | base_url: str, 65 | throw_on_error: bool, 66 | request_id, 67 | prompt_name, 68 | prompt_input_variables, 69 | version=None, 70 | label=None, 71 | ): 72 | if not isinstance(prompt_input_variables, dict): 73 | raise _exceptions.PromptLayerValidationError( 74 | "Please provide a dictionary of input variables.", response=None, body=None 75 | ) 76 | return await apromptlayer_track_prompt( 77 | api_key, base_url, throw_on_error, request_id, prompt_name, prompt_input_variables, version, label 78 | ) 79 | 80 | 81 | async def ametadata(api_key: str, base_url: str, throw_on_error: bool, request_id, metadata): 82 | if not isinstance(metadata, dict): 83 | raise _exceptions.PromptLayerValidationError( 84 | "Please provide a dictionary of metadata.", response=None, body=None 85 | ) 86 | for key, value in metadata.items(): 87 | if not isinstance(key, str) or not isinstance(value, str): 88 | raise _exceptions.PromptLayerValidationError( 89 | "Please provide a dictionary of metadata with key-value pairs of strings.", response=None, body=None 90 | ) 91 | return await apromptlayer_track_metadata(api_key, base_url, throw_on_error, request_id, metadata) 92 | 93 | 94 | async def ascore(api_key: str, base_url: str, throw_on_error: bool, request_id, score, score_name=None): 95 | if not isinstance(score, int): 96 | raise _exceptions.PromptLayerValidationError("Please provide an integer score.", response=None, body=None) 97 | if not isinstance(score_name, str) and score_name is not None: 98 | raise _exceptions.PromptLayerValidationError("Please provide a string as score name.", response=None, body=None) 99 | if score < 0 or score > 100: 100 | raise _exceptions.PromptLayerValidationError( 101 | "Please provide a score between 0 and 100.", response=None, body=None 102 | ) 103 | return await apromptlayer_track_score(api_key, base_url, throw_on_error, request_id, score, score_name) 104 | 105 | 106 | async def agroup(api_key: str, base_url: str, throw_on_error: bool, request_id, group_id): 107 | return await apromptlayer_track_group(api_key, base_url, throw_on_error, request_id, group_id) 108 | -------------------------------------------------------------------------------- /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_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_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_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_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.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 | -------------------------------------------------------------------------------- /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 WebAnnotation(TypedDict, total=False): 22 | type: Literal["web_annotation"] 23 | title: str 24 | url: str 25 | start_index: int 26 | end_index: int 27 | 28 | 29 | class FileAnnotation(TypedDict, total=False): 30 | type: Literal["file_annotation"] 31 | index: int 32 | file_id: str 33 | filename: str 34 | 35 | 36 | class TextContent(TypedDict, total=False): 37 | type: Literal["text"] 38 | text: str 39 | id: Union[str, None] 40 | annotations: Union[List[Union[WebAnnotation, FileAnnotation]], None] 41 | 42 | 43 | class CodeContent(TypedDict, total=False): 44 | type: Literal["code"] 45 | code: str 46 | id: Union[str, None] 47 | container_id: Union[str, None] 48 | 49 | 50 | class ThinkingContent(TypedDict, total=False): 51 | signature: Union[str, None] 52 | type: Literal["thinking"] 53 | thinking: str 54 | id: Union[str, None] 55 | 56 | 57 | class ImageContent(TypedDict, total=False): 58 | type: Literal["image_url"] 59 | image_url: ImageUrl 60 | 61 | 62 | class Media(TypedDict, total=False): 63 | title: str 64 | type: str 65 | url: str 66 | 67 | 68 | class MediaContnt(TypedDict, total=False): 69 | type: Literal["media"] 70 | media: Media 71 | 72 | 73 | class MediaVariable(TypedDict, total=False): 74 | type: Literal["media_variable"] 75 | name: str 76 | 77 | 78 | Content = Union[TextContent, ThinkingContent, CodeContent, ImageContent, MediaContnt, MediaVariable] 79 | 80 | 81 | class Function(TypedDict, total=False): 82 | name: str 83 | description: str 84 | parameters: dict 85 | 86 | 87 | class Tool(TypedDict, total=False): 88 | type: Literal["function"] 89 | function: Function 90 | 91 | 92 | class FunctionCall(TypedDict, total=False): 93 | name: str 94 | arguments: str 95 | 96 | 97 | class SystemMessage(TypedDict, total=False): 98 | role: Literal["system"] 99 | input_variables: List[str] 100 | template_format: TemplateFormat 101 | content: Sequence[Content] 102 | name: str 103 | 104 | 105 | class UserMessage(TypedDict, total=False): 106 | role: Literal["user"] 107 | input_variables: List[str] 108 | template_format: TemplateFormat 109 | content: Sequence[Content] 110 | name: str 111 | 112 | 113 | class ToolCall(TypedDict, total=False): 114 | id: str 115 | tool_id: Union[str, None] 116 | type: Literal["function"] 117 | function: FunctionCall 118 | 119 | 120 | class AssistantMessage(TypedDict, total=False): 121 | role: Literal["assistant"] 122 | input_variables: List[str] 123 | template_format: TemplateFormat 124 | content: Sequence[Content] 125 | function_call: FunctionCall 126 | name: str 127 | tool_calls: List[ToolCall] 128 | 129 | 130 | class FunctionMessage(TypedDict, total=False): 131 | role: Literal["function"] 132 | input_variables: List[str] 133 | template_format: TemplateFormat 134 | content: Sequence[Content] 135 | name: str 136 | 137 | 138 | class ToolMessage(TypedDict, total=False): 139 | role: Literal["tool"] 140 | input_variables: List[str] 141 | template_format: TemplateFormat 142 | content: Sequence[Content] 143 | tool_call_id: str 144 | name: str 145 | 146 | 147 | class PlaceholderMessage(TypedDict, total=False): 148 | role: Literal["placeholder"] 149 | name: str 150 | 151 | 152 | class DeveloperMessage(TypedDict, total=False): 153 | role: Literal["developer"] 154 | input_variables: List[str] 155 | template_format: TemplateFormat 156 | content: Sequence[Content] 157 | 158 | 159 | class ChatFunctionCall(TypedDict, total=False): 160 | name: str 161 | 162 | 163 | class ChatToolChoice(TypedDict, total=False): 164 | type: Literal["function"] 165 | function: ChatFunctionCall 166 | 167 | 168 | ToolChoice = Union[str, ChatToolChoice] 169 | 170 | Message = Union[ 171 | SystemMessage, 172 | UserMessage, 173 | AssistantMessage, 174 | FunctionMessage, 175 | ToolMessage, 176 | PlaceholderMessage, 177 | DeveloperMessage, 178 | ] 179 | 180 | 181 | class CompletionPromptTemplate(TypedDict, total=False): 182 | type: Required[Literal["completion"]] 183 | template_format: TemplateFormat 184 | content: Sequence[Content] 185 | input_variables: List[str] 186 | 187 | 188 | class ChatPromptTemplate(TypedDict, total=False): 189 | type: Required[Literal["chat"]] 190 | messages: Required[Sequence[Message]] 191 | functions: Sequence[Function] 192 | function_call: Union[Literal["auto", "none"], ChatFunctionCall] 193 | input_variables: List[str] 194 | tools: Sequence[Tool] 195 | tool_choice: ToolChoice 196 | 197 | 198 | PromptTemplate = Union[CompletionPromptTemplate, ChatPromptTemplate] 199 | 200 | 201 | class Model(TypedDict, total=False): 202 | provider: Required[str] 203 | name: Required[str] 204 | parameters: Required[Dict[str, object]] 205 | 206 | 207 | class Metadata(TypedDict, total=False): 208 | model: Model 209 | 210 | 211 | class BasePromptTemplate(TypedDict, total=False): 212 | prompt_name: str 213 | tags: List[str] 214 | 215 | 216 | class PromptBlueprint(TypedDict, total=False): 217 | prompt_template: PromptTemplate 218 | commit_message: str 219 | metadata: Metadata 220 | 221 | 222 | class PublishPromptTemplate(BasePromptTemplate, PromptBlueprint, total=False): 223 | release_labels: Optional[List[str]] = None 224 | 225 | 226 | class BaseProviderBaseURL(TypedDict): 227 | name: Required[str] 228 | provider: Required[str] 229 | url: Required[str] 230 | 231 | 232 | class ProviderBaseURL(BaseProviderBaseURL): 233 | id: Required[int] 234 | 235 | 236 | class BasePromptTemplateResponse(TypedDict, total=False): 237 | id: Required[int] 238 | prompt_name: Required[str] 239 | tags: List[str] 240 | prompt_template: Required[PromptTemplate] 241 | commit_message: str 242 | metadata: Metadata 243 | provider_base_url: ProviderBaseURL 244 | 245 | 246 | a: BasePromptTemplateResponse = {"provider_base_url": {"url": ""}} 247 | 248 | 249 | class PublishPromptTemplateResponse(BasePromptTemplateResponse): 250 | pass 251 | 252 | 253 | class GetPromptTemplateResponse(BasePromptTemplateResponse): 254 | llm_kwargs: Union[Dict[str, object], None] 255 | version: int 256 | 257 | 258 | class ListPromptTemplateResponse(BasePromptTemplateResponse, total=False): 259 | version: int 260 | -------------------------------------------------------------------------------- /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_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_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 | -------------------------------------------------------------------------------- /promptlayer/promptlayer_base.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import inspect 3 | import re 4 | 5 | from promptlayer import exceptions as _exceptions 6 | from promptlayer.utils import async_wrapper, promptlayer_api_handler 7 | 8 | 9 | class PromptLayerBase(object): 10 | __slots__ = [ 11 | "_obj", 12 | "__weakref__", 13 | "_function_name", 14 | "_provider_type", 15 | "_api_key", 16 | "_tracer", 17 | "_base_url", 18 | ] 19 | 20 | def __init__(self, api_key: str, base_url: str, obj, function_name="", provider_type="openai", tracer=None): 21 | object.__setattr__(self, "_obj", obj) 22 | object.__setattr__(self, "_function_name", function_name) 23 | object.__setattr__(self, "_provider_type", provider_type) 24 | object.__setattr__(self, "_api_key", api_key) 25 | object.__setattr__(self, "_tracer", tracer) 26 | object.__setattr__(self, "_base_url", base_url) 27 | 28 | def __getattr__(self, name): 29 | attr = getattr(object.__getattribute__(self, "_obj"), name) 30 | 31 | if ( 32 | name != "count_tokens" # fix for anthropic count_tokens 33 | and not re.match(r"", str(attr)) # fix for anthropic errors 34 | and not re.match(r"", str(attr)) # fix for openai errors 35 | and ( 36 | inspect.isclass(attr) 37 | or inspect.isfunction(attr) 38 | or inspect.ismethod(attr) 39 | or str(type(attr)) == "" 40 | or str(type(attr)) == "" 41 | or str(type(attr)) == "" 42 | or str(type(attr)) == "" 43 | or re.match(r"", str(type(attr))) 44 | ) 45 | ): 46 | return PromptLayerBase( 47 | object.__getattribute__(self, "_api_key"), 48 | object.__getattribute__(self, "_base_url"), 49 | attr, 50 | function_name=f"{object.__getattribute__(self, '_function_name')}.{name}", 51 | provider_type=object.__getattribute__(self, "_provider_type"), 52 | tracer=object.__getattribute__(self, "_tracer"), 53 | ) 54 | return attr 55 | 56 | def __delattr__(self, name): 57 | delattr(object.__getattribute__(self, "_obj"), name) 58 | 59 | def __setattr__(self, name, value): 60 | setattr(object.__getattribute__(self, "_obj"), name, value) 61 | 62 | def __call__(self, *args, **kwargs): 63 | tags = kwargs.pop("pl_tags", None) 64 | if tags is not None and not isinstance(tags, list): 65 | raise _exceptions.PromptLayerValidationError("pl_tags must be a list of strings.", response=None, body=None) 66 | 67 | return_pl_id = kwargs.pop("return_pl_id", False) 68 | request_start_time = datetime.datetime.now().timestamp() 69 | function_object = object.__getattribute__(self, "_obj") 70 | tracer = object.__getattribute__(self, "_tracer") 71 | function_name = object.__getattribute__(self, "_function_name") 72 | 73 | if tracer: 74 | with tracer.start_as_current_span(function_name) as llm_request_span: 75 | llm_request_span_id = hex(llm_request_span.context.span_id)[2:].zfill(16) 76 | llm_request_span.set_attribute("provider", object.__getattribute__(self, "_provider_type")) 77 | llm_request_span.set_attribute("function_name", function_name) 78 | llm_request_span.set_attribute("function_input", str({"args": args, "kwargs": kwargs})) 79 | 80 | if inspect.isclass(function_object): 81 | result = PromptLayerBase( 82 | object.__getattribute__(self, "_api_key"), 83 | object.__getattribute__(self, "_base_url"), 84 | function_object(*args, **kwargs), 85 | function_name=function_name, 86 | provider_type=object.__getattribute__(self, "_provider_type"), 87 | tracer=tracer, 88 | ) 89 | llm_request_span.set_attribute("function_output", str(result)) 90 | return result 91 | 92 | function_response = function_object(*args, **kwargs) 93 | 94 | if inspect.iscoroutinefunction(function_object) or inspect.iscoroutine(function_response): 95 | return async_wrapper( 96 | object.__getattribute__(self, "_api_key"), 97 | object.__getattribute__(self, "_base_url"), 98 | function_response, 99 | return_pl_id, 100 | request_start_time, 101 | function_name, 102 | object.__getattribute__(self, "_provider_type"), 103 | tags, 104 | llm_request_span_id=llm_request_span_id, 105 | tracer=tracer, # Pass the tracer to async_wrapper 106 | *args, 107 | **kwargs, 108 | ) 109 | 110 | request_end_time = datetime.datetime.now().timestamp() 111 | result = promptlayer_api_handler( 112 | object.__getattribute__(self, "_api_key"), 113 | object.__getattribute__(self, "_base_url"), 114 | function_name, 115 | object.__getattribute__(self, "_provider_type"), 116 | args, 117 | kwargs, 118 | tags, 119 | function_response, 120 | request_start_time, 121 | request_end_time, 122 | return_pl_id=return_pl_id, 123 | llm_request_span_id=llm_request_span_id, 124 | ) 125 | llm_request_span.set_attribute("function_output", str(result)) 126 | return result 127 | else: 128 | # Without tracing 129 | if inspect.isclass(function_object): 130 | return PromptLayerBase( 131 | object.__getattribute__(self, "_api_key"), 132 | object.__getattribute__(self, "_base_url"), 133 | function_object(*args, **kwargs), 134 | function_name=function_name, 135 | provider_type=object.__getattribute__(self, "_provider_type"), 136 | ) 137 | 138 | function_response = function_object(*args, **kwargs) 139 | 140 | if inspect.iscoroutinefunction(function_object) or inspect.iscoroutine(function_response): 141 | return async_wrapper( 142 | object.__getattribute__(self, "_api_key"), 143 | object.__getattribute__(self, "_base_url"), 144 | function_response, 145 | return_pl_id, 146 | request_start_time, 147 | function_name, 148 | object.__getattribute__(self, "_provider_type"), 149 | tags, 150 | *args, 151 | **kwargs, 152 | ) 153 | 154 | request_end_time = datetime.datetime.now().timestamp() 155 | return promptlayer_api_handler( 156 | object.__getattribute__(self, "_api_key"), 157 | object.__getattribute__(self, "_base_url"), 158 | function_name, 159 | object.__getattribute__(self, "_provider_type"), 160 | args, 161 | kwargs, 162 | tags, 163 | function_response, 164 | request_start_time, 165 | request_end_time, 166 | return_pl_id=return_pl_id, 167 | ) 168 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------