Skip to content

Commit 19f1f37

Browse files
feat(api): add safety warning and deactivation webhook events (openai#3908)
## Summary Adds typed webhook payloads for safety warnings and deactivations. ## Changes - Parse safety.warning_issued and safety.deactivation_issued through the existing webhook unwrap methods. - Expose each event's safety case ID in data.id.
1 parent c377eb2 commit 19f1f37

8 files changed

Lines changed: 276 additions & 7 deletions

File tree

‎.castiron.stats.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
schema_version: 1
2-
generation_id: 4e2c0a68-f552-4057-9398-0656631a643d
3-
openapi_spec_hash: b81e20e8186e5f45afc36ecb4ceb3b33
4-
openapi_transformed_spec_hash: 73682ea454f2f3c2bde8466c223eb434
2+
generation_id: df936b0c-3a74-4407-aff0-23ee0aa5a051
3+
openapi_spec_hash: 83d739d1f34f3c6641c60fb43f882cb9
4+
openapi_transformed_spec_hash: 11ef1888efd4aab54eed55a63ac4c9cd
55
config_hash: ba291299a0f8e738664099a7d7741f24
6-
codegen_sha: 91a42dc9d1a7ce0aae0e5202636b0e6470190813
7-
codegen_hash: ee7a457167dd360a7d59f207be427c7c12db2bae58ac9967fbf8a46b2757cf9a
8-
public_codegen_sha: 367700a19c7975b8e616abcd58e1b7c1a574b106
6+
codegen_sha: c5827dc664f987f9b5c708746cb2f5365c3d5755
7+
codegen_hash: 76e577a907d602c7d7f45ba7ecb9b19db4db064f8196f59ec372b7d496ae5f67
8+
public_codegen_sha: 842234b163ff84a841598aba8ac8569f69204051

‎api_reference/openapi.transformed.yml‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30653,6 +30653,20 @@ webhooks:
3065330653
responses:
3065430654
'200':
3065530655
description: Return any 2xx status code to acknowledge receipt. A 410 Gone response also stops retries; other non-2xx responses are retried.
30656+
safety_deactivation_issued:
30657+
post:
30658+
description: |
30659+
Sent when a deactivation is issued for a safety identifier in your organization.
30660+
Retrieve the case details with `GET /v1/safety/cases/{id}` using `data.id`.
30661+
requestBody:
30662+
description: The event payload sent by the API.
30663+
content:
30664+
application/json:
30665+
schema:
30666+
$ref: '#/components/schemas/WebhookSafetyDeactivationIssued'
30667+
responses:
30668+
'200':
30669+
description: Return any 2xx status code to acknowledge receipt. A 410 Gone response also stops retries; other non-2xx responses are retried.
3065630670
safety_org_alert_created:
3065730671
post:
3065830672
description: |
@@ -30669,6 +30683,20 @@ webhooks:
3066930683
responses:
3067030684
'200':
3067130685
description: Return any 2xx status code to acknowledge receipt. A 410 Gone response also stops retries; other non-2xx responses are retried.
30686+
safety_warning_issued:
30687+
post:
30688+
description: |
30689+
Sent when a warning is issued for a safety identifier in your organization.
30690+
Retrieve the case details with `GET /v1/safety/cases/{id}` using `data.id`.
30691+
requestBody:
30692+
description: The event payload sent by the API.
30693+
content:
30694+
application/json:
30695+
schema:
30696+
$ref: '#/components/schemas/WebhookSafetyWarningIssued'
30697+
responses:
30698+
'200':
30699+
description: Return any 2xx status code to acknowledge receipt. A 410 Gone response also stops retries; other non-2xx responses are retried.
3067230700
components:
3067330701
schemas:
3067430702
AddUploadPartRequest:
@@ -66166,6 +66194,56 @@ components:
6616666194
"type": "safety.alert.created",
6616766195
"data": {"id": "alert_0123456789abcdef0123456789abcdef"}
6616866196
}
66197+
WebhookSafetyDeactivationIssued:
66198+
type: object
66199+
title: safety.deactivation_issued
66200+
description: Sent when a deactivation is issued for a safety identifier in your organization.
66201+
required:
66202+
- id
66203+
- object
66204+
- created_at
66205+
- type
66206+
- data
66207+
properties:
66208+
id:
66209+
type: string
66210+
description: The unique ID of the webhook event.
66211+
object:
66212+
type: string
66213+
enum:
66214+
- event
66215+
x-stainless-const: true
66216+
description: Always `event`.
66217+
created_at:
66218+
type: integer
66219+
format: unixtime
66220+
description: The Unix timestamp in seconds when the event was created.
66221+
type:
66222+
type: string
66223+
enum:
66224+
- safety.deactivation_issued
66225+
x-stainless-const: true
66226+
description: Always `safety.deactivation_issued`.
66227+
data:
66228+
type: object
66229+
additionalProperties: false
66230+
required:
66231+
- id
66232+
properties:
66233+
id:
66234+
type: string
66235+
description: The safety case ID to pass to `GET /v1/safety/cases/{id}`.
66236+
x-oaiMeta:
66237+
name: safety.deactivation_issued
66238+
group: webhook-events
66239+
example: |
66240+
{
66241+
"id": "evt_123",
66242+
"object": "event",
66243+
"created_at": 1787659200,
66244+
"type": "safety.deactivation_issued",
66245+
"data": {"id": "C-abc123"}
66246+
}
6616966247
WebhookSafetyOrgAlertCreated:
6617066248
type: object
6617166249
title: safety.org_alert.created
@@ -66217,6 +66295,56 @@ components:
6621766295
"type": "safety.org_alert.created",
6621866296
"data": {"id": "alert_0123456789abcdef0123456789abcdef"}
6621966297
}
66298+
WebhookSafetyWarningIssued:
66299+
type: object
66300+
title: safety.warning_issued
66301+
description: Sent when a warning is issued for a safety identifier in your organization.
66302+
required:
66303+
- id
66304+
- object
66305+
- created_at
66306+
- type
66307+
- data
66308+
properties:
66309+
id:
66310+
type: string
66311+
description: The unique ID of the webhook event.
66312+
object:
66313+
type: string
66314+
enum:
66315+
- event
66316+
x-stainless-const: true
66317+
description: Always `event`.
66318+
created_at:
66319+
type: integer
66320+
format: unixtime
66321+
description: The Unix timestamp in seconds when the event was created.
66322+
type:
66323+
type: string
66324+
enum:
66325+
- safety.warning_issued
66326+
x-stainless-const: true
66327+
description: Always `safety.warning_issued`.
66328+
data:
66329+
type: object
66330+
additionalProperties: false
66331+
required:
66332+
- id
66333+
properties:
66334+
id:
66335+
type: string
66336+
description: The safety case ID to pass to `GET /v1/safety/cases/{id}`.
66337+
x-oaiMeta:
66338+
name: safety.warning_issued
66339+
group: webhook-events
66340+
example: |
66341+
{
66342+
"id": "evt_123",
66343+
"object": "event",
66344+
"created_at": 1787659200,
66345+
"type": "safety.warning_issued",
66346+
"data": {"id": "C-abc123"}
66347+
}
6622066348
ModerationInputType:
6622166349
type: string
6622266350
enum:
@@ -96519,6 +96647,12 @@ x-oaiMeta:
9651996647
- type: object
9652096648
key: WebhookLiveTransportIncoming
9652196649
path: <auto>
96650+
- type: object
96651+
key: WebhookSafetyWarningIssued
96652+
path: <auto>
96653+
- type: object
96654+
key: WebhookSafetyDeactivationIssued
96655+
path: <auto>
9652296656
- type: object
9652396657
key: WebhookSafetyAlertCreated
9652496658
path: <auto>

