Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,69 @@ concurrency:
cancel-in-progress: true

jobs:
openai-compatibility:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
python: ["3.11", "3.14"]
openai: ["2.20.0", "2.54.0", "3.0.0", "3.29.0"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python }}
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.9.17"
enable-cache: true
- name: Install default dependencies without LiteLLM
run: uv sync --frozen
- name: Test the selected OpenAI SDK without LiteLLM
env:
OPENAI_VERSION: ${{ matrix.openai }}
LITELLM_LOCAL_MODEL_COST_MAP: "True"
LITELLM_TELEMETRY: "False"
run: |
uv run --frozen --no-sync --with "openai==$OPENAI_VERSION" python -c '
import importlib.util, os, openai, pytest
assert openai.__version__ == os.environ["OPENAI_VERSION"]
assert importlib.util.find_spec("litellm") is None
raise SystemExit(pytest.main([
"-q",
"tests/unit/prompt_target/target/test_openai_sdk_compatibility.py",
"tests/unit/prompt_target/target/test_openai_url_warnings.py",
"tests/unit/prompt_target/target/test_openai_error_handling.py",
"tests/unit/prompt_target/target/test_openai_target_auth.py",
"tests/unit/embedding/test_azure_text_embedding.py",
"tests/unit/prompt_target/target/test_litellm_chat_target.py",
]))
'
- name: Install LiteLLM compatibility extra
if: startsWith(matrix.openai, '2.')
run: uv sync --frozen --extra litellm
- name: Test LiteLLM with the selected OpenAI SDK
if: startsWith(matrix.openai, '2.')
env:
OPENAI_VERSION: ${{ matrix.openai }}
LITELLM_LOCAL_MODEL_COST_MAP: "True"
LITELLM_TELEMETRY: "False"
run: |
uv run --frozen --no-sync --with "openai==$OPENAI_VERSION" python -c '
import importlib.metadata, os, openai, pytest
from packaging.version import Version
assert openai.__version__ == os.environ["OPENAI_VERSION"]
assert not Version(importlib.metadata.version("litellm")).is_prerelease
raise SystemExit(pytest.main([
"-q",
"tests/unit/prompt_target/target/test_openai_sdk_compatibility.py",
"tests/unit/prompt_target/target/test_litellm_chat_target.py",
"-k", "litellm",
]))
'

pre-commit:
strategy:
fail-fast: true
Expand Down
55 changes: 55 additions & 0 deletions doc/code/targets/0_prompt_targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,61 @@ Here are some examples:
| **A2ATarget** | **No** (multi-turn, but no editable history) | Text-only Agent-to-Agent endpoints through the official a2a-sdk (v0.3 / v1.0). |
| **AzureBlobStorageTarget** | **No** | Used primarily for storage; not for conversation-based AI. |

## OpenAI SDK compatibility

PyRIT supports OpenAI Python SDK 2.20 and later 2.x releases, and SDK 3.x.
The upper bound excludes SDK 4 until it has been evaluated. The compatibility
tests exercise both SDK lines with fabricated provider responses, including
custom HTTP clients, authentication, errors, and binary responses.

SDK 2 uses `httpx` by default; SDK 3 uses `httpx2`. The
`httpx_client_kwargs` parameter keeps its existing name and forwards options to
`AsyncOpenAI`, not to an HTTP client constructor. Numeric timeouts keep working:

```python
target = OpenAIChatTarget(
model_name="my-deployment",
endpoint="https://my-resource.openai.azure.com/openai/v1",
api_key="...",
httpx_client_kwargs={"timeout": 180},
)
```

For a custom transport, proxy, TLS context, or event hook, construct an async
HTTP client and pass it as `httpx_client_kwargs={"http_client": client}`.
An `httpx.AsyncClient` works on both SDK lines using SDK 3's legacy-client
compatibility layer. On SDK 3, `httpx2.AsyncClient` and
`openai.DefaultAsyncHttpxClient` use the native HTTPX2 transport. Keep custom
transport classes, request/response objects, and hooks in the same HTTP family
as the client, and close caller-created clients when finished.

