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),
}