‎src/openai/resources/webhooks/api.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ from openai.types.webhooks import (
2323
ResponseFailedWebhookEvent,
2424
ResponseIncompleteWebhookEvent,
2525
SafetyAlertCreatedWebhookEvent,
26+
SafetyDeactivationIssuedWebhookEvent,
2627
SafetyOrgAlertCreatedWebhookEvent,
28+
SafetyWarningIssuedWebhookEvent,
2729
UnwrapWebhookEvent,
2830
WebhookEndpoint,
2931
WebhookEndpointList,

‎src/openai/types/webhooks/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from .response_completed_webhook_event import ResponseCompletedWebhookEvent as ResponseCompletedWebhookEvent
3030
from .response_incomplete_webhook_event import ResponseIncompleteWebhookEvent as ResponseIncompleteWebhookEvent
3131
from .safety_alert_created_webhook_event import SafetyAlertCreatedWebhookEvent as SafetyAlertCreatedWebhookEvent
32+
from .safety_warning_issued_webhook_event import SafetyWarningIssuedWebhookEvent as SafetyWarningIssuedWebhookEvent
3233
from .fine_tuning_job_failed_webhook_event import FineTuningJobFailedWebhookEvent as FineTuningJobFailedWebhookEvent
3334
from .realtime_call_incoming_webhook_event import RealtimeCallIncomingWebhookEvent as RealtimeCallIncomingWebhookEvent
3435
from .live_transport_incoming_webhook_event import (
@@ -46,3 +47,6 @@
4647
from .safety_identifier_blocked_webhook_event import (
4748
SafetyIdentifierBlockedWebhookEvent as SafetyIdentifierBlockedWebhookEvent,
4849
)
50+
from .safety_deactivation_issued_webhook_event import (
51+
SafetyDeactivationIssuedWebhookEvent as SafetyDeactivationIssuedWebhookEvent,
52+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
2+
3+
from typing_extensions import Literal
4+
5+
from ..._models import BaseModel
6+
7+
__all__ = ["SafetyDeactivationIssuedWebhookEvent", "Data"]
8+
9+
10+
class Data(BaseModel):
11+
id: str
12+
"""The safety case ID to pass to `GET /v1/safety/cases/{id}`."""
13+
14+
15+
class SafetyDeactivationIssuedWebhookEvent(BaseModel):
16+
"""
17+
Sent when a deactivation is issued for a safety identifier in your organization.
18+
"""
19+
20+
id: str
21+
"""The unique ID of the webhook event."""
22+
23+
created_at: int
24+
"""The Unix timestamp in seconds when the event was created."""
25+
26+
data: Data
27+
28+
object: Literal["event"]
29+
"""Always `event`."""
30+
31+
type: Literal["safety.deactivation_issued"]
32+
"""Always `safety.deactivation_issued`."""
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
2+
3+
from typing_extensions import Literal
4+
5+
from ..._models import BaseModel
6+
7+
__all__ = ["SafetyWarningIssuedWebhookEvent", "Data"]
8+
9+
10+
class Data(BaseModel):
11+
id: str
12+
"""The safety case ID to pass to `GET /v1/safety/cases/{id}`."""
13+
14+
15+
class SafetyWarningIssuedWebhookEvent(BaseModel):
16+
"""Sent when a warning is issued for a safety identifier in your organization."""
17+
18+
id: str
19+
"""The unique ID of the webhook event."""
20+
21+
created_at: int
22+
"""The Unix timestamp in seconds when the event was created."""
23+
24+
data: Data
25+
26+
object: Literal["event"]
27+
"""Always `event`."""
28+
29+
type: Literal["safety.warning_issued"]
30+
"""Always `safety.warning_issued`."""

‎src/openai/types/webhooks/unwrap_webhook_event.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
from .response_completed_webhook_event import ResponseCompletedWebhookEvent
1818
from .response_incomplete_webhook_event import ResponseIncompleteWebhookEvent
1919
from .safety_alert_created_webhook_event import SafetyAlertCreatedWebhookEvent
20+
from .safety_warning_issued_webhook_event import SafetyWarningIssuedWebhookEvent
2021
from .fine_tuning_job_failed_webhook_event import FineTuningJobFailedWebhookEvent
2122
from .realtime_call_incoming_webhook_event import RealtimeCallIncomingWebhookEvent
2223
from .live_transport_incoming_webhook_event import LiveTransportIncomingWebhookEvent
2324
from .safety_org_alert_created_webhook_event import SafetyOrgAlertCreatedWebhookEvent
2425
from .fine_tuning_job_cancelled_webhook_event import FineTuningJobCancelledWebhookEvent
2526
from .fine_tuning_job_succeeded_webhook_event import FineTuningJobSucceededWebhookEvent
27+
from .safety_deactivation_issued_webhook_event import SafetyDeactivationIssuedWebhookEvent
2628

2729
__all__ = ["UnwrapWebhookEvent"]
2830

@@ -46,7 +48,9 @@
4648
ResponseFailedWebhookEvent,
4749
ResponseIncompleteWebhookEvent,
4850
SafetyAlertCreatedWebhookEvent,
51+
SafetyDeactivationIssuedWebhookEvent,
4952
SafetyOrgAlertCreatedWebhookEvent,
53+
SafetyWarningIssuedWebhookEvent,
5054
],
5155
PropertyInfo(discriminator="type"),
5256
]

‎tests/lib/test_webhooks.py‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010

1111
import openai
1212
from openai._exceptions import InvalidWebhookSignatureError
13-
from openai.types.webhooks import UnwrapWebhookEvent
13+
from openai.types.webhooks import (
14+
UnwrapWebhookEvent,
15+
SafetyWarningIssuedWebhookEvent,
16+
SafetyDeactivationIssuedWebhookEvent,
17+
)
1418

1519
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
1620

@@ -22,6 +26,10 @@
2226
TEST_SIGNATURE = "v1,gUAg4R2hWouRZqRQG4uJypNS8YK885G838+EHb4nKBY="
2327
SIP_EVENT_TYPES = ["live.call.incoming", "live.transport.incoming", "realtime.call.incoming"]
2428
SIP_MEDIA_SECURITY_VALUES = [None, "rtp", "srtp", "future_media_security"]
29+
SAFETY_EVENT_TYPES = {
30+
"safety.warning_issued": SafetyWarningIssuedWebhookEvent,
31+
"safety.deactivation_issued": SafetyDeactivationIssuedWebhookEvent,
32+
}
2533

2634

2735
def create_test_headers(
@@ -66,9 +74,49 @@ def assert_sip_event(event: UnwrapWebhookEvent, event_type: str, media_security:
6674
assert event.to_dict() == json.loads(payload)
6775

6876

77+
def create_safety_test_payload(event_type: str) -> tuple[str, dict[str, str]]:
78+
payload = json.dumps(
79+
{
80+
"id": "evt_safety_test",
81+
"object": "event",
82+
"created_at": TEST_TIMESTAMP,
83+
"type": event_type,
84+
"data": {"id": "case_test"},
85+
}
86+
)
87+
signed_payload = f"{TEST_WEBHOOK_ID}.{TEST_TIMESTAMP}.{payload}".encode()
88+
signature = base64.b64encode(
89+
hmac.new(base64.b64decode(TEST_SECRET.removeprefix("whsec_")), signed_payload, "sha256").digest()
90+
).decode()
91+
return payload, create_test_headers(signature=f"v1,{signature}")
92+
93+
94+
def assert_safety_event(event: UnwrapWebhookEvent, event_type: str, payload: str) -> None:
95+
assert isinstance(event, SAFETY_EVENT_TYPES[event_type])
96+
assert event.type == "safety.warning_issued" or event.type == "safety.deactivation_issued"
97+
assert event.type == event_type
98+
assert event.data.id == "case_test"
99+
assert event.to_dict() == json.loads(payload)
100+
101+
69102
class TestWebhooks:
70103
parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
71104

105+
@mock.patch("time.time", mock.MagicMock(return_value=TEST_TIMESTAMP))
106+
@parametrize
107+
@pytest.mark.parametrize("event_type", SAFETY_EVENT_TYPES)
108+
def test_unwrap_safety_events(self, client: openai.OpenAI, event_type: str) -> None:
109+
payload, headers = create_safety_test_payload(event_type)
110+
event = client.webhooks.unwrap(payload, headers, secret=TEST_SECRET)
111+
assert_safety_event(event, event_type, payload)
112+
113+
for tampered_payload in [payload.replace("case_test", "case_tampered"), "{"]:
114+
with pytest.raises(InvalidWebhookSignatureError, match="The given webhook signature does not match"):
115+
client.webhooks.unwrap(tampered_payload, headers, secret=TEST_SECRET)
116+
117+
with pytest.raises(InvalidWebhookSignatureError, match="The given webhook signature does not match"):
118+
client.webhooks.unwrap(payload, headers, secret="wrong_safety_webhook_secret")
119+
72120
@mock.patch("time.time", mock.MagicMock(return_value=TEST_TIMESTAMP))
73121
@parametrize
74122
@pytest.mark.parametrize("event_type", SIP_EVENT_TYPES)
@@ -220,6 +268,21 @@ class TestAsyncWebhooks:
220268
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
221269
)
222270

271+
@mock.patch("time.time", mock.MagicMock(return_value=TEST_TIMESTAMP))
272+
@parametrize
273+
@pytest.mark.parametrize("event_type", SAFETY_EVENT_TYPES)
274+
async def test_unwrap_safety_events(self, async_client: openai.AsyncOpenAI, event_type: str) -> None:
275+
payload, headers = create_safety_test_payload(event_type)
276+
event = async_client.webhooks.unwrap(payload, headers, secret=TEST_SECRET)
277+
assert_safety_event(event, event_type, payload)
278+
279+
for tampered_payload in [payload.replace("case_test", "case_tampered"), "{"]:
280+
with pytest.raises(InvalidWebhookSignatureError, match="The given webhook signature does not match"):
281+
async_client.webhooks.unwrap(tampered_payload, headers, secret=TEST_SECRET)
282+
283+
with pytest.raises(InvalidWebhookSignatureError, match="The given webhook signature does not match"):
284+
async_client.webhooks.unwrap(payload, headers, secret="wrong_safety_webhook_secret")
285+
223286
@mock.patch("time.time", mock.MagicMock(return_value=TEST_TIMESTAMP))
224287
@parametrize
225288
@pytest.mark.parametrize("event_type", SIP_EVENT_TYPES)

0 commit comments

Comments
 (0)