Skip to content

Commit 6611b42

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

6 files changed

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

0 commit comments

Comments
 (0)