import warnings
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, model_validator
"""
Classes are inspired by classes for Response and ResponseStreamEvent in openai-python
https://github.com/openai/openai-python/blob/ed53107e10e6c86754866b48f8bd862659134ca8/src/openai/types/responses/response.py#L31
https://github.com/openai/openai-python/blob/ed53107e10e6c86754866b48f8bd862659134ca8/src/openai/types/responses/response_stream_event.py#L42
"""
#########################
# Response helper classes
#########################
[docs]class Status(BaseModel):
status: str | None = None
@model_validator(mode="after")
def check_status(self) -> "Status":
if self.status is not None and self.status not in {
"in_progress",
"completed",
"incomplete",
}:
raise ValueError(
f"Invalid status: {self.status} for {self.__class__.__name__}. "
"Must be 'in_progress', 'completed', or 'incomplete'."
)
return self
[docs]class ResponseError(BaseModel):
code: str | None = None
message: str
[docs]class AnnotationFileCitation(BaseModel):
file_id: str
index: int
type: str = "file_citation"
[docs]class AnnotationURLCitation(BaseModel):
end_index: int | None = None
start_index: int | None = None
title: str
type: str = "url_citation"
url: str
[docs]class AnnotationFilePath(BaseModel):
file_id: str
index: int
type: str = "file_path"
[docs]class Annotation(BaseModel):
model_config = ConfigDict(extra="allow")
type: str
@model_validator(mode="after")
def check_type(self) -> "Annotation":
if self.type == "file_citation":
AnnotationFileCitation(**self.model_dump())
elif self.type == "url_citation":
AnnotationURLCitation(**self.model_dump())
elif self.type == "file_path":
AnnotationFilePath(**self.model_dump())
else:
raise ValueError(f"Invalid annotation type: {self.type}")
return self
[docs]class ResponseOutputText(BaseModel):
annotations: list[Annotation] = Field(default_factory=list)
text: str
type: str = "output_text"
[docs]class ResponseOutputRefusal(BaseModel):
refusal: str
type: str = "refusal"
[docs]class Content(BaseModel):
model_config = ConfigDict(extra="allow")
type: str
@model_validator(mode="after")
def check_type(self) -> "Content":
if self.type == "output_text":
ResponseOutputText(**self.model_dump())
elif self.type == "refusal":
ResponseOutputRefusal(**self.model_dump())
else:
raise ValueError(f"Invalid content type: {self.type} for {self.__class__.__name__}")
return self
[docs]class ResponseOutputMessage(Status):
id: str
content: list[Content]
role: str = "assistant"
type: str = "message"
@model_validator(mode="after")
def check_role(self) -> "ResponseOutputMessage":
if self.role != "assistant":
raise ValueError(f"Invalid role: {self.role}. Must be 'assistant'.")
return self
@model_validator(mode="after")
def check_content(self) -> "ResponseOutputMessage":
if self.content is None:
raise ValueError(f"content must not be None for {self.__class__.__name__}")
if isinstance(self.content, list) and len(self.content) == 0:
raise ValueError("content must not be an empty list")
return self
[docs]class Summary(BaseModel):
text: str
type: str = "summary_text"
[docs]class ResponseReasoningItem(Status):
id: str
summary: list[Summary]
type: str = "reasoning"
[docs]class McpApprovalRequest(Status):
id: str
arguments: str
name: str
server_label: str
type: str = "mcp_approval_request"
[docs]class McpApprovalResponse(Status):
approval_request_id: str
approve: bool
type: str = "mcp_approval_response"
id: str | None = None
reason: str | None = None
[docs]class OutputItem(BaseModel):
model_config = ConfigDict(extra="allow")
type: str
@model_validator(mode="after")
def check_type(self) -> "OutputItem":
if self.type == "message":
ResponseOutputMessage(**self.model_dump())
elif self.type == "function_call":
ResponseFunctionToolCall(**self.model_dump())
elif self.type == "reasoning":
ResponseReasoningItem(**self.model_dump())
elif self.type == "function_call_output":
FunctionCallOutput(**self.model_dump())
elif self.type == "mcp_approval_request":
McpApprovalRequest(**self.model_dump())
elif self.type == "mcp_approval_response":
McpApprovalResponse(**self.model_dump())
elif self.type not in {
"file_search_call",
"computer_call",
"web_search_call",
}:
raise ValueError(f"Invalid type: {self.type} for {self.__class__.__name__}")
return self
[docs]class IncompleteDetails(BaseModel):
reason: str | None = None
@model_validator(mode="after")
def check_reason(self) -> "IncompleteDetails":
if self.reason and self.reason not in {"max_output_tokens", "content_filter"}:
warnings.warn(f"Invalid reason: {self.reason}")
return self
[docs]class ReasoningParams(BaseModel):
effort: str | None = None
generate_summary: str | None = None
@model_validator(mode="after")
def check_generate_summary(self) -> "ReasoningParams":
if self.generate_summary and self.generate_summary not in {"concise", "detailed"}:
warnings.warn(f"Invalid generate_summary: {self.generate_summary}")
return self
@model_validator(mode="after")
def check_effort(self) -> "ReasoningParams":
if self.effort and self.effort not in {"low", "medium", "high"}:
warnings.warn(f"Invalid effort: {self.effort}")
return self
[docs]class OutputTokensDetails(BaseModel):
reasoning_tokens: int
[docs]class ResponseUsage(BaseModel):
input_tokens: int
input_tokens_details: InputTokensDetails
output_tokens: int
output_tokens_details: OutputTokensDetails
total_tokens: int
[docs]class Truncation(BaseModel):
truncation: str | None = None
@model_validator(mode="after")
def check_truncation(self) -> "Truncation":
if self.truncation and self.truncation not in {"auto", "disabled"}:
warnings.warn(f"Invalid truncation: {self.truncation}. Must be 'auto' or 'disabled'.")
return self
[docs]class Response(Truncation, ToolChoice):
id: str | None = None
created_at: float | None = None
error: ResponseError | None = None
incomplete_details: IncompleteDetails | None = None
instructions: str | None = None
metadata: dict[str, str] | None = None
model: str | None = None
object: str = "response"
output: list[OutputItem]
parallel_tool_calls: bool | None = None
temperature: float | None = None
tools: list[Tool] | None = None
top_p: float | None = None
max_output_tokens: int | None = None
previous_response_id: str | None = None
reasoning: ReasoningParams | None = None
status: str | None = None
text: Any | None = None
usage: ResponseUsage | None = None
user: str | None = None
@property
def output_text(self) -> str:
"""Convenience property that aggregates all `output_text` items from the `output`
list.
If no `output_text` content blocks exist, then an empty string is returned.
"""
texts: list[str] = []
for output in self.output:
if output.type == "message":
texts.extend(
content.text for content in output.content if content.type == "output_text"
)
return "".join(texts)
@model_validator(mode="after")
def check_status(self) -> "Response":
if self.status and self.status not in {
"completed",
"failed",
"in_progress",
"incomplete",
}:
warnings.warn(
f"Invalid status: {self.status}. Must be 'completed', 'failed', "
"'in_progress', or 'incomplete'."
)
return self
#################################
# ResponsesRequest helper classes
#################################
[docs]class ResponseInputTextParam(BaseModel):
text: str
type: str = "input_text"
[docs]class Message(Status):
content: str | list[ResponseInputTextParam | dict[str, Any]]
role: str
status: str | None = None
type: str = "message"
@model_validator(mode="after")
def check_content(self) -> "Message":
if self.content is None:
raise ValueError("content must not be None")
if isinstance(self.content, list):
for item in self.content:
if isinstance(item, dict):
if "type" not in item:
raise ValueError(
"dictionary type content field values must have key 'type'"
)
if item["type"] == "input_text":
ResponseInputTextParam(**item)
elif item["type"] not in {"input_image", "input_file"}:
raise ValueError(f"Invalid type: {item['type']}.")
return self
@model_validator(mode="after")
def check_role(self) -> "Message":
if self.role not in {"user", "assistant", "system", "developer"}:
raise ValueError(
f"Invalid role: {self.role}. Must be 'user', 'assistant', 'system', or 'developer'."
)
return self
[docs]class FunctionCallOutput(Status):
call_id: str
output: str | list[dict[str, Any]]
type: str = "function_call_output"
[docs]class BaseRequestPayload(Truncation, ToolChoice):
max_output_tokens: int | None = None
metadata: dict[str, str] | None = None
parallel_tool_calls: bool | None = None
tools: list[Tool] | None = None
reasoning: ReasoningParams | None = None
store: bool | None = None
stream: bool | None = None
temperature: float | None = None
text: Any | None = None
top_p: float | None = None
user: str | None = None
background: bool | None = None
#####################################
# ResponsesStreamEvent helper classes
#####################################
[docs]class ResponseTextDeltaEvent(BaseModel):
content_index: int | None = None
delta: str
item_id: str
output_index: int | None = None
type: str = "response.output_text.delta"
[docs]class ResponseTextAnnotationDeltaEvent(BaseModel):
annotation: Annotation
annotation_index: int
content_index: int | None = None
item_id: str
output_index: int | None = None
type: str = "response.output_text.annotation.added"
[docs]class ResponseOutputItemDoneEvent(BaseModel):
item: OutputItem
output_index: int | None = None
type: str = "response.output_item.done"
[docs]class ResponseErrorEvent(BaseModel):
code: str | None = None
message: str
param: str | None = None
type: str = "error"
[docs]class ResponseCompletedEvent(BaseModel):
response: Response
type: str = "response.completed"