Skip to content

Commit c6dc33b

Browse files
rushitatcursoragent
andcommitted
Add types for Responses API (with tests)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1da4a91 commit c6dc33b

6 files changed

Lines changed: 336 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Types for the Responses API. See docs/RESPONSES_API_PR_BREAKDOWN.md.
2+
3+
from __future__ import annotations
4+
5+
from .response_create_params import (
6+
ResponseTool as ResponseTool,
7+
ResponseInputItem as ResponseInputItem,
8+
ResponseToolChoice as ResponseToolChoice,
9+
ResponseCreateParams as ResponseCreateParams,
10+
ResponseToolFunction as ResponseToolFunction,
11+
ResponseToolChoiceNamed as ResponseToolChoiceNamed,
12+
ResponseInputUserMessage as ResponseInputUserMessage,
13+
ResponseInputFunctionCall as ResponseInputFunctionCall,
14+
ResponseToolChoiceFunction as ResponseToolChoiceFunction,
15+
ResponseInputFunctionCallOutput as ResponseInputFunctionCallOutput,
16+
)
17+
from .response_create_response import (
18+
ResponseOutputItem as ResponseOutputItem,
19+
ResponseOutputMessage as ResponseOutputMessage,
20+
ResponseCreateResponse as ResponseCreateResponse,
21+
ResponseOutputFunctionCall as ResponseOutputFunctionCall,
22+
)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Types for the Responses API (POST /v1/responses). See docs/RESPONSES_API_PR_BREAKDOWN.md.
2+
3+
from __future__ import annotations
4+
5+
from typing import Dict, Union, Iterable, Optional
6+
from typing_extensions import Literal, Required, TypeAlias, TypedDict
7+
8+
__all__ = [
9+
"ResponseCreateParams",
10+
"ResponseInputItem",
11+
"ResponseInputUserMessage",
12+
"ResponseInputFunctionCall",
13+
"ResponseInputFunctionCallOutput",
14+
"ResponseToolChoice",
15+
"ResponseToolChoiceFunction",
16+
"ResponseTool",
17+
"ResponseToolFunction",
18+
]
19+
20+
21+
class ResponseInputUserMessage(TypedDict, total=False):
22+
"""User message in the request input list."""
23+
24+
type: Required[Literal["message"]]
25+
role: Required[Literal["user"]]
26+
content: Required[str]
27+
28+
29+
class ResponseInputFunctionCall(TypedDict, total=False):
30+
"""Function call (assistant turn) in the request input list."""
31+
32+
type: Required[Literal["function_call"]]
33+
id: Required[str]
34+
name: Required[str]
35+
arguments: Required[str]
36+
37+
38+
class ResponseInputFunctionCallOutput(TypedDict, total=False):
39+
"""Function call result (tool output) in the request input list."""
40+
41+
type: Required[Literal["function_call_output"]]
42+
call_id: Required[str]
43+
output: Required[str]
44+
45+
46+
ResponseInputItem: TypeAlias = Union[
47+
ResponseInputUserMessage,
48+
ResponseInputFunctionCall,
49+
ResponseInputFunctionCallOutput,
50+
]
51+
52+
53+
class ResponseToolFunction(TypedDict, total=False):
54+
"""Function definition for a tool."""
55+
56+
name: Required[str]
57+
description: str
58+
parameters: Dict[str, object]
59+
60+
61+
class ResponseTool(TypedDict, total=False):
62+
"""Tool the model may call (e.g. a function)."""
63+
64+
type: Required[Literal["function"]]
65+
function: Required[ResponseToolFunction]
66+
67+
68+
class ResponseToolChoiceFunction(TypedDict, total=False):
69+
name: Required[str]
70+
71+
72+
class ResponseToolChoiceNamed(TypedDict, total=False):
73+
type: Required[Literal["function"]]
74+
function: Required[ResponseToolChoiceFunction]
75+
76+
77+
ResponseToolChoice: TypeAlias = Union[
78+
Literal["none", "auto", "required"],
79+
ResponseToolChoiceNamed,
80+
]
81+
82+
83+
class ResponseCreateParams(TypedDict, total=False):
84+
"""Request body for POST /v1/responses."""
85+
86+
model: Required[str]
87+
"""Model ID (e.g. openai-gpt-5.2-pro)."""
88+
89+
input: Required[Iterable[ResponseInputItem]]
90+
"""List of input items: user messages, function_call, function_call_output."""
91+
92+
tools: Iterable[ResponseTool]
93+
"""Optional list of tools the model may call."""
94+
95+
max_output_tokens: Optional[int]
96+
"""Maximum tokens to generate."""
97+
98+
instructions: Optional[str]
99+
"""System or developer instructions."""
100+
101+
temperature: Optional[float]
102+
"""Sampling temperature."""
103+
104+
tool_choice: ResponseToolChoice
105+
"""Which tool (if any) the model must or may call."""
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Response type for the Responses API (POST /v1/responses). See docs/RESPONSES_API_PR_BREAKDOWN.md.
2+
3+
from __future__ import annotations
4+
5+
from typing import List, Union, Optional
6+
from typing_extensions import Literal, Annotated, TypeAlias
7+
8+
from ..._utils import PropertyInfo
9+
from ..._models import BaseModel
10+
from ..shared.completion_usage import CompletionUsage
11+
12+
__all__ = [
13+
"ResponseCreateResponse",
14+
"ResponseOutputItem",
15+
"ResponseOutputMessage",
16+
"ResponseOutputFunctionCall",
17+
]
18+
19+
20+
class ResponseOutputMessage(BaseModel):
21+
"""Message item in the response output list."""
22+
23+
type: Literal["message"] = "message"
24+
role: Literal["assistant"] = "assistant"
25+
content: Optional[str] = None
26+
"""Text content of the message."""
27+
output_text: Optional[str] = None
28+
"""Aggregated or final text for this item (when present)."""
29+
30+
31+
class ResponseOutputFunctionCall(BaseModel):
32+
"""Function call item in the response output list."""
33+
34+
type: Literal["function_call"] = "function_call"
35+
id: str
36+
name: str
37+
arguments: str
38+
39+
40+
# Discriminated union so Pydantic parses each output item by "type".
41+
ResponseOutputItem: TypeAlias = Annotated[
42+
Union[ResponseOutputMessage, ResponseOutputFunctionCall],
43+
PropertyInfo(discriminator="type"),
44+
]
45+
46+
47+
class ResponseCreateResponse(BaseModel):
48+
"""
49+
Response from POST /v1/responses.
50+
Use the `output_text` property to get aggregated text from message items in `output`.
51+
"""
52+
53+
id: str
54+
"""Unique identifier for the response."""
55+
56+
output: List[ResponseOutputItem]
57+
"""List of output items (messages, function calls)."""
58+
59+
status: str
60+
"""Status of the response (e.g. completed, failed)."""
61+
62+
error: Optional[str] = None
63+
"""Error message if status indicates failure."""
64+
65+
model: Optional[str] = None
66+
"""Model used for the response."""
67+
68+
usage: Optional[CompletionUsage] = None
69+
"""Token usage statistics."""
70+
71+
@property
72+
def output_text(self) -> str:
73+
"""
74+
Aggregate text from all message items in `output`.
75+
For each item with type "message", uses `output_text` if present, else `content`.
76+
"""
77+
parts: List[str] = []
78+
for item in self.output:
79+
if isinstance(item, ResponseOutputMessage):
80+
text: Optional[str] = item.output_text if item.output_text is not None else item.content
81+
if text:
82+
parts.append(text)
83+
return "".join(parts)

