feat: harden public OpenAI API contracts
Quality gate / backend (push) Canceled after 0s
Quality gate / frontend (push) Canceled after 0s

This commit is contained in:
Xin Wang
2026-09-07 14:25:48 +08:00
parent 8025ed8aa0
commit 2649d64b55
36 changed files with 16643 additions and 236 deletions
+84 -13
View File
@@ -2,12 +2,14 @@
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from typing import Any
from uuid import uuid4
from db.models import OpenAIResponse
from services.openai_api import error_payload as public_error_payload
LOCKED_FIELDS = {"instructions", "tools", "tool_choice"}
@@ -54,26 +56,26 @@ class ResponseRequest:
def error_payload(error: ResponsesAPIError) -> dict[str, Any]:
return {
"error": {
"type": "invalid_request_error",
"code": error.code,
"message": error.message,
"param": error.param,
}
}
return public_error_payload(
error.message,
code=error.code,
param=error.param,
)
def _text_content(value: object, *, role: str) -> list[dict[str, str]]:
def _text_content(value: object, *, role: str) -> list[dict[str, Any]]:
expected = "input_text" if role == "user" else "output_text"
if isinstance(value, str):
text = value.strip()
if not text:
raise ResponsesAPIError("Message content cannot be empty", param="input")
return [{"type": expected, "text": text}]
item: dict[str, Any] = {"type": expected, "text": text}
if expected == "output_text":
item["annotations"] = []
return [item]
if not isinstance(value, list) or not value:
raise ResponsesAPIError("Message content must be text", param="input")
result: list[dict[str, str]] = []
result: list[dict[str, Any]] = []
for part in value:
if not isinstance(part, dict) or part.get("type") != expected:
raise ResponsesAPIError(
@@ -82,7 +84,10 @@ def _text_content(value: object, *, role: str) -> list[dict[str, str]]:
text = str(part.get("text") or "").strip()
if not text:
raise ResponsesAPIError("Message text cannot be empty", param="input")
result.append({"type": expected, "text": text})
item: dict[str, Any] = {"type": expected, "text": text}
if expected == "output_text":
item["annotations"] = []
result.append(item)
return result
@@ -90,9 +95,11 @@ def normalize_input(value: object) -> list[dict[str, Any]]:
if isinstance(value, str):
return [
{
"id": new_id("msg"),
"type": "message",
"role": "user",
"content": _text_content(value, role="user"),
"status": "completed",
}
]
if not isinstance(value, list) or not value:
@@ -105,6 +112,13 @@ def normalize_input(value: object) -> list[dict[str, Any]]:
raise ResponsesAPIError("Input item must be an object", param=param)
item_type = str(raw.get("type") or "message")
if item_type == "message":
unknown_fields = sorted(set(raw) - {"type", "role", "content"})
if unknown_fields:
raise ResponsesAPIError(
f"Unsupported message field: {unknown_fields[0]}",
code="unsupported_parameter",
param=f"{param}.{unknown_fields[0]}",
)
role = str(raw.get("role") or "user")
if role not in {"user", "assistant"}:
raise ResponsesAPIError(
@@ -112,13 +126,22 @@ def normalize_input(value: object) -> list[dict[str, Any]]:
)
items.append(
{
"id": new_id("msg"),
"type": "message",
"role": role,
"content": _text_content(raw.get("content"), role=role),
"status": "completed",
}
)
continue
if item_type == "function_call_output":
unknown_fields = sorted(set(raw) - {"type", "call_id", "output"})
if unknown_fields:
raise ResponsesAPIError(
f"Unsupported function_call_output field: {unknown_fields[0]}",
code="unsupported_parameter",
param=f"{param}.{unknown_fields[0]}",
)
call_id = str(raw.get("call_id") or "").strip()
if not call_id:
raise ResponsesAPIError(
@@ -130,9 +153,11 @@ def normalize_input(value: object) -> list[dict[str, Any]]:
output = json.dumps(output, ensure_ascii=False, default=str)
items.append(
{
"id": new_id("fc"),
"type": "function_call_output",
"call_id": call_id,
"output": output,
"status": "completed",
}
)
continue
@@ -188,6 +213,20 @@ def parse_request(body: object) -> ResponseRequest:
"variables must be an object",
param="x_interactive_media.variables",
)
unknown_extensions = sorted(set(extension) - {"version", "variables"})
if unknown_extensions:
raise ResponsesAPIError(
f"Unsupported extension parameter: {unknown_extensions[0]}",
code="unsupported_parameter",
param=f"x_interactive_media.{unknown_extensions[0]}",
)
extension_version = extension.get("version")
if extension_version not in (None, "1"):
raise ResponsesAPIError(
"x_interactive_media.version must be 1",
code="unsupported_extension_version",
param="x_interactive_media.version",
)
raw_model = body.get("model")
if raw_model is not None and not isinstance(raw_model, str):
raise ResponsesAPIError("model must be a string", param="model")
@@ -217,6 +256,38 @@ def new_id(prefix: str) -> str:
return f"{prefix}_{uuid4().hex}"
def request_fingerprint(body: dict[str, Any]) -> str:
"""Return a stable digest for Idempotency-Key conflict detection."""
canonical = json.dumps(
body,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def normalize_idempotency_key(value: str | None) -> str | None:
if value is None:
return None
key = value.strip()
if not key:
raise ResponsesAPIError(
"Idempotency-Key must not be empty",
code="invalid_idempotency_key",
param="Idempotency-Key",
)
if len(key) > 255:
raise ResponsesAPIError(
"Idempotency-Key must be at most 255 characters",
code="invalid_idempotency_key",
param="Idempotency-Key",
)
return key
def response_payload(row: OpenAIResponse) -> dict[str, Any]:
return {
"id": row.id,
@@ -237,5 +308,5 @@ def response_payload(row: OpenAIResponse) -> dict[str, Any]:
"tool_choice": "auto",
"tools": [],
"metadata": {},
"usage": None,
"usage": getattr(row, "usage", None),
}
+82 -12
View File
@@ -28,8 +28,10 @@ from pipecat.frames.frames import (
LLMMessagesAppendFrame,
LLMTextFrame,
ManuallySwitchServiceFrame,
MetricsFrame,
TTSSpeakFrame,
)
from pipecat.metrics.metrics import LLMTokenUsage, LLMUsageMetricsData
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
@@ -63,6 +65,19 @@ class ResponseRunError(RuntimeError):
DeltaCallback = Callable[[str], Awaitable[None]]
def _normalized_tool_arguments(value: object) -> object:
if not isinstance(value, str):
return value
try:
return json.loads(value)
except json.JSONDecodeError:
return value
def _tool_arguments_equal(left: object, right: object) -> bool:
return _normalized_tool_arguments(left) == _normalized_tool_arguments(right)
@dataclass(frozen=True)
class ResponseRunResult:
text: str
@@ -70,6 +85,27 @@ class ResponseRunResult:
context_messages: list[dict[str, Any]]
dynamic_variables: dict[str, Any]
runtime_state: dict[str, Any]
usage: dict[str, Any] | None
def _usage_payload(values: list[LLMTokenUsage]) -> dict[str, Any] | None:
"""Combine Provider-reported LLM usage without estimating missing values."""
if not values:
return None
return {
"input_tokens": sum(value.prompt_tokens for value in values),
"input_tokens_details": {
"cached_tokens": sum(
value.cache_read_input_tokens or 0 for value in values
),
},
"output_tokens": sum(value.completion_tokens for value in values),
"output_tokens_details": {
"reasoning_tokens": sum(value.reasoning_tokens or 0 for value in values),
},
"total_tokens": sum(value.total_tokens for value in values),
}
@dataclass
@@ -101,6 +137,7 @@ class _CaptureProcessor(FrameProcessor):
self.window = _ResponseWindow()
self.deltas: list[str] = []
self.outputs: list[str] = []
self.llm_usage: list[LLMTokenUsage] = []
self.error: str | None = None
def reset(self) -> None:
@@ -143,6 +180,12 @@ class _CaptureProcessor(FrameProcessor):
self.error = frame.error or "Pipeline execution failed"
self.window.pending_tools.clear()
self.window.touch()
elif isinstance(frame, MetricsFrame):
self.llm_usage.extend(
metric.value
for metric in frame.data
if isinstance(metric, LLMUsageMetricsData)
)
elif isinstance(frame, _BoundaryFrame):
frame.reached.set()
self.window.touch()
@@ -162,6 +205,13 @@ class ResponsesClientToolPort:
def supports(self, _function_name: str) -> bool:
return True
@staticmethod
def _arguments_match(pending: dict[str, Any], arguments: dict[str, Any]) -> bool:
original = pending.get("arguments")
if original is None:
return True
return _tool_arguments_equal(original, arguments)
async def call(
self,
function_name: str,
@@ -174,7 +224,10 @@ class ResponsesClientToolPort:
) -> dict[str, Any]:
del timeout_seconds, wait_for_response, response_wait_mode, interrupt_on_result
for index, pending in enumerate(self._results):
if pending.get("name") == function_name:
if (
pending.get("name") == function_name
and self._arguments_match(pending, arguments)
):
self._results.pop(index)
return dict(pending.get("result") or {})
item = {
@@ -207,18 +260,29 @@ def _context_snapshot(
]
if fallback_messages:
messages.extend(deepcopy(fallback_messages))
candidates = [
tool_call
for message in messages
for tool_call in message.get("tool_calls") or []
]
used_candidates: set[int] = set()
for public_call in function_calls:
matched = False
for message in reversed(messages):
for tool_call in reversed(message.get("tool_calls") or []):
function = tool_call.get("function") or {}
if function.get("name") != public_call["name"]:
continue
tool_call["id"] = public_call["call_id"]
matched = True
break
if matched:
break
for index, tool_call in enumerate(candidates):
if index in used_candidates:
continue
function = tool_call.get("function") or {}
if function.get("name") != public_call["name"]:
continue
if not _tool_arguments_equal(
function.get("arguments"),
public_call.get("arguments"),
):
continue
tool_call["id"] = public_call["call_id"]
used_candidates.add(index)
matched = True
break
if not matched:
messages.append(
{
@@ -368,7 +432,10 @@ class ResponsesTextRunner:
)
worker = PipelineWorker(
pipeline,
params=PipelineParams(enable_metrics=False),
params=PipelineParams(
enable_metrics=False,
enable_usage_metrics=True,
),
enable_rtvi=False,
enable_turn_tracking=False,
idle_timeout_secs=None,
@@ -562,6 +629,7 @@ class ResponsesTextRunner:
),
dynamic_variables=_public_variables(brain, cfg),
runtime_state=_runtime_state(brain),
usage=_usage_payload(capture.llm_usage),
)
capture.reset()
@@ -589,6 +657,7 @@ class ResponsesTextRunner:
),
dynamic_variables=_public_variables(brain, cfg),
runtime_state=_runtime_state(brain),
usage=_usage_payload(capture.llm_usage),
)
text = (capture.outputs[-1] if capture.outputs else "".join(capture.deltas)).strip()
if not text:
@@ -599,6 +668,7 @@ class ResponsesTextRunner:
context_messages=_context_snapshot(context, function_calls=[]),
dynamic_variables=_public_variables(brain, cfg),
runtime_state=_runtime_state(brain),
usage=_usage_payload(capture.llm_usage),
)
finally:
if not worker.has_finished():
@@ -0,0 +1,314 @@
"""Pydantic contract for the supported Responses API surface."""
from __future__ import annotations
from typing import Annotated, Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
ItemStatus = Literal["in_progress", "completed", "incomplete"]
ResponseStatus = Literal[
"in_progress",
"completed",
"failed",
"cancelled",
"incomplete",
]
class _StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class InputText(_StrictModel):
type: Literal["input_text"] = "input_text"
text: str
class OutputTextInput(_StrictModel):
type: Literal["output_text"] = "output_text"
text: str
annotations: list[dict[str, Any]] = Field(default_factory=list)
MessageContent = Annotated[
InputText | OutputTextInput,
Field(discriminator="type"),
]
class ResponseInputMessage(_StrictModel):
type: Literal["message"] = "message"
role: Literal["user", "assistant"] = "user"
content: str | list[MessageContent]
class ResponseFunctionCallOutputInput(_StrictModel):
type: Literal["function_call_output"] = "function_call_output"
call_id: str
output: Any
ResponseInputItem = Annotated[
ResponseInputMessage | ResponseFunctionCallOutputInput,
Field(discriminator="type"),
]
class InteractiveMediaRequestExtension(_StrictModel):
version: Literal["1"] | None = Field(
default=None,
description="Interactive Media extension version. Omit or use `1`.",
)
variables: dict[str, Any] = Field(default_factory=dict)
class ResponseCreateRequest(BaseModel):
"""Supported request fields; extras are preserved for semantic rejection."""
model_config = ConfigDict(
extra="allow",
json_schema_extra={
"examples": [
{
"model": "assistant:asst_xxx",
"input": "请介绍一下这个产品",
"store": True,
},
{
"previous_response_id": "resp_xxx",
"input": "继续说明",
"stream": True,
},
]
},
)
model: str | None = Field(
default=None,
examples=["assistant:asst_xxx"],
description="Required on the first turn; inherited on later turns.",
)
previous_response_id: str | None = None
input: str | list[ResponseInputItem]
store: bool | None = Field(
default=None,
description="Stateful subset: omit this field or set it to true.",
)
stream: bool = False
x_interactive_media: InteractiveMediaRequestExtension | None = None
@model_validator(mode="before")
@classmethod
def default_message_discriminator(cls, value: Any) -> Any:
"""Accept the official SDK's easy input message with omitted `type`."""
if not isinstance(value, dict) or not isinstance(value.get("input"), list):
return value
copied = dict(value)
copied["input"] = [
({"type": "message", **item} if isinstance(item, dict) and "type" not in item else item)
for item in value["input"]
]
return copied
class ResponseOutputText(_StrictModel):
type: Literal["output_text"] = "output_text"
text: str
annotations: list[dict[str, Any]] = Field(default_factory=list)
logprobs: list[dict[str, Any]] | None = None
class ResponseOutputMessage(_StrictModel):
id: str
type: Literal["message"] = "message"
status: ItemStatus
role: Literal["assistant"] = "assistant"
content: list[ResponseOutputText]
class ResponseFunctionCall(_StrictModel):
id: str
type: Literal["function_call"] = "function_call"
status: ItemStatus
call_id: str
name: str
arguments: str
ResponseOutputItem = Annotated[
ResponseOutputMessage | ResponseFunctionCall,
Field(discriminator="type"),
]
class ResponseError(_StrictModel):
code: str
message: str
class ResponseUsage(_StrictModel):
input_tokens: int
output_tokens: int
total_tokens: int
input_tokens_details: dict[str, Any] = Field(default_factory=dict)
output_tokens_details: dict[str, Any] = Field(default_factory=dict)
class ResponseObject(_StrictModel):
model_config = ConfigDict(
extra="forbid",
json_schema_extra={
"examples": [
{
"id": "resp_xxx",
"object": "response",
"created_at": 1788739200,
"completed_at": 1788739201,
"status": "completed",
"error": None,
"incomplete_details": None,
"instructions": None,
"model": "assistant:asst_xxx",
"output": [
{
"id": "msg_xxx",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "这是一个实时对话助手。",
"annotations": [],
}
],
}
],
"parallel_tool_calls": False,
"previous_response_id": None,
"store": True,
"tool_choice": "auto",
"tools": [],
"metadata": {},
"usage": None,
}
]
},
)
id: str
object: Literal["response"] = "response"
created_at: int
completed_at: int | None = None
status: ResponseStatus
error: ResponseError | None = None
incomplete_details: dict[str, Any] | None = None
instructions: None = None
model: str | None
output: list[ResponseOutputItem]
parallel_tool_calls: Literal[False] = False
previous_response_id: str | None = None
store: Literal[True] = True
tool_choice: Literal["auto"] = "auto"
tools: list[Any] = Field(default_factory=list)
metadata: dict[str, str] = Field(default_factory=dict)
usage: ResponseUsage | None = None
class ResponseInputMessageItem(_StrictModel):
id: str
type: Literal["message"] = "message"
role: Literal["user", "assistant"]
content: list[MessageContent]
status: ItemStatus = "completed"
class ResponseFunctionCallOutputItem(_StrictModel):
id: str
type: Literal["function_call_output"] = "function_call_output"
call_id: str
output: str
status: ItemStatus = "completed"
ResponseStoredInputItem = Annotated[
ResponseInputMessageItem | ResponseFunctionCallOutputItem,
Field(discriminator="type"),
]
class ResponseInputItemList(_StrictModel):
model_config = ConfigDict(
extra="forbid",
json_schema_extra={
"examples": [
{
"object": "list",
"data": [
{
"id": "msg_input_xxx",
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "你好"}
],
"status": "completed",
}
],
"first_id": "msg_input_xxx",
"last_id": "msg_input_xxx",
"has_more": False,
}
]
},
)
object: Literal["list"] = "list"
data: list[ResponseStoredInputItem]
first_id: str | None = None
last_id: str | None = None
has_more: bool
class ModelObject(_StrictModel):
id: str
object: Literal["model"] = "model"
created: int
owned_by: str = "interactive-media"
name: str
capabilities: list[Literal["responses", "realtime"]]
assistant_id: str
assistant_type: str
runtime_mode: str
class ModelList(_StrictModel):
model_config = ConfigDict(
extra="forbid",
json_schema_extra={
"examples": [
{
"object": "list",
"data": [
{
"id": "assistant:asst_xxx",
"object": "model",
"created": 1788739200,
"owned_by": "interactive-media",
"name": "产品顾问",
"capabilities": ["responses", "realtime"],
"assistant_id": "asst_xxx",
"assistant_type": "prompt",
"runtime_mode": "pipeline",
}
],
}
]
},
)
object: Literal["list"] = "list"
data: list[ModelObject]
+157 -10
View File
@@ -18,9 +18,11 @@ from services.openai_responses.protocol import (
)
from services.realtime.launcher import resolve_assistant_config
from sqlalchemy import func, select, update
from sqlalchemy.exc import IntegrityError
CONTINUABLE_STATUSES = {"completed"}
IDEMPOTENCY_CONSTRAINT = "uq_openai_response_idempotency"
@dataclass(frozen=True)
@@ -33,6 +35,26 @@ class PreparedResponse:
client_tool_results: list[dict[str, Any]]
def _integrity_constraint_name(error: IntegrityError) -> str | None:
current = getattr(error, "orig", None)
seen: set[int] = set()
while current is not None and id(current) not in seen:
seen.add(id(current))
direct = getattr(current, "constraint_name", None)
if direct:
return str(direct)
diagnostic = getattr(current, "diag", None)
value = getattr(diagnostic, "constraint_name", None)
if value:
return str(value)
current = getattr(current, "__cause__", None) or getattr(
current,
"__context__",
None,
)
return None
def _content_text(item: dict[str, Any]) -> str:
return "\n".join(
str(part.get("text") or "")
@@ -97,6 +119,7 @@ async def _load_chain(
seen: set[str] = set()
async with SessionLocal() as db:
current_id: str | None = response_id
is_requested_response = True
while current_id:
if current_id in seen or len(chain) >= 1000:
raise ResponsesAPIError(
@@ -104,12 +127,17 @@ async def _load_chain(
)
seen.add(current_id)
row = await db.get(OpenAIResponse, current_id)
if row is None or row.api_key_id != api_key_id:
if (
row is None
or row.api_key_id != api_key_id
or (is_requested_response and row.deleted_at is not None)
):
raise ResponsesAPIError(
"Response not found", code="response_not_found", status_code=404
)
chain.append(row)
current_id = row.previous_response_id
is_requested_response = False
chain.reverse()
return chain
@@ -158,17 +186,23 @@ def _client_tool_results(
chain: list[OpenAIResponse],
current_items: list[dict[str, Any]],
) -> list[dict[str, Any]]:
calls = {
str(item.get("call_id") or ""): item
calls = [
item
for row in chain
for item in row.output_items or []
if item.get("type") == "function_call"
]
outputs = {
str(item.get("call_id") or ""): item
for item in current_items
if item.get("type") == "function_call_output"
}
results: list[dict[str, Any]] = []
for item in current_items:
if item.get("type") != "function_call_output":
for call in calls:
call_id = str(call.get("call_id") or "")
item = outputs.get(call_id)
if item is None:
continue
call = calls.get(str(item.get("call_id") or "")) or {}
output = str(item.get("output") or "")
try:
value = json.loads(output)
@@ -178,8 +212,9 @@ def _client_tool_results(
value = {"status": "ok", "data": value}
results.append(
{
"call_id": item.get("call_id"),
"call_id": call_id,
"name": call.get("name"),
"arguments": call.get("arguments"),
"result": value,
}
)
@@ -190,6 +225,8 @@ async def prepare_response(
request: ResponseRequest,
*,
api_key_id: str,
idempotency_key: str | None = None,
request_hash: str | None = None,
) -> PreparedResponse:
chain: list[OpenAIResponse] = []
parent: OpenAIResponse | None = None
@@ -221,7 +258,14 @@ async def prepare_response(
)
assistant_id = parent.assistant_id
if request.model:
requested_id = assistant_id_from_model(request.model)
try:
requested_id = assistant_id_from_model(request.model)
except ValueError as exc:
raise ResponsesAPIError(
str(exc),
code="invalid_model",
param="model",
) from exc
if requested_id != assistant_id:
raise ResponsesAPIError(
"model must match the previous response",
@@ -307,6 +351,8 @@ async def prepare_response(
context_messages=[],
runtime_state={},
dynamic_variables=effective_variables,
idempotency_key=idempotency_key,
request_hash=request_hash,
)
async with SessionLocal() as db:
if parent is None:
@@ -334,7 +380,21 @@ async def prepare_response(
conversation.status = "active"
conversation.ended_at = None
db.add(row)
await db.commit()
try:
await db.commit()
except IntegrityError as exc:
await db.rollback()
if (
idempotency_key is not None
and _integrity_constraint_name(exc) == IDEMPOTENCY_CONSTRAINT
):
raise ResponsesAPIError(
"Idempotency-Key is already in use",
code="idempotency_key_in_use",
param="Idempotency-Key",
status_code=409,
) from exc
raise
await db.refresh(row)
return PreparedResponse(
@@ -351,16 +411,99 @@ async def get_response(
response_id: str,
*,
api_key_id: str,
include_deleted: bool = False,
) -> OpenAIResponse:
async with SessionLocal() as db:
row = await db.get(OpenAIResponse, response_id)
if row is None or row.api_key_id != api_key_id:
if (
row is None
or row.api_key_id != api_key_id
or (row.deleted_at is not None and not include_deleted)
):
raise ResponsesAPIError(
"Response not found", code="response_not_found", status_code=404
)
return row
async def get_idempotent_response(
*,
api_key_id: str,
idempotency_key: str | None,
request_hash: str,
) -> OpenAIResponse | None:
"""Return a terminal replay or reject conflicting reuse of a request key."""
if idempotency_key is None:
return None
async with SessionLocal() as db:
row = (
await db.execute(
select(OpenAIResponse).where(
OpenAIResponse.api_key_id == api_key_id,
OpenAIResponse.idempotency_key == idempotency_key,
)
)
).scalar_one_or_none()
if row is None:
return None
if row.request_hash != request_hash:
raise ResponsesAPIError(
"Idempotency-Key was already used with a different request",
code="idempotency_key_conflict",
param="Idempotency-Key",
status_code=409,
)
if row.deleted_at is not None:
raise ResponsesAPIError(
"The response created by this Idempotency-Key was deleted",
code="idempotency_response_deleted",
param="Idempotency-Key",
status_code=409,
)
if row.status == "in_progress":
raise ResponsesAPIError(
"A request with this Idempotency-Key is still in progress",
code="idempotency_key_in_use",
param="Idempotency-Key",
status_code=409,
)
return row
async def delete_response(
response_id: str,
*,
api_key_id: str,
) -> OpenAIResponse:
"""Create a tombstone while retaining state needed by existing branches."""
async with SessionLocal() as db:
row = await db.get(
OpenAIResponse,
response_id,
with_for_update=True,
)
if (
row is None
or row.api_key_id != api_key_id
or row.deleted_at is not None
):
raise ResponsesAPIError(
"Response not found", code="response_not_found", status_code=404
)
if row.status == "in_progress":
raise ResponsesAPIError(
"An in-progress response cannot be deleted",
code="response_not_ready",
status_code=409,
)
row.deleted_at = datetime.now(UTC)
await db.commit()
await db.refresh(row)
return row
async def finish_response(
response_id: str,
*,
@@ -370,6 +513,7 @@ async def finish_response(
dynamic_variables: dict[str, Any] | None = None,
runtime_state: dict[str, Any] | None = None,
error: dict[str, Any] | None = None,
usage: dict[str, Any] | None = None,
) -> OpenAIResponse:
now = datetime.now(UTC)
async with SessionLocal() as db:
@@ -384,6 +528,7 @@ async def finish_response(
if runtime_state is not None:
row.runtime_state = runtime_state
row.error = error
row.usage = usage
row.completed_at = now
conversation = (
@@ -445,6 +590,8 @@ async def finish_response(
await db.commit()
await db.refresh(row)
return row
async def recover_interrupted_responses() -> None:
"""Mark generations interrupted by a process restart as cancelled."""
now = datetime.now(UTC)
+193
View File
@@ -0,0 +1,193 @@
"""Wire-format helpers for the public Responses API subset."""
from __future__ import annotations
import json
from typing import Any
from db.models import OpenAIResponse
def encode_sse(event: dict[str, Any]) -> bytes:
"""Encode one named Responses event as an SSE record."""
return (
f"event: {event['type']}\n"
f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
).encode("utf-8")
class SSEEventSequence:
"""Attach the monotonically increasing sequence required by SDK models."""
def __init__(self) -> None:
self._next = 0
def encode(self, event_type: str, **payload: Any) -> bytes:
event = {
"type": event_type,
"sequence_number": self._next,
**payload,
}
self._next += 1
return encode_sse(event)
def assistant_text_item(item_id: str, text: str) -> dict[str, Any]:
return {
"id": item_id,
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": text,
"annotations": [],
}
],
}
def build_output_items(
*,
text: str,
text_item_id: str,
function_calls: list[dict[str, Any]],
) -> list[dict[str, Any]]:
items: list[dict[str, Any]] = []
if text:
items.append(assistant_text_item(text_item_id, text))
items.extend(function_calls)
return items
def output_lifecycle_events(
row: OpenAIResponse,
*,
started_text_items: set[str],
replay_text: bool,
) -> list[dict[str, Any]]:
"""Build terminal item events shared by live and idempotent streams."""
events: list[dict[str, Any]] = []
for output_index, item in enumerate(row.output_items or []):
if item.get("type") != "message":
pending_item = {**item, "status": "in_progress"}
if item.get("type") == "function_call":
pending_item["arguments"] = ""
events.append(
{
"type": "response.output_item.added",
"output_index": output_index,
"item": pending_item,
}
)
if item.get("type") == "function_call":
item_id = str(item.get("id") or "")
arguments = str(item.get("arguments") or "")
if arguments:
events.append(
{
"type": "response.function_call_arguments.delta",
"item_id": item_id,
"output_index": output_index,
"delta": arguments,
}
)
events.append(
{
"type": "response.function_call_arguments.done",
"item_id": item_id,
"output_index": output_index,
"name": str(item.get("name") or ""),
"arguments": arguments,
}
)
events.append(
{
"type": "response.output_item.done",
"output_index": output_index,
"item": item,
}
)
continue
item_id = str(item["id"])
content = item.get("content") or []
part = content[0] if content else {
"type": "output_text",
"text": "",
"annotations": [],
}
text = str(part.get("text") or "")
if item_id not in started_text_items:
events.extend(
[
{
"type": "response.output_item.added",
"output_index": output_index,
"item": {**item, "status": "in_progress", "content": []},
},
{
"type": "response.content_part.added",
"item_id": item_id,
"output_index": output_index,
"content_index": 0,
"part": {
"type": "output_text",
"text": "",
"annotations": [],
},
},
]
)
started_text_items.add(item_id)
if replay_text and text:
events.append(
{
"type": "response.output_text.delta",
"item_id": item_id,
"output_index": output_index,
"content_index": 0,
"delta": text,
"logprobs": [],
}
)
events.extend(
[
{
"type": "response.output_text.done",
"item_id": item_id,
"output_index": output_index,
"content_index": 0,
"text": text,
"logprobs": [],
},
{
"type": "response.content_part.done",
"item_id": item_id,
"output_index": output_index,
"content_index": 0,
"part": part,
},
{
"type": "response.output_item.done",
"output_index": output_index,
"item": item,
},
]
)
return events
def stored_input_items(row: OpenAIResponse) -> list[dict[str, Any]]:
"""Backfill stable wire IDs for records created before contract v1."""
result: list[dict[str, Any]] = []
for index, stored in enumerate(row.input_items or []):
item = dict(stored)
item.setdefault("id", f"item_{row.id}_{index}")
item.setdefault("status", "completed")
result.append(item)
return result