Skip to content

Commit 6c139b5

Browse files
authored
MAINT Breaking: Updating openai targets and parameters (Azure#1179)
1 parent 6b564ec commit 6c139b5

30 files changed

+279
-864
lines changed

.env_example

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ PLATFORM_OPENAI_CHAT_ENDPOINT="https://api.openai.com/v1/chat/completions"
1414
PLATFORM_OPENAI_CHAT_API_KEY="sk-xxxxx"
1515
PLATFORM_OPENAI_CHAT_GPT4O_MODEL="gpt-4o"
1616

17-
AZURE_OPENAI_GPT4O_ENDPOINT="https://xxxx.openai.azure.com/openai/deployments/xxxxx/chat/completions"
17+
# Note: For Azure OpenAI endpoints, include the api-version query parameter in the URL
18+
# Example: https://xxxx.openai.azure.com/openai/deployments/xxxxx/chat/completions?api-version=2024-10-21
19+
AZURE_OPENAI_GPT4O_ENDPOINT="https://xxxx.openai.azure.com/openai/deployments/xxxxx/chat/completions?api-version=2024-10-21"
1820
AZURE_OPENAI_GPT4O_KEY="xxxxx"
1921

20-
AZURE_OPENAI_INTEGRATION_TEST_ENDPOINT="https://xxxxx.openai.azure.com/openai/deployments/xxxx/chat/completions"
22+
AZURE_OPENAI_INTEGRATION_TEST_ENDPOINT="https://xxxxx.openai.azure.com/openai/deployments/xxxx/chat/completions?api-version=2024-10-21"
2123
AZURE_OPENAI_INTEGRATION_TEST_KEY="xxxxx"
2224

23-
AZURE_OPENAI_GPT3_5_CHAT_ENDPOINT="https://xxxxx.openai.azure.com/openai/deployments/xxxxx/chat/completions"
25+
AZURE_OPENAI_GPT3_5_CHAT_ENDPOINT="https://xxxxx.openai.azure.com/openai/deployments/xxxxx/chat/completions?api-version=2024-10-21"
2426
AZURE_OPENAI_GPT3_5_CHAT_KEY="xxxxx"
2527

26-
AZURE_OPENAI_GPT4_CHAT_ENDPOINT="https://xxxxx.openai.azure.com/openai/deployments/xxxxx/chat/completions"
28+
AZURE_OPENAI_GPT4_CHAT_ENDPOINT="https://xxxxx.openai.azure.com/openai/deployments/xxxxx/chat/completions?api-version=2024-10-21"
2729
AZURE_OPENAI_GPT4_CHAT_KEY="xxxxx"
2830

2931
AZURE_FOUNDRY_DEEPSEEK_ENDPOINT="https://xxxxx.eastus2.models.ai.azure.com/chat/completions"
@@ -58,7 +60,7 @@ OPENAI_CHAT_MODEL=${PLATFORM_OPENAI_CHAT_GPT4O_MODEL}
5860
# OPENAI RESPONSES TARGET SECRETS
5961
##################################
6062

61-
AZURE_OPENAI_GPT5_RESPONSES_ENDPOINT="https://xxxxxxxxx.azure.com/openai/deployments/gpt-5/responses"
63+
AZURE_OPENAI_GPT5_RESPONSES_ENDPOINT="https://xxxxxxxxx.azure.com/openai/deployments/gpt-5/responses?api-version=2025-03-01-preview"
6264
AZURE_OPENAI_GPT5_COMPLETION_ENDPOINT="https://xxxxxxxxx.azure.com/openai/deployments/gpt-5/chat/completions?api-version=2025-01-01-preview"
6365
AZURE_OPENAI_GPT5_KEY="xxxxxxx"
6466
AZURE_OPENAI_GPT5_MODEL="gpt-5"
@@ -67,7 +69,7 @@ PLATFORM_OPENAI_RESPONSES_ENDPOINT="https://api.openai.com/v1/responses"
6769
PLATFORM_OPENAI_RESPONSES_KEY="sk-xxxxx"
6870
PLATFORM_OPENAI_RESPONSES_MODEL="o4-mini"
6971

70-
AZURE_OPENAI_RESPONSES_ENDPOINT="https://xxxxx.openai.azure.com/openai/responses"
72+
AZURE_OPENAI_RESPONSES_ENDPOINT="https://xxxxx.openai.azure.com/openai/responses?api-version=2025-03-01-preview"
7173
AZURE_OPENAI_RESPONSES_KEY="xxxxx"
7274
AZURE_OPENAI_RESPONSES_MODEL="o4-mini"
7375

@@ -86,7 +88,7 @@ PLATFORM_OPENAI_REALTIME_ENDPOINT="wss://api.openai.com/v1/realtime"
8688
PLATFORM_OPENAI_REALTIME_API_KEY="sk-xxxxx"
8789
PLATFORM_OPENAI_REALTIME_MODEL="gpt-4o-realtime-preview"
8890

89-
AZURE_OPENAI_REALTIME_ENDPOINT = "wss://xxxx.openai.azure.com/openai/realtime"
91+
AZURE_OPENAI_REALTIME_ENDPOINT = "wss://xxxx.openai.azure.com/openai/realtime?api-version=2024-10-01-preview"
9092
AZURE_OPENAI_REALTIME_API_KEY = "xxxxx"
9193
AZURE_OPENAI_REALTIME_MODEL = "gpt-4o-realtime-preview"
9294

@@ -101,7 +103,7 @@ OPENAI_REALTIME_MODEL = ${PLATFORM_OPENAI_REALTIME_MODEL}
101103
# or copy to OPENAI_DALLE_ENDPOINT
102104
###################################
103105

104-
OPENAI_DALLE_ENDPOINT1 = "https://xxxxx.openai.azure.com/openai/deployments/xxxxx/images/generations"
106+
OPENAI_DALLE_ENDPOINT1 = "https://xxxxx.openai.azure.com/openai/deployments/xxxxx/images/generations?api-version=2024-10-21"
105107
OPENAI_DALLE_API_KEY1 = "xxxxxx"
106108

107109
OPENAI_DALLE_ENDPOINT2 = "https://api.openai.com/v1/images/generations"

pyrit/common/net_utility.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Licensed under the MIT license.
33

44
from typing import Any, Literal, Optional
5+
from urllib.parse import parse_qs, urlparse, urlunparse
56

67
import httpx
78
from tenacity import retry, stop_after_attempt, wait_fixed
@@ -21,6 +22,45 @@ def get_httpx_client(use_async: bool = False, debug: bool = False, **httpx_clien
2122
return client_class(proxy=proxy, verify=verify_certs, timeout=timeout, **httpx_client_kwargs)
2223

2324

25+
def extract_url_parameters(url: str) -> dict[str, str]:
26+
"""
27+
Extract query parameters from a URL.
28+
29+
Args:
30+
url (str): The URL to extract parameters from.
31+
32+
Returns:
33+
dict[str, str]: Dictionary of query parameters (flattened from lists).
34+
"""
35+
parsed_url = urlparse(url)
36+
url_params = parse_qs(parsed_url.query)
37+
# Flatten params (parse_qs returns lists)
38+
return {k: v[0] if isinstance(v, list) and len(v) > 0 else "" for k, v in url_params.items()}
39+
40+
41+
def remove_url_parameters(url: str) -> str:
42+
"""
43+
Remove query parameters from a URL, returning just the base URL.
44+
45+
Args:
46+
url (str): The URL to clean.
47+
48+
Returns:
49+
str: The URL without query parameters.
50+
"""
51+
parsed_url = urlparse(url)
52+
return urlunparse(
53+
(
54+
parsed_url.scheme,
55+
parsed_url.netloc,
56+
parsed_url.path,
57+
parsed_url.params,
58+
"", # Remove query string
59+
parsed_url.fragment,
60+
)
61+
)
62+
63+
2464
PostType = Literal["json", "data"]
2565

2666

@@ -30,23 +70,39 @@ async def make_request_and_raise_if_error_async(
3070
method: str,
3171
post_type: PostType = "json",
3272
debug: bool = False,
33-
params: Optional[dict[str, str]] = None,
73+
extra_url_parameters: Optional[dict[str, str]] = None,
3474
request_body: Optional[dict[str, object]] = None,
3575
files: Optional[dict[str, tuple]] = None,
3676
headers: Optional[dict[str, str]] = None,
3777
**httpx_client_kwargs: Optional[Any],
3878
) -> httpx.Response:
39-
"""Make a request and raise an exception if it fails."""
79+
"""
80+
Make a request and raise an exception if it fails.
81+
82+
Query parameters can be specified either:
83+
1. In the endpoint_uri (e.g., "https://api.com/endpoint?api-version=2024-10-21")
84+
2. Via the extra_url_parameters dict
85+
3. Both (extra_url_parameters will be merged with URL query parameters, with extra_url_parameters taking precedence)
86+
"""
4087
headers = headers or {}
4188
request_body = request_body or {}
4289

43-
params = params or {}
90+
# Extract any existing query parameters from the URL
91+
url_params = extract_url_parameters(endpoint_uri)
92+
93+
# Merge URL parameters with provided extra_url_parameters (extra_url_parameters takes precedence)
94+
merged_params = url_params.copy()
95+
if extra_url_parameters:
96+
merged_params.update(extra_url_parameters)
97+
98+
# Get clean URL without query string (we'll pass params separately to httpx)
99+
clean_url = remove_url_parameters(endpoint_uri)
44100

45101
async with get_httpx_client(debug=debug, use_async=True, **httpx_client_kwargs) as async_client:
46102
response = await async_client.request(
47103
method=method,
48-
params=params,
49-
url=endpoint_uri,
104+
params=merged_params if merged_params else None,
105+
url=clean_url,
50106
json=request_body if request_body and post_type == "json" and not files else None,
51107
data=request_body if request_body and post_type != "json" and not files else None,
52108
files=files if files else None,

pyrit/prompt_target/openai/openai_chat_target.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ def __init__(
8383
instead of API Key. DefaultAzureCredential is taken for
8484
https://cognitiveservices.azure.com/.default . Please run `az login` locally
8585
to leverage user AuthN.
86-
api_version (str, Optional): The version of the Azure OpenAI API. Defaults to
87-
"2024-10-21".
8886
max_requests_per_minute (int, Optional): Number of requests the target can handle per
8987
minute before hitting a rate limit. The number of requests sent to the target
9088
will be capped at the value provided.
@@ -134,8 +132,8 @@ def __init__(
134132
if max_completion_tokens and max_tokens:
135133
raise ValueError("Cannot provide both max_tokens and max_completion_tokens.")
136134

137-
# Validate endpoint URL
138-
self._warn_if_irregular_endpoint(self.CHAT_URL_REGEX)
135+
chat_url_patterns = [r"/chat/completions"]
136+
self._warn_if_irregular_endpoint(chat_url_patterns)
139137

140138
self._max_completion_tokens = max_completion_tokens
141139
self._max_tokens = max_tokens

pyrit/prompt_target/openai/openai_chat_target_base.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ def __init__(
5454
instead of API Key. DefaultAzureCredential is taken for
5555
https://cognitiveservices.azure.com/.default . Please run `az login` locally
5656
to leverage user AuthN.
57-
api_version (str, Optional): The version of the Azure OpenAI API. Defaults to
58-
"2024-10-21".
5957
max_requests_per_minute (int, Optional): Number of requests the target can handle per
6058
minute before hitting a rate limit. The number of requests sent to the target
6159
will be capped at the value provided.
@@ -119,17 +117,12 @@ async def send_prompt_async(self, *, message: Message) -> Message:
119117

120118
body = await self._construct_request_body(conversation=conversation, is_json_response=is_json_response)
121119

122-
params = {}
123-
if self._api_version is not None:
124-
params["api-version"] = self._api_version
125-
126120
try:
127121
str_response: httpx.Response = await net_utility.make_request_and_raise_if_error_async(
128122
endpoint_uri=self._endpoint,
129123
method="POST",
130124
headers=self._headers,
131125
request_body=body,
132-
params=params,
133126
**self._httpx_client_kwargs,
134127
)
135128
except httpx.HTTPStatusError as StatusError:

pyrit/prompt_target/openai/openai_completion_target.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ def __init__(
4444
instead of API Key. DefaultAzureCredential is taken for
4545
https://cognitiveservices.azure.com/.default . Please run `az login` locally
4646
to leverage user AuthN.
47-
api_version (str, Optional): The version of the Azure OpenAI API. Defaults to
48-
"2024-06-01".
4947
max_requests_per_minute (int, Optional): Number of requests the target can handle per
5048
minute before hitting a rate limit. The number of requests sent to the target
5149
will be capped at the value provided.
@@ -93,17 +91,12 @@ async def send_prompt_async(self, *, message: Message) -> Message:
9391

9492
body = await self._construct_request_body(request=message_piece)
9593

96-
params = {}
97-
if self._api_version is not None:
98-
params["api-version"] = self._api_version
99-
10094
try:
10195
str_response: httpx.Response = await net_utility.make_request_and_raise_if_error_async(
10296
endpoint_uri=self._endpoint,
10397
method="POST",
10498
headers=self._headers,
10599
request_body=body,
106-
params=params,
107100
**self._httpx_client_kwargs,
108101
)
109102
except httpx.HTTPStatusError as StatusError:

pyrit/prompt_target/openai/openai_dall_e_target.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,7 @@
2525

2626

2727
class OpenAIDALLETarget(OpenAITarget):
28-
"""OpenAI DALL-E Target for generating images from text prompts.
29-
30-
This class provides an interface to OpenAI's DALL-E image generation API,
31-
supporting various image generation parameters and formats. It handles
32-
prompt processing, API communication, and image response handling.
33-
"""
28+
"""A target for image generation using OpenAI's DALL-E models."""
3429

3530
def __init__(
3631
self,
@@ -55,8 +50,6 @@ def __init__(
5550
instead of API Key. DefaultAzureCredential is taken for
5651
https://cognitiveservices.azure.com/.default . Please run `az login` locally
5752
to leverage user AuthN.
58-
api_version (str, Optional): The version of the Azure OpenAI API. Defaults to
59-
"2024-06-01".
6053
max_requests_per_minute (int, Optional): Number of requests the target can handle per
6154
minute before hitting a rate limit. The number of requests sent to the target
6255
will be capped at the value provided.
@@ -96,8 +89,8 @@ def __init__(
9689

9790
super().__init__(*args, **kwargs)
9891

99-
# Validate endpoint URL
100-
self._warn_if_irregular_endpoint(self.DALLE_URL_REGEX)
92+
dalle_url_patterns = [r"/images/generations"]
93+
self._warn_if_irregular_endpoint(dalle_url_patterns)
10194

10295
def _set_openai_env_configuration_vars(self):
10396
self.model_name_environment_variable = "OPENAI_DALLE_MODEL"
@@ -130,17 +123,12 @@ async def send_prompt_async(
130123

131124
body = self._construct_request_body(prompt=request.converted_value)
132125

133-
params = {}
134-
if self._api_version is not None:
135-
params["api-version"] = self._api_version
136-
137126
try:
138127
http_response: httpx.Response = await net_utility.make_request_and_raise_if_error_async(
139128
endpoint_uri=self._endpoint,
140129
method="POST",
141130
headers=self._headers,
142131
request_body=body,
143-
params=params,
144132
**self._httpx_client_kwargs,
145133
)
146134

pyrit/prompt_target/openai/openai_realtime_target.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ class RealtimeTarget(OpenAITarget):
5353
def __init__(
5454
self,
5555
*,
56-
api_version: str = "2025-04-01-preview",
5756
system_prompt: Optional[str] = None,
5857
voice: Optional[RealTimeVoice] = None,
5958
existing_convo: Optional[dict] = None,
@@ -75,12 +74,9 @@ def __init__(
7574
instead of API Key. DefaultAzureCredential is taken for
7675
https://cognitiveservices.azure.com/.default . Please run `az login` locally
7776
to leverage user AuthN.
78-
api_version (str, Optional): The version of the Azure OpenAI API. Defaults to
79-
"2024-06-01".
8077
max_requests_per_minute (int, Optional): Number of requests the target can handle per
8178
minute before hitting a rate limit. The number of requests sent to the target
8279
will be capped at the value provided.
83-
api_version (str, Optional): The version of the Azure OpenAI API. Defaults to "2024-10-01-preview".
8480
system_prompt (str, Optional): The system prompt to use. Defaults to "You are a helpful AI assistant".
8581
voice (literal str, Optional): The voice to use. Defaults to None.
8682
the only supported voices by the AzureOpenAI Realtime API are "alloy", "echo", and "shimmer".
@@ -90,7 +86,7 @@ def __init__(
9086
For example, to specify a 3 minutes timeout: httpx_client_kwargs={"timeout": 180}
9187
"""
9288

93-
super().__init__(api_version=api_version, **kwargs)
89+
super().__init__(**kwargs)
9490

9591
self.system_prompt = system_prompt or "You are a helpful AI assistant"
9692
self.voice = voice
@@ -116,10 +112,9 @@ async def connect(self):
116112

117113
self._add_auth_param_to_query_params(query_params)
118114

119-
if self._api_version is not None:
120-
query_params["api-version"] = self._api_version
121-
122-
url = f"{self._endpoint}?{urlencode(query_params)}"
115+
# Check if endpoint already has query parameters
116+
separator = "&" if "?" in self._endpoint else "?"
117+
url = f"{self._endpoint}{separator}{urlencode(query_params)}"
123118
websocket = await websockets.connect(url)
124119
logger.info("Successfully connected to AzureOpenAI Realtime API")
125120
return websocket

pyrit/prompt_target/openai/openai_response_target.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ def __init__(
6565
self,
6666
*,
6767
custom_functions: Optional[Dict[str, ToolExecutor]] = None,
68-
api_version: Optional[str] = "2025-03-01-preview",
6968
max_output_tokens: Optional[int] = None,
7069
temperature: Optional[float] = None,
7170
top_p: Optional[float] = None,
@@ -83,8 +82,6 @@ def __init__(
8382
api_key (str, Optional): The API key for accessing the Azure OpenAI service.
8483
Defaults to the OPENAI_RESPONSES_KEY environment variable.
8584
headers (str, Optional): Headers of the endpoint (JSON).
86-
api_version (str, Optional): The version of the Azure OpenAI API. Defaults to
87-
"2025-03-01-preview".
8885
max_requests_per_minute (int, Optional): Number of requests the target can handle per
8986
minute before hitting a rate limit. The number of requests sent to the target
9087
will be capped at the value provided.
@@ -118,11 +115,11 @@ def __init__(
118115
json.JSONDecodeError: If the response from the target is not valid JSON.
119116
Exception: If the request fails for any other reason.
120117
"""
121-
super().__init__(api_version=api_version, temperature=temperature, top_p=top_p, **kwargs)
118+
super().__init__(temperature=temperature, top_p=top_p, **kwargs)
122119
self._max_output_tokens = max_output_tokens
123120

124-
# Validate endpoint URL for OpenAI Response API
125-
self._warn_if_irregular_endpoint(self.RESPONSE_URL_REGEX)
121+
response_url_patterns = [r"/responses"]
122+
self._warn_if_irregular_endpoint(response_url_patterns)
126123

127124
# Reasoning parameters are not yet supported by PyRIT.
128125
# See https://platform.openai.com/docs/api-reference/responses/create#responses-create-reasoning

0 commit comments

Comments
 (0)