tests/types/__init__.py

Whitespace-only changes.

tests/types/responses/__init__.py

Whitespace-only changes.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Tests for Responses API response types. No network; static payloads only.
2+
3+
from __future__ import annotations
4+
5+
from gradient.types.responses import (
6+
ResponseOutputMessage,
7+
ResponseCreateResponse,
8+
ResponseOutputFunctionCall,
9+
)
10+
11+
# Minimal valid response payload (static, no network).
12+
MINIMAL_RESPONSE = {
13+
"id": "resp_123",
14+
"output": [],
15+
"status": "completed",
16+
"model": "openai-gpt-5.2-pro",
17+
}
18+
19+
20+
class TestResponseCreateResponseParse:
21+
"""Test that ResponseCreateResponse parses minimal and extended JSON."""
22+
23+
def test_parse_minimal_response(self) -> None:
24+
parsed = ResponseCreateResponse.model_validate(MINIMAL_RESPONSE)
25+
assert parsed.id == "resp_123"
26+
assert parsed.output == []
27+
assert parsed.status == "completed"
28+
assert parsed.model == "openai-gpt-5.2-pro"
29+
assert parsed.output_text == ""
30+
31+
def test_parse_response_with_usage(self) -> None:
32+
payload = {
33+
**MINIMAL_RESPONSE,
34+
"usage": {
35+
"prompt_tokens": 10,
36+
"completion_tokens": 5,
37+
"total_tokens": 15,
38+
},
39+
}
40+
parsed = ResponseCreateResponse.model_validate(payload)
41+
assert parsed.usage is not None
42+
assert parsed.usage.prompt_tokens == 10
43+
assert parsed.usage.completion_tokens == 5
44+
assert parsed.usage.total_tokens == 15
45+
46+
47+
class TestResponseCreateResponseOutputText:
48+
"""Test that output_text aggregates text from message items in output."""
49+
50+
def test_output_text_aggregates_content(self) -> None:
51+
payload = {
52+
**MINIMAL_RESPONSE,
53+
"output": [
54+
{"type": "message", "role": "assistant", "content": "Hello "},
55+
{"type": "message", "role": "assistant", "content": "world."},
56+
],
57+
}
58+
parsed = ResponseCreateResponse.model_validate(payload)
59+
assert parsed.output_text == "Hello world."
60+
61+
def test_output_text_prefers_output_text_field(self) -> None:
62+
payload = {
63+
**MINIMAL_RESPONSE,
64+
"output": [
65+
{
66+
"type": "message",
67+
"role": "assistant",
68+
"content": "raw",
69+
"output_text": "aggregated",
70+
},
71+
],
72+
}
73+
parsed = ResponseCreateResponse.model_validate(payload)
74+
assert parsed.output_text == "aggregated"
75+
76+
def test_output_text_skips_function_call_items(self) -> None:
77+
payload = {
78+
**MINIMAL_RESPONSE,
79+
"output": [
80+
{"type": "message", "role": "assistant", "content": "Here is "},
81+
{
82+
"type": "function_call",
83+
"id": "call_1",
84+
"name": "get_weather",
85+
"arguments": "{}",
86+
},
87+
{"type": "message", "role": "assistant", "content": "the result."},
88+
],
89+
}
90+
parsed = ResponseCreateResponse.model_validate(payload)
91+
assert parsed.output_text == "Here is the result."
92+
93+
def test_output_text_empty_message_content_treated_as_empty(self) -> None:
94+
payload = {
95+
**MINIMAL_RESPONSE,
96+
"output": [
97+
{"type": "message", "role": "assistant", "content": None},
98+
{"type": "message", "role": "assistant", "output_text": "only this"},
99+
],
100+
}
101+
parsed = ResponseCreateResponse.model_validate(payload)
102+
assert parsed.output_text == "only this"
103+
104+
105+
class TestResponseOutputItemTypes:
106+
"""Test that output item types parse correctly."""
107+
108+
def test_message_item_parses(self) -> None:
109+
msg = ResponseOutputMessage.model_validate({"type": "message", "role": "assistant", "content": "Hi"})
110+
assert msg.type == "message"
111+
assert msg.role == "assistant"
112+
assert msg.content == "Hi"
113+
114+
def test_function_call_item_parses(self) -> None:
115+
fc = ResponseOutputFunctionCall.model_validate(
116+
{
117+
"type": "function_call",
118+
"id": "call_1",
119+
"name": "foo",
120+
"arguments": '{"x": 1}',
121+
}
122+
)
123+
assert fc.type == "function_call"
124+
assert fc.id == "call_1"
125+
assert fc.name == "foo"
126+
assert fc.arguments == '{"x": 1}'

0 commit comments

Comments
 (0)