Skip to content

Commit 29204c7

Browse files
authored
Merge pull request #3081 from locustio/add-openai-User-and-example
Add OpenAI User and example
2 parents f600fdd + 8ae54df commit 29204c7

File tree

3 files changed

+112
-0
lines changed

3 files changed

+112
-0
lines changed

Diff for: docs/testing-other-systems.rst

+11
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ REST
5858

5959
See :ref:`FastHttpUser <rest>`
6060

61+
OpenAI
62+
======
63+
64+
Performance/load testing AI services is a little different. While you could call the OpenAI API using HttpUser or FastHttpUser, it is often convenient to use `the SDK <https://github.com/openai/openai-python>`_.
65+
66+
.. literalinclude:: ../examples/openai_ex.py
67+
68+
.. note::
69+
70+
OpenAIUser is experimental and may change without notice.
71+
6172
Other examples
6273
==============
6374

Diff for: examples/openai_ex.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# You need to install the openai package and set OPENAI_API_KEY env var to run this
2+
3+
# OpenAIUser tracks the number of output tokens in the response_length field,
4+
# because it is more useful than the actual payload size. This field is available to event handlers,
5+
# but only graphed in Locust Cloud.
6+
7+
from locust import run_single_user, task
8+
from locust.contrib.oai import OpenAIUser
9+
10+
11+
class MyUser(OpenAIUser):
12+
@task
13+
def t(self):
14+
self.client.responses.create(
15+
model="gpt-4o",
16+
instructions="You are a coding assistant that speaks like it were a Monty Python skit.",
17+
input="How do I check if a Python object is an instance of a class?",
18+
)
19+
with self.client.rename_request("mini"): # here's how to rename requests
20+
self.client.responses.create(
21+
model="gpt-4o-mini",
22+
instructions="You are a coding assistant that speaks like it were a Monty Python skit.",
23+
input="How do I check if a Python object is an instance of a class?",
24+
)
25+
26+
27+
if __name__ == "__main__":
28+
run_single_user(MyUser)

Diff for: locust/contrib/oai.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Note: this User is experimental and may change without notice.
2+
# The filename is oai.py so it doesnt clash with the openai package.
3+
from locust.user import User
4+
5+
import os
6+
import time
7+
from collections.abc import Generator
8+
from contextlib import contextmanager
9+
10+
import httpx
11+
from openai import OpenAI # dont forget to install openai
12+
13+
# convenience for when running in Locust Cloud, where only LOCUST_* env vars are forwarded
14+
if "LOCUST_OPENAI_API_KEY" in os.environ:
15+
os.environ["OPENAI_API_KEY"] = os.environ["LOCUST_OPENAI_API_KEY"]
16+
17+
if not "OPENAI_API_KEY" in os.environ:
18+
raise Exception("You need to set OPENAI_API_KEY or LOCUST_OPENAI_API_KEY env var to use OpenAIUser")
19+
20+
21+
class OpenAIClient(OpenAI):
22+
def __init__(self, request_event, user, *args, **kwargs):
23+
self.request_name = None # used to override url-based request names
24+
self.user = user # currently unused, but could be useful later
25+
26+
def request_start(request):
27+
request.start_time = time.time()
28+
request.start_perf_counter = time.perf_counter()
29+
30+
def request_end(response):
31+
exception = None
32+
response.read()
33+
response_time = (time.perf_counter() - response.request.start_perf_counter) * 1000
34+
try:
35+
response.raise_for_status()
36+
except httpx.HTTPStatusError as e:
37+
exception = e
38+
request_event.fire(
39+
request_type=response.request.method,
40+
name=self.request_name if self.request_name else response.url.path,
41+
context={},
42+
response=response,
43+
exception=exception,
44+
start_time=response.request.start_time,
45+
response_time=response_time,
46+
# Store the number of output tokens as response_length instead of the actual payload size because it is more useful
47+
response_length=response.json().get("usage", {}).get("output_tokens", 0),
48+
url=response.url,
49+
)
50+
51+
super().__init__(
52+
*args,
53+
**kwargs,
54+
http_client=httpx.Client(event_hooks={"request": [request_start], "response": [request_end]}),
55+
)
56+
57+
@contextmanager
58+
def rename_request(self, name: str) -> Generator[None]:
59+
"""Group requests using the "with" keyword"""
60+
61+
self.request_name = name
62+
try:
63+
yield
64+
finally:
65+
self.request_name = None
66+
67+
68+
class OpenAIUser(User):
69+
abstract = True
70+
71+
def __init__(self, *args, **kwargs):
72+
super().__init__(*args, **kwargs)
73+
self.client = OpenAIClient(self.environment.events.request, user=self)

0 commit comments

Comments
 (0)