The SDK 3 default transport uses the operating-system TLS trust store rather
than HTTPX's `certifi` bundle. Install corporate or private CA certificates in
that trust store, set `SSL_CERT_FILE` / `SSL_CERT_DIR`, or pass a client with
an explicit TLS context. Catch SDK exceptions such as `openai.APITimeoutError`
and `openai.APIConnectionError`, rather than assuming their underlying cause
is always an HTTPX exception. See the upstream
[HTTPX2 migration guide](https://github.com/openai/openai-python/blob/v3.29.0/httpx2.md).

Use base URLs without the API operation path, for example `/openai/v1` rather
than `/openai/deployments/.../chat/completions`. Pass deployment names through
`model_name` and additional query parameters through
`httpx_client_kwargs={"default_query": {"api-version": "..."}}`.
The SDK owns trailing-slash normalization and appending the operation path.

`OpenAIVideoTarget` retains the deprecated videos API for compatible endpoints.
This does not restore availability of OpenAI's retired Sora service. SDK 3's
deprecation warnings remain visible; type-check exceptions are limited to the
specific SDK calls in that adapter.

The default installation does not require LiteLLM and can resolve to SDK 3.
The optional `litellm` and `all` extras retain stable LiteLLM releases. A stable
LiteLLM release that requires `openai<3` selects SDK 2, which PyRIT also supports;
there is no need to install a development build to use these extras.
The shared `uv.lock` therefore retains SDK 2 to satisfy all extras. CI tests
SDK 2 and 3 without LiteLLM, and stable LiteLLM with SDK 2. Ordinary package
installations resolve the selected extras independently of this development lock.

## A2A agents

Install the optional client with `pip install "pyrit[a2a]"` (also included in
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ dependencies = [
"mcp>=2.2,<3",
"numpy>=1.26.0; python_version < '3.14'",
"numpy>=2.3.0; python_version >= '3.14'",
"openai>=2.2.0",
"openai>=2.20.0,<4",
"openpyxl>=3.1.5",
"opentelemetry-sdk>=1.36,<2",
"pillow>=12.2.0",
Expand Down
5 changes: 3 additions & 2 deletions pyrit/prompt_target/openai/openai_chat_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ def __init__(
extra_body_parameters (dict, Optional): Additional parameters to be included in the request body.
custom_configuration (TargetConfiguration, Optional): Override the default target configuration.
**kwargs: Additional keyword arguments passed to the parent OpenAITarget class.
httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()``
constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}``
httpx_client_kwargs (dict, Optional): Additional ``AsyncOpenAI`` constructor options.
For example, ``{"timeout": 180}`` sets a three-minute timeout. Custom ``http_client``
instances use HTTPX on SDK 2, or HTTPX2 / legacy HTTPX on SDK 3.

Raises:
PyritException: If the temperature or top_p values are out of bounds.
Expand Down
5 changes: 3 additions & 2 deletions pyrit/prompt_target/openai/openai_completion_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ def __init__(
custom_configuration (TargetConfiguration, Optional): Override the default configuration for
this target instance. Defaults to None.
**kwargs: Additional keyword arguments passed to the parent OpenAITarget class.
httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()``
constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}``
httpx_client_kwargs (dict, Optional): Additional ``AsyncOpenAI`` constructor options.
For example, ``{"timeout": 180}`` sets a three-minute timeout. Custom ``http_client``
instances use HTTPX on SDK 2, or HTTPX2 / legacy HTTPX on SDK 3.

Raises:
PyritException: If temperature is not between 0 and 2 (inclusive), or top_p is not
Expand Down
6 changes: 3 additions & 3 deletions pyrit/prompt_target/openai/openai_image_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ def __init__(
custom_configuration (TargetConfiguration, Optional): Override the default configuration for
this target instance. Defaults to None.
**kwargs: Additional keyword arguments to be passed to AzureOpenAITarget.
httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the
`httpx.AsyncClient()` constructor.
For example, to specify a 3 minutes timeout: httpx_client_kwargs={"timeout": 180}
httpx_client_kwargs (dict, Optional): Additional ``AsyncOpenAI`` constructor options.
For example, ``{"timeout": 180}`` sets a three-minute timeout. Custom ``http_client``
instances use HTTPX on SDK 2, or HTTPX2 / legacy HTTPX on SDK 3.

Raises:
ValueError: If background is "transparent" and output_format is "jpeg",
Expand Down
4 changes: 2 additions & 2 deletions pyrit/prompt_target/openai/openai_realtime_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ def __init__(
custom_configuration (TargetConfiguration, Optional): Override the default configuration for
this target instance. Defaults to None.
**kwargs: Additional keyword arguments passed to the parent OpenAITarget class.
httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()``
constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}``
httpx_client_kwargs (dict, Optional): Additional ``AsyncOpenAI`` constructor options
for the HTTP client. WebSocket connections use the Realtime SDK client.
"""
super().__init__(custom_configuration=custom_configuration, **kwargs)

Expand Down
5 changes: 3 additions & 2 deletions pyrit/prompt_target/openai/openai_response_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ def __init__(
custom_configuration (TargetConfiguration, Optional): Override the default configuration for
this target instance. Defaults to None.
**kwargs: Additional keyword arguments passed to the parent OpenAITarget class.
httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()``
constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}``
httpx_client_kwargs (dict, Optional): Additional ``AsyncOpenAI`` constructor options.
For example, ``{"timeout": 180}`` sets a three-minute timeout. Custom ``http_client``
instances use HTTPX on SDK 2, or HTTPX2 / legacy HTTPX on SDK 3.


Raises:
Expand Down
5 changes: 3 additions & 2 deletions pyrit/prompt_target/openai/openai_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ def __init__(
max_requests_per_minute (int, Optional): Number of requests the target can handle per
minute before hitting a rate limit. The number of requests sent to the target
will be capped at the value provided.
httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the
`httpx.AsyncClient()` constructor.
httpx_client_kwargs (dict, Optional): Additional ``AsyncOpenAI`` constructor options,
such as ``timeout`` or ``http_client``. SDK 2 uses HTTPX by default; SDK 3 uses
HTTPX2. Existing HTTPX clients remain supported by SDK 3's compatibility layer.
underlying_model (str, Optional): The underlying model name (e.g., "gpt-4o") used solely for
target identifier purposes. This is useful when the deployment name in Azure differs
from the actual model. If not provided, the identifier will use the model_name.
Expand Down
5 changes: 3 additions & 2 deletions pyrit/prompt_target/openai/openai_tts_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ def __init__(
custom_configuration (TargetConfiguration, Optional): Override the default configuration for
this target instance. Defaults to None.
**kwargs: Additional keyword arguments passed to the parent OpenAITarget class.
httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()``
constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}``
httpx_client_kwargs (dict, Optional): Additional ``AsyncOpenAI`` constructor options.
For example, ``{"timeout": 180}`` sets a three-minute timeout. Custom ``http_client``
instances use HTTPX on SDK 2, or HTTPX2 / legacy HTTPX on SDK 3.
"""
super().__init__(custom_configuration=custom_configuration, **kwargs)

Expand Down
19 changes: 11 additions & 8 deletions pyrit/prompt_target/openai/openai_video_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class OpenAIVideoTarget(OpenAITarget):
"""
OpenAI Video Target using the OpenAI SDK for video generation.

Supports Sora-2 and Sora-2-Pro models via the OpenAI videos API.
Retains the videos API for compatible providers. OpenAI deprecated its Sora API;
availability on Azure and other providers is determined by the endpoint.

Supports three modes:
- Text-to-video: Generate video from a text prompt
Expand Down Expand Up @@ -91,8 +92,9 @@ def __init__(
custom_configuration (TargetConfiguration, Optional): Override the default configuration for
this target instance. Defaults to None.
**kwargs: Additional keyword arguments passed to the parent OpenAITarget class.
httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()``
constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}``
httpx_client_kwargs (dict, Optional): Additional ``AsyncOpenAI`` constructor options.
For example, ``{"timeout": 180}`` sets a three-minute timeout. Custom ``http_client``
instances use HTTPX on SDK 2, or HTTPX2 / legacy HTTPX on SDK 3.

Remix workflow:
To remix an existing video, set ``prompt_metadata={"video_id": "<id>"}`` on the text
Expand Down Expand Up @@ -261,7 +263,8 @@ async def _send_text_plus_image_to_video_async(
logger.info("Text+Image-to-video mode: Using image as first frame")
input_file = await self._prepare_image_input_async(image_piece=image_piece)
return await self._handle_openai_request_async(
api_call=lambda: self._client.videos.create_and_poll(
# SDK 3 deprecates Sora, but compatible endpoints may still implement this API.
api_call=lambda: self._client.videos.create_and_poll( # type: ignore[ty:deprecated]
model=self._model_name,
prompt=prompt,
size=self._size,
Expand All @@ -283,7 +286,7 @@ async def _send_text_to_video_async(self, *, prompt: str, request: Message) -> M
The response Message with the generated video path.
"""
return await self._handle_openai_request_async(
api_call=lambda: self._client.videos.create_and_poll(
api_call=lambda: self._client.videos.create_and_poll( # type: ignore[ty:deprecated]
model=self._model_name,
prompt=prompt,
size=self._size,
Expand Down Expand Up @@ -337,11 +340,11 @@ async def _remix_and_poll_async(self, *, video_id: str, prompt: str) -> Any:
Returns:
The completed Video object from the OpenAI SDK.
"""
video = await self._client.videos.remix(video_id, prompt=prompt)
video = await self._client.videos.remix(video_id, prompt=prompt) # type: ignore[ty:deprecated]

# Poll until completion if not already done
if video.status not in ["completed", "failed"]:
video = await self._client.videos.poll(video.id)
video = await self._client.videos.poll(video.id) # type: ignore[ty:deprecated]

return video

Expand Down Expand Up @@ -391,7 +394,7 @@ async def _construct_message_from_response_async(self, response: Any, request: A
logger.info(f"Video was remixed from: {video.remixed_from_video_id}")

# Download video content using SDK
video_response = await self._client.videos.download_content(video.id)
video_response = await self._client.videos.download_content(video.id) # type: ignore[ty:deprecated]
# Extract bytes from HttpxBinaryResponseContent
video_content = video_response.content

Expand Down
Loading
Loading