From 97d036d1a0ac17048bbf574eb41362005a05b407 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:39:57 +0000 Subject: [PATCH 1/3] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 24895ee467..d9b63f0de9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 271 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai/openai-356010b9b9fd6228b457b8fcfa376cf4928a8f3bd4728e7ba5e4b6b5ef4f5843.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai/openai-39637fc9365e6f80f12424e7c87179553f157b96b47c599b96d5a02f6966d3da.yml openapi_spec_hash: 885864ae98a443166f585f856c464fb2 -config_hash: 1f1e3b4050e2cb4bc780ce0b70e2b3e6 +config_hash: b40012963b4619be8229fcca0105b627 From f589d04bf9f377ecb1f54335ab3ab9d825b5dfee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:58:51 +0000 Subject: [PATCH 2/3] feat(api): add owner_project_access to APIKeyListParams --- .stats.yml | 4 +- .../admin/organization/projects/api_keys.py | 18 ++++++++ src/openai/resources/webhooks/api.md | 1 - .../projects/api_key_list_params.py | 11 ++++- .../organization/projects/project_api_key.py | 3 ++ .../beta/beta_response_reasoning_item.py | 7 +-- .../beta_response_reasoning_item_param.py | 7 +-- .../responses/response_reasoning_item.py | 7 +-- .../response_reasoning_item_param.py | 7 +-- src/openai/types/webhooks/__init__.py | 3 -- ...safety_identifier_blocked_webhook_event.py | 46 ------------------- .../types/webhooks/unwrap_webhook_event.py | 2 - .../organization/projects/test_api_keys.py | 2 + 13 files changed, 51 insertions(+), 67 deletions(-) delete mode 100644 src/openai/types/webhooks/safety_identifier_blocked_webhook_event.py diff --git a/.stats.yml b/.stats.yml index d9b63f0de9..b0a41f8128 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 271 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai/openai-39637fc9365e6f80f12424e7c87179553f157b96b47c599b96d5a02f6966d3da.yml -openapi_spec_hash: 885864ae98a443166f585f856c464fb2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai/openai-ae4c1a92306e31f05678c3de72efbf1903b5c09924dacd42a555cc5ccc0e5eb1.yml +openapi_spec_hash: 0973932f9a3f0b46a0933b143878f9fe config_hash: b40012963b4619be8229fcca0105b627 diff --git a/src/openai/resources/admin/organization/projects/api_keys.py b/src/openai/resources/admin/organization/projects/api_keys.py index 1517d213e0..407bb6b485 100644 --- a/src/openai/resources/admin/organization/projects/api_keys.py +++ b/src/openai/resources/admin/organization/projects/api_keys.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing_extensions import Literal + import httpx from ..... import _legacy_response @@ -89,6 +91,7 @@ def list( *, after: str | Omit = omit, limit: int | Omit = omit, + owner_project_access: Literal["active", "inactive", "any"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -108,6 +111,12 @@ def list( limit: A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + owner_project_access: Filter API keys by whether the owner currently has effective access to the + project. Use `active` for owners with access, `inactive` for owners without + access, or `any` for all enabled project API keys. If omitted, the endpoint + applies its existing membership-based visibility rules, which may exclude some + enabled keys. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -130,6 +139,7 @@ def list( { "after": after, "limit": limit, + "owner_project_access": owner_project_access, }, api_key_list_params.APIKeyListParams, ), @@ -256,6 +266,7 @@ def list( *, after: str | Omit = omit, limit: int | Omit = omit, + owner_project_access: Literal["active", "inactive", "any"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -275,6 +286,12 @@ def list( limit: A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + owner_project_access: Filter API keys by whether the owner currently has effective access to the + project. Use `active` for owners with access, `inactive` for owners without + access, or `any` for all enabled project API keys. If omitted, the endpoint + applies its existing membership-based visibility rules, which may exclude some + enabled keys. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -297,6 +314,7 @@ def list( { "after": after, "limit": limit, + "owner_project_access": owner_project_access, }, api_key_list_params.APIKeyListParams, ), diff --git a/src/openai/resources/webhooks/api.md b/src/openai/resources/webhooks/api.md index 8efcc3e33e..8e3c312eb0 100644 --- a/src/openai/resources/webhooks/api.md +++ b/src/openai/resources/webhooks/api.md @@ -19,7 +19,6 @@ from openai.types.webhooks import ( ResponseCompletedWebhookEvent, ResponseFailedWebhookEvent, ResponseIncompleteWebhookEvent, - SafetyIdentifierBlockedWebhookEvent, UnwrapWebhookEvent, ) ``` diff --git a/src/openai/types/admin/organization/projects/api_key_list_params.py b/src/openai/types/admin/organization/projects/api_key_list_params.py index 422a28518e..b83f655a1b 100644 --- a/src/openai/types/admin/organization/projects/api_key_list_params.py +++ b/src/openai/types/admin/organization/projects/api_key_list_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Literal, TypedDict __all__ = ["APIKeyListParams"] @@ -22,3 +22,12 @@ class APIKeyListParams(TypedDict, total=False): Limit can range between 1 and 100, and the default is 20. """ + + owner_project_access: Literal["active", "inactive", "any"] + """ + Filter API keys by whether the owner currently has effective access to the + project. Use `active` for owners with access, `inactive` for owners without + access, or `any` for all enabled project API keys. If omitted, the endpoint + applies its existing membership-based visibility rules, which may exclude some + enabled keys. + """ diff --git a/src/openai/types/admin/organization/projects/project_api_key.py b/src/openai/types/admin/organization/projects/project_api_key.py index 7e5e6949eb..ba08335b39 100644 --- a/src/openai/types/admin/organization/projects/project_api_key.py +++ b/src/openai/types/admin/organization/projects/project_api_key.py @@ -74,5 +74,8 @@ class ProjectAPIKey(BaseModel): owner: Owner + owner_project_access: Literal["active", "inactive"] + """Whether the API key's owner currently has effective access to the project.""" + redacted_value: str """The redacted value of the API key""" diff --git a/src/openai/types/beta/beta_response_reasoning_item.py b/src/openai/types/beta/beta_response_reasoning_item.py index 57bed79f63..14d3c5abe7 100644 --- a/src/openai/types/beta/beta_response_reasoning_item.py +++ b/src/openai/types/beta/beta_response_reasoning_item.py @@ -59,9 +59,10 @@ class BetaResponseReasoningItem(BaseModel): """Reasoning text content.""" encrypted_content: Optional[str] = None - """ - The encrypted content of the reasoning item - populated when a response is - generated with `reasoning.encrypted_content` in the `include` parameter. + """The encrypted content of the reasoning item. + + This is populated by default for reasoning items returned by + `POST /v1/responses` and WebSocket `response.create` requests. """ status: Optional[Literal["in_progress", "completed", "incomplete"]] = None diff --git a/src/openai/types/beta/beta_response_reasoning_item_param.py b/src/openai/types/beta/beta_response_reasoning_item_param.py index 8a91128af8..608a31189f 100644 --- a/src/openai/types/beta/beta_response_reasoning_item_param.py +++ b/src/openai/types/beta/beta_response_reasoning_item_param.py @@ -59,9 +59,10 @@ class BetaResponseReasoningItemParam(TypedDict, total=False): """Reasoning text content.""" encrypted_content: Optional[str] - """ - The encrypted content of the reasoning item - populated when a response is - generated with `reasoning.encrypted_content` in the `include` parameter. + """The encrypted content of the reasoning item. + + This is populated by default for reasoning items returned by + `POST /v1/responses` and WebSocket `response.create` requests. """ status: Literal["in_progress", "completed", "incomplete"] diff --git a/src/openai/types/responses/response_reasoning_item.py b/src/openai/types/responses/response_reasoning_item.py index 1a22eb60cc..7f24ef2c57 100644 --- a/src/openai/types/responses/response_reasoning_item.py +++ b/src/openai/types/responses/response_reasoning_item.py @@ -49,9 +49,10 @@ class ResponseReasoningItem(BaseModel): """Reasoning text content.""" encrypted_content: Optional[str] = None - """ - The encrypted content of the reasoning item - populated when a response is - generated with `reasoning.encrypted_content` in the `include` parameter. + """The encrypted content of the reasoning item. + + This is populated by default for reasoning items returned by + `POST /v1/responses` and WebSocket `response.create` requests. """ status: Optional[Literal["in_progress", "completed", "incomplete"]] = None diff --git a/src/openai/types/responses/response_reasoning_item_param.py b/src/openai/types/responses/response_reasoning_item_param.py index 40320b72e1..e4cbc884e3 100644 --- a/src/openai/types/responses/response_reasoning_item_param.py +++ b/src/openai/types/responses/response_reasoning_item_param.py @@ -49,9 +49,10 @@ class ResponseReasoningItemParam(TypedDict, total=False): """Reasoning text content.""" encrypted_content: Optional[str] - """ - The encrypted content of the reasoning item - populated when a response is - generated with `reasoning.encrypted_content` in the `include` parameter. + """The encrypted content of the reasoning item. + + This is populated by default for reasoning items returned by + `POST /v1/responses` and WebSocket `response.create` requests. """ status: Literal["in_progress", "completed", "incomplete"] diff --git a/src/openai/types/webhooks/__init__.py b/src/openai/types/webhooks/__init__.py index dcb4f3dec4..8b9e55653b 100644 --- a/src/openai/types/webhooks/__init__.py +++ b/src/openai/types/webhooks/__init__.py @@ -22,6 +22,3 @@ from .fine_tuning_job_succeeded_webhook_event import ( FineTuningJobSucceededWebhookEvent as FineTuningJobSucceededWebhookEvent, ) -from .safety_identifier_blocked_webhook_event import ( - SafetyIdentifierBlockedWebhookEvent as SafetyIdentifierBlockedWebhookEvent, -) diff --git a/src/openai/types/webhooks/safety_identifier_blocked_webhook_event.py b/src/openai/types/webhooks/safety_identifier_blocked_webhook_event.py deleted file mode 100644 index 04472727ac..0000000000 --- a/src/openai/types/webhooks/safety_identifier_blocked_webhook_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from typing_extensions import Literal - -from ..._models import BaseModel - -__all__ = ["SafetyIdentifierBlockedWebhookEvent", "Data"] - - -class Data(BaseModel): - """Event data payload.""" - - safety_category: str - """The safety category that triggered the block, such as `bio` or `cyber`.""" - - safety_identifier: str - """The stable safety identifier associated with the blocked request.""" - - model: Optional[str] = None - """The model used for the blocked request, if available.""" - - project_id: Optional[str] = None - """The project associated with the blocked request, if available.""" - - request_id: Optional[str] = None - """The OpenAI request ID for the blocked request, if available.""" - - -class SafetyIdentifierBlockedWebhookEvent(BaseModel): - """Sent when a request associated with a safety identifier has been blocked.""" - - id: str - """The unique ID of the event.""" - - created_at: int - """The Unix timestamp (in seconds) of when the request was blocked.""" - - data: Data - """Event data payload.""" - - type: Literal["safety_identifier.blocked"] - """The type of the event. Always `safety_identifier.blocked`.""" - - object: Optional[Literal["event"]] = None - """The object of the event. Always `event`.""" diff --git a/src/openai/types/webhooks/unwrap_webhook_event.py b/src/openai/types/webhooks/unwrap_webhook_event.py index 20b77c493f..952383c049 100644 --- a/src/openai/types/webhooks/unwrap_webhook_event.py +++ b/src/openai/types/webhooks/unwrap_webhook_event.py @@ -19,7 +19,6 @@ from .realtime_call_incoming_webhook_event import RealtimeCallIncomingWebhookEvent from .fine_tuning_job_cancelled_webhook_event import FineTuningJobCancelledWebhookEvent from .fine_tuning_job_succeeded_webhook_event import FineTuningJobSucceededWebhookEvent -from .safety_identifier_blocked_webhook_event import SafetyIdentifierBlockedWebhookEvent __all__ = ["UnwrapWebhookEvent"] @@ -40,7 +39,6 @@ ResponseCompletedWebhookEvent, ResponseFailedWebhookEvent, ResponseIncompleteWebhookEvent, - SafetyIdentifierBlockedWebhookEvent, ], PropertyInfo(discriminator="type"), ] diff --git a/tests/api_resources/admin/organization/projects/test_api_keys.py b/tests/api_resources/admin/organization/projects/test_api_keys.py index f9aef44216..b2717c59d2 100644 --- a/tests/api_resources/admin/organization/projects/test_api_keys.py +++ b/tests/api_resources/admin/organization/projects/test_api_keys.py @@ -79,6 +79,7 @@ def test_method_list_with_all_params(self, client: OpenAI) -> None: project_id="project_id", after="after", limit=0, + owner_project_access="active", ) assert_matches_type(SyncConversationCursorPage[ProjectAPIKey], api_key, path=["response"]) @@ -228,6 +229,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncOpenAI) -> N project_id="project_id", after="after", limit=0, + owner_project_access="active", ) assert_matches_type(AsyncConversationCursorPage[ProjectAPIKey], api_key, path=["response"]) From c11f3c9239b7558d6288d133dc5485a5b3888717 Mon Sep 17 00:00:00 2001 From: Pranav Mishra Date: Tue, 14 Jul 2026 12:15:21 -0700 Subject: [PATCH 3/3] fix(lib): preserve custom tool calls in parse_chat_completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `parse_chat_completion` (behind `client.chat.completions.parse()` and the streaming `get_final_completion()`) logged a warning and then *dropped* every `custom`-type tool call, so a supported GPT-5 tool call the model made vanished from the parsed result (`tool_calls` could even come back `None`). The handling was also inconsistent: the trailing `else` branch already preserves any non-function tool call by appending it unchanged; only `custom` was special-cased to discard. Append the custom call the same way (it has no schema to parse `parsed_arguments` against, so it's surfaced as-is) instead of dropping it, and remove the now-inaccurate "Ignoring tool call" warning that fired on every custom call in normal use. Adds tests/lib/chat/test_parse_custom_tool_calls.py covering a custom-only and a mixed function+custom message; both fail before this change. Note: `ParsedChatCompletionMessage.tool_calls` is a generated type narrowed to `list[ParsedFunctionToolCall]`, so a custom call round-trips through attribute access and its own `model_dump()` but its `custom` payload is still dropped when the whole completion is serialized — same limitation the existing `else` branch already has for non-function calls. Fully fixing serialization needs the generated type widened, which is out of scope for a lib-only change. Co-Authored-By: Claude Opus 4.8 --- src/openai/lib/_parsing/_completions.py | 14 ++-- .../lib/chat/test_parse_custom_tool_calls.py | 71 +++++++++++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 tests/lib/chat/test_parse_custom_tool_calls.py diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index 7a1bded1de..45428ec4e4 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -124,13 +124,13 @@ def parse_chat_completion( ) ) elif tool_call.type == "custom": - # warn user that custom tool calls are not callable here - log.warning( - "Custom tool calls are not callable. Ignoring tool call: %s - %s", - tool_call.id, - tool_call.custom.name, - stacklevel=2, - ) + # `.parse()` doesn't attach `parsed_arguments` to custom tool calls + # (there's no schema to parse their free-form input against), but the + # call must still be surfaced rather than dropped — the raw completion + # includes it and callers rely on `tool_calls` reflecting every call + # the model made. This mirrors the `else` branch below, which already + # preserves any non-function tool call unchanged. + tool_calls.append(tool_call) elif TYPE_CHECKING: # type: ignore[unreachable] assert_never(tool_call) else: diff --git a/tests/lib/chat/test_parse_custom_tool_calls.py b/tests/lib/chat/test_parse_custom_tool_calls.py new file mode 100644 index 0000000000..007e53c636 --- /dev/null +++ b/tests/lib/chat/test_parse_custom_tool_calls.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from typing import Any, Dict, List, cast + +from openai._types import omit +from openai.types.chat import ChatCompletion +from openai.lib._parsing import parse_chat_completion + +_FUNCTION_CALL: Dict[str, Any] = { + "id": "call_fn", + "type": "function", + "function": {"name": "get_weather", "arguments": "{}"}, +} +_CUSTOM_CALL: Dict[str, Any] = { + "id": "call_custom", + "type": "custom", + "custom": {"name": "run_python", "input": "print(1)"}, +} + + +def _completion_with_tool_calls(tool_calls: List[Dict[str, Any]]) -> ChatCompletion: + return ChatCompletion.construct( + id="chatcmpl-test", + object="chat.completion", + created=0, + model="gpt-5", + choices=[ + { + "index": 0, + "finish_reason": "tool_calls", + "logprobs": None, + "message": {"role": "assistant", "content": None, "tool_calls": tool_calls}, + } + ], + ) + + +def _dump_tool_calls(completion: ChatCompletion) -> List[Dict[str, Any]]: + parsed = parse_chat_completion(chat_completion=completion, response_format=omit, input_tools=omit) + # `cast` avoids the generic `ResponseFormatT` (unbound here) leaking `Unknown` + # into attribute access under strict type checking. Dump each tool call + # individually so a custom call is serialized by its own type rather than the + # message field's declared `list[ParsedFunctionToolCall]`. + tool_calls = cast("Any", parsed).choices[0].message.tool_calls + assert tool_calls is not None + return [tc.model_dump() for tc in tool_calls] + + +def test_parse_preserves_custom_tool_call() -> None: + # Regression: a `custom` tool call used to be logged and discarded by + # `parse_chat_completion`, so `.parse()` returned `tool_calls=None` and the + # call the model made vanished from the parsed completion. + dumped = _dump_tool_calls(_completion_with_tool_calls([_CUSTOM_CALL])) + + assert len(dumped) == 1 + assert dumped[0]["type"] == "custom" + assert dumped[0]["id"] == "call_custom" + assert dumped[0]["custom"]["name"] == "run_python" + assert dumped[0]["custom"]["input"] == "print(1)" + + +def test_parse_preserves_custom_alongside_function_tool_call() -> None: + dumped = _dump_tool_calls(_completion_with_tool_calls([_FUNCTION_CALL, _CUSTOM_CALL])) + + assert [tc["type"] for tc in dumped] == ["function", "custom"] + # the function call is still parsed as before (gets `parsed_arguments`) + assert dumped[0]["function"]["name"] == "get_weather" + assert "parsed_arguments" in dumped[0]["function"] + # the custom call is surfaced unchanged rather than dropped + assert dumped[1]["id"] == "call_custom" + assert dumped[1]["custom"]["name"] == "run_python"