Coverage for src / lexigram / contracts / ai / relay / dto / openai_responses.py: 36%

205 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-19 05:41 +0800

1"""OpenAI Responses wire DTO family.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass, field 

6from typing import Any 

7 

8from lexigram.contracts.ai.relay.dto.common import require_field 

9 

10__all__ = [ 

11 "ResponsesEvent", 

12 "ResponsesIncompleteDetails", 

13 "ResponsesItem", 

14 "ResponsesRequest", 

15 "ResponsesResponse", 

16 "ResponsesUsage", 

17] 

18 

19 

20@dataclass(frozen=True) 

21class ResponsesItem: 

22 """An item in a Responses request ``input`` or response ``output``. 

23 

24 Attributes: 

25 type: ``message``, ``function_call``, ``function_call_output``, 

26 ``reasoning``, ``web_search_call``. 

27 role: ``user`` / ``assistant`` for message items. 

28 content: Message content list, or ``None``. 

29 id: Item id, or ``None``. 

30 call_id: Function call id, or ``None``. 

31 name: Function name, or ``None``. 

32 arguments: Function call arguments string, or ``None``. 

33 output: Function call output string, or ``None``. 

34 summary: Reasoning summary for ``reasoning`` items, or ``None``. 

35 status: Output item status (``completed`` / ``incomplete``). 

36 quality: Output item quality (relaykit always serializes ``""``). 

37 size: Output item size (relaykit always serializes ``""``). 

38 passthrough: Unknown fields preserved verbatim. 

39 """ 

40 

41 type: str | None = None 

42 role: str | None = None 

43 content: str | list[dict[str, Any]] | None = None 

44 id: str | None = None 

45 call_id: str | None = None 

46 name: str | None = None 

47 arguments: str | None = None 

48 output: str | None = None 

49 summary: list[dict[str, Any]] | None = None 

50 status: str | None = None 

51 quality: str | None = None 

52 size: str | None = None 

53 passthrough: dict[str, Any] = field(default_factory=dict) 

54 

55 def to_dict(self) -> dict[str, Any]: 

56 """Serialize to wire dict. 

57 

58 Output items (those carrying a ``status``) always serialize 

59 ``content`` (relaykit uses no ``omitempty`` on it, so function 

60 calls carry an explicit ``null``) plus the ``status``, 

61 ``quality``, and ``size`` fields relaykit Go structs always 

62 render. Input request items never render a ``content`` key for 

63 function calls. 

64 """ 

65 data: dict[str, Any] = {**self.passthrough} 

66 if self.type is not None: 

67 data["type"] = self.type 

68 if self.role is not None: 

69 data["role"] = self.role 

70 if ( 

71 self.type == "function_call" and self.status is not None 

72 ) or self.content is not None: 

73 data["content"] = self.content 

74 if self.status is not None: 

75 data["status"] = self.status 

76 if self.quality is not None: 

77 data["quality"] = self.quality 

78 if self.size is not None: 

79 data["size"] = self.size 

80 if self.id is not None: 

81 data["id"] = self.id 

82 if self.call_id is not None: 

83 data["call_id"] = self.call_id 

84 if self.name is not None: 

85 data["name"] = self.name 

86 if self.arguments is not None: 

87 data["arguments"] = self.arguments 

88 if self.output is not None: 

89 data["output"] = self.output 

90 if self.summary is not None: 

91 data["summary"] = self.summary 

92 return data 

93 

94 @classmethod 

95 def from_dict(cls, data: dict[str, Any]) -> ResponsesItem: 

96 """Build an item from a wire dict, capturing unknown keys.""" 

97 known = { 

98 "type", 

99 "role", 

100 "content", 

101 "id", 

102 "call_id", 

103 "name", 

104 "arguments", 

105 "output", 

106 "summary", 

107 "status", 

108 "quality", 

109 "size", 

110 } 

111 return cls( 

112 type=data.get("type", "message"), 

113 role=data.get("role"), 

114 content=data.get("content"), 

115 id=data.get("id"), 

116 call_id=data.get("call_id"), 

117 name=data.get("name"), 

118 arguments=data.get("arguments"), 

119 output=data.get("output"), 

120 summary=data.get("summary"), 

121 status=data.get("status"), 

122 quality=data.get("quality"), 

123 size=data.get("size"), 

124 passthrough={k: v for k, v in data.items() if k not in known}, 

125 ) 

126 

127 

128@dataclass(frozen=True) 

129class ResponsesRequest: 

130 """OpenAI Responses request body. 

131 

132 Attributes: 

133 model: Model name. 

134 input: List of items, or a plain string. 

135 instructions: System instructions, or ``None``. 

136 tools: Tool definitions, or ``None``. 

137 temperature: Sampling temperature, or ``None``. 

138 max_output_tokens: Max output tokens, or ``None``. 

139 stream: Whether the caller wants a stream. 

140 include: Extra top-level fields to include, or ``None``. 

141 parallel_tool_calls: Parallel tool-call flag, or ``None``. 

142 reasoning: Reasoning config (e.g. ``{"effort": ...}``), or ``None``. 

143 text: Response-format config, or ``None``. 

144 service_tier: Service tier, or ``None``. 

145 passthrough: Unknown fields preserved verbatim. 

146 """ 

147 

148 model: str 

149 input: list[ResponsesItem] | str 

150 instructions: str | None = None 

151 tools: list[dict[str, Any]] | None = None 

152 temperature: float | None = None 

153 max_output_tokens: int | None = None 

154 stream: bool = False 

155 include: list[str] | None = None 

156 parallel_tool_calls: bool | None = None 

157 reasoning: dict[str, Any] | None = None 

158 text: dict[str, Any] | None = None 

159 service_tier: str | None = None 

160 tool_choice: Any | None = None 

161 passthrough: dict[str, Any] = field(default_factory=dict) 

162 

163 def to_dict(self) -> dict[str, Any]: 

164 """Serialize to wire dict, omitting ``None`` optional fields.""" 

165 data: dict[str, Any] = {**self.passthrough, "model": self.model} 

166 if isinstance(self.input, str): 

167 data["input"] = self.input 

168 else: 

169 data["input"] = [i.to_dict() for i in self.input] 

170 if self.instructions is not None: 

171 data["instructions"] = self.instructions 

172 if self.tools is not None: 

173 data["tools"] = self.tools 

174 if self.temperature is not None: 

175 data["temperature"] = self.temperature 

176 if self.max_output_tokens is not None: 

177 data["max_output_tokens"] = self.max_output_tokens 

178 data["stream"] = self.stream 

179 if self.include is not None: 

180 data["include"] = self.include 

181 if self.parallel_tool_calls is not None: 

182 data["parallel_tool_calls"] = self.parallel_tool_calls 

183 if self.reasoning is not None: 

184 data["reasoning"] = self.reasoning 

185 if self.text is not None: 

186 data["text"] = self.text 

187 if self.service_tier is not None: 

188 data["service_tier"] = self.service_tier 

189 if self.tool_choice is not None: 

190 data["tool_choice"] = self.tool_choice 

191 return data 

192 

193 @classmethod 

194 def from_dict(cls, data: dict[str, Any]) -> ResponsesRequest: 

195 """Build a request from a wire dict, capturing unknown keys. 

196 

197 Raises: 

198 RelayError: With code ``malformed_payload`` when ``model`` 

199 is absent. 

200 """ 

201 known = { 

202 "model", 

203 "input", 

204 "instructions", 

205 "tools", 

206 "temperature", 

207 "max_output_tokens", 

208 "stream", 

209 "include", 

210 "parallel_tool_calls", 

211 "reasoning", 

212 "text", 

213 "service_tier", 

214 "tool_choice", 

215 } 

216 raw_input = data.get("input", []) 

217 return cls( 

218 model=require_field(data, "model"), 

219 input=( 

220 raw_input 

221 if isinstance(raw_input, str) 

222 else [ResponsesItem.from_dict(i) for i in raw_input] 

223 ), 

224 instructions=data.get("instructions"), 

225 tools=data.get("tools"), 

226 temperature=data.get("temperature"), 

227 max_output_tokens=data.get("max_output_tokens"), 

228 stream=bool(data.get("stream", False)), 

229 include=data.get("include"), 

230 parallel_tool_calls=data.get("parallel_tool_calls"), 

231 reasoning=data.get("reasoning"), 

232 text=data.get("text"), 

233 service_tier=data.get("service_tier"), 

234 tool_choice=data.get("tool_choice"), 

235 passthrough={k: v for k, v in data.items() if k not in known}, 

236 ) 

237 

238 

239@dataclass(frozen=True) 

240class ResponsesUsage: 

241 """OpenAI Responses usage accounting. 

242 

243 Attributes: 

244 prompt_tokens: Chat-style prompt token count. 

245 completion_tokens: Chat-style completion token count. 

246 total_tokens: Explicit totals (can differ from prompt + completion). 

247 prompt_tokens_details: Cache details (relaykit serializes a 

248 zero-value prompt_tokens_details dict even when empty). 

249 completion_tokens_details: Reasoning/details count (relaykit 

250 serializes it even when empty). 

251 input_tokens: Source input count carried into chat emission. 

252 input_tokens_details: Raw input token details per ``input_tokens``. 

253 output_tokens: Source output count carried into chat emails. 

254 output_tokens_details: Legacy capture of the raw output details 

255 (never serialized; relaykit emits ``completion_tokens_details``). 

256 passthrough: Unknown fields preserved verbatim. 

257 """ 

258 

259 prompt_tokens: int = 0 

260 completion_tokens: int = 0 

261 total_tokens: int = 0 

262 prompt_tokens_details: dict[str, Any] | None = None 

263 completion_tokens_details: dict[str, Any] | None = None 

264 input_tokens: int = 0 

265 input_tokens_details: dict[str, Any] | None = None 

266 output_tokens: int = 0 

267 output_tokens_details: dict[str, Any] | None = None 

268 passthrough: dict[str, Any] = field(default_factory=dict) 

269 

270 def to_dict(self) -> dict[str, Any]: 

271 """Serialize to wire dict, mirroring relaykit's always-on usage.""" 

272 data: dict[str, Any] = { 

273 **self.passthrough, 

274 "prompt_tokens": self.prompt_tokens, 

275 "completion_tokens": self.completion_tokens, 

276 "total_tokens": self.total_tokens 

277 or (self.input_tokens + self.output_tokens), 

278 "prompt_tokens_details": self.prompt_tokens_details or {"cached_tokens": 0}, 

279 "completion_tokens_details": self.completion_tokens_details 

280 or {"reasoning_tokens": 0}, 

281 "input_tokens": self.input_tokens, 

282 "output_tokens": self.output_tokens, 

283 } 

284 if self.input_tokens_details is not None: 

285 data["input_tokens_details"] = self.input_tokens_details 

286 return data 

287 

288 @classmethod 

289 def from_dict(cls, data: dict[str, Any]) -> ResponsesUsage: 

290 """Build usage from a wire dict, capturing unknown keys.""" 

291 known = { 

292 "input_tokens", 

293 "input_tokens_details", 

294 "output_tokens", 

295 "output_tokens_details", 

296 "total_tokens", 

297 "prompt_tokens_details", 

298 "completion_tokens_details", 

299 "prompt_tokens", 

300 "completion_tokens", 

301 } 

302 return cls( 

303 prompt_tokens=data.get("prompt_tokens", 0), 

304 completion_tokens=data.get("completion_tokens", 0), 

305 total_tokens=data.get("total_tokens", 0), 

306 prompt_tokens_details=data.get("prompt_tokens_details"), 

307 completion_tokens_details=data.get("completion_tokens_details"), 

308 input_tokens=data.get("input_tokens", 0), 

309 input_tokens_details=data.get("input_tokens_details"), 

310 output_tokens=data.get("output_tokens", 0), 

311 output_tokens_details=data.get("output_tokens_details"), 

312 passthrough={k: v for k, v in data.items() if k not in known}, 

313 ) 

314 

315 

316@dataclass(frozen=True) 

317class ResponsesIncompleteDetails: 

318 """Why a Responses response is incomplete. 

319 

320 Attributes: 

321 reason: ``max_output_tokens``, ``content_filter``, etc. 

322 passthrough: Unknown fields preserved verbatim. 

323 """ 

324 

325 reason: str | None = None 

326 passthrough: dict[str, Any] = field(default_factory=dict) 

327 

328 def to_dict(self) -> dict[str, Any]: 

329 """Serialize to wire dict, omitting ``None`` optional fields.""" 

330 data: dict[str, Any] = {**self.passthrough} 

331 if self.reason is not None: 

332 data["reason"] = self.reason 

333 return data 

334 

335 @classmethod 

336 def from_dict(cls, data: dict[str, Any]) -> ResponsesIncompleteDetails: 

337 """Build details from a wire dict, capturing unknown keys.""" 

338 known = {"reason"} 

339 return cls( 

340 reason=data.get("reason"), 

341 passthrough={k: v for k, v in data.items() if k not in known}, 

342 ) 

343 

344 

345@dataclass(frozen=True) 

346class ResponsesResponse: 

347 """OpenAI Responses non-streamed response body. 

348 

349 Attributes: 

350 id: Response id. 

351 model: Model name. 

352 output: Output items (messages, function calls, reasoning). 

353 object: Object type (``response``). 

354 created_at: Unix timestamp. 

355 status: ``completed``, ``in_progress``, ``incomplete``, ``failed``. 

356 incomplete_details: Why the response is incomplete, or ``None``. 

357 error: Error object for failed responses, or ``None``. 

358 usage: Typed usage, or ``None``. 

359 passthrough: Unknown fields preserved verbatim. 

360 """ 

361 

362 id: str 

363 model: str 

364 output: list[ResponsesItem] 

365 object: str = "response" 

366 created_at: int = 0 

367 status: str | None = None 

368 incomplete_details: ResponsesIncompleteDetails | None = None 

369 error: dict[str, Any] | None = None 

370 usage: ResponsesUsage | None = None 

371 passthrough: dict[str, Any] = field(default_factory=dict) 

372 

373 def to_dict(self) -> dict[str, Any]: 

374 """Serialize to wire dict. 

375 

376 Relaykit's Go response structs always serialize the optional 

377 request-style fields (``instructions``, ``temperature``, ...) as 

378 explicit nulls/zeros, so this mirrors that shape. 

379 """ 

380 data: dict[str, Any] = { 

381 **self.passthrough, 

382 "id": self.id, 

383 "object": self.object, 

384 "created_at": self.created_at, 

385 "status": self.status or "completed", 

386 "instructions": None, 

387 "max_output_tokens": 0, 

388 "model": self.model, 

389 "output": [i.to_dict() for i in self.output], 

390 "parallel_tool_calls": False, 

391 "previous_response_id": None, 

392 "reasoning": None, 

393 "store": False, 

394 "temperature": 0, 

395 "tool_choice": None, 

396 "tools": None, 

397 "top_p": 0, 

398 "truncation": None, 

399 } 

400 if self.incomplete_details is not None: 

401 data["incomplete_details"] = self.incomplete_details.to_dict() 

402 if self.error is not None: 

403 data["error"] = self.error 

404 if self.usage is not None: 

405 data["usage"] = self.usage.to_dict() 

406 data["user"] = None 

407 data["metadata"] = None 

408 return data 

409 

410 @classmethod 

411 def from_dict(cls, data: dict[str, Any]) -> ResponsesResponse: 

412 """Build a response from a wire dict, capturing unknown keys. 

413 

414 Raises: 

415 RelayError: With code ``malformed_payload`` when ``id`` or 

416 ``model`` is absent. 

417 """ 

418 known = { 

419 "id", 

420 "object", 

421 "created_at", 

422 "model", 

423 "output", 

424 "status", 

425 "incomplete_details", 

426 "error", 

427 "usage", 

428 } 

429 incomplete = data.get("incomplete_details") 

430 usage = data.get("usage") 

431 return cls( 

432 id=require_field(data, "id"), 

433 object=data.get("object", "response"), 

434 created_at=data.get("created_at", 0), 

435 model=require_field(data, "model"), 

436 output=[ResponsesItem.from_dict(i) for i in data.get("output", [])], 

437 status=data.get("status"), 

438 incomplete_details=( 

439 ResponsesIncompleteDetails.from_dict(incomplete) 

440 if isinstance(incomplete, dict) 

441 else None 

442 ), 

443 error=data.get("error"), 

444 usage=ResponsesUsage.from_dict(usage) if isinstance(usage, dict) else None, 

445 passthrough={k: v for k, v in data.items() if k not in known}, 

446 ) 

447 

448 

449@dataclass(frozen=True) 

450class ResponsesEvent: 

451 """One SSE event in the Responses stream lifecycle. 

452 

453 The ``type`` field is the discriminator (``response.created``, 

454 ``response.output_item.added``, ``response.content_part.added``, 

455 ``response.output_text.delta``, ``response.output_text.done``, 

456 ``response.content_part.done``, ``response.output_item.done``, 

457 ``response.function_call_arguments.delta``, 

458 ``response.function_call_arguments.done``, 

459 ``response.reasoning_summary_text.delta``, 

460 ``response.reasoning_summary_text.done``, ``response.completed``, 

461 ``response.incomplete``, ``response.failed``, ``response.error``). 

462 

463 Attributes: 

464 type: Event type discriminator. 

465 sequence_number: Monotonic event sequence number. 

466 response: Snapshot on ``response.*`` lifecycle events, or ``None``. 

467 item: Item on ``output_item.*`` events, or ``None``. 

468 item_id: Id of the item an event describes, or ``None``. 

469 output_index: Output item index, or ``None``. 

470 content_index: Content part index, or ``None``. 

471 part: Raw content part on ``content_part.*`` events, or ``None``. 

472 delta: Text/arguments delta, or ``None``. 

473 error: Raw error payload, or ``None``. 

474 passthrough: Unknown fields preserved verbatim. 

475 """ 

476 

477 type: str 

478 sequence_number: int = 0 

479 response: ResponsesResponse | None = None 

480 item: ResponsesItem | None = None 

481 item_id: str | None = None 

482 output_index: int | None = None 

483 content_index: int | None = None 

484 part: dict[str, Any] | None = None 

485 delta: str | None = None 

486 error: dict[str, Any] | None = None 

487 passthrough: dict[str, Any] = field(default_factory=dict) 

488 

489 def to_dict(self) -> dict[str, Any]: 

490 """Serialize to wire dict as the Responses SSE envelope. 

491 

492 Relaykit frames each stream event as ``{"Type": <type>, "Payload": 

493 <fields>}`` — the ``Type`` is the SSE ``event`` name and ``Payload`` 

494 is the JSON body (which repeats the ``type`` discriminator). 

495 """ 

496 payload: dict[str, Any] = {**self.passthrough, "type": self.type} 

497 if self.sequence_number: 

498 payload["sequence_number"] = self.sequence_number 

499 if self.response is not None: 

500 payload["response"] = self.response.to_dict() 

501 if self.item is not None: 

502 payload["item"] = self.item.to_dict() 

503 if self.item_id is not None: 

504 payload["item_id"] = self.item_id 

505 if self.output_index is not None: 

506 payload["output_index"] = self.output_index 

507 if self.content_index is not None: 

508 payload["content_index"] = self.content_index 

509 if self.part is not None: 

510 payload["part"] = self.part 

511 if self.delta is not None: 

512 payload["delta"] = self.delta 

513 if self.error is not None: 

514 payload["error"] = self.error 

515 return {"Type": self.type, "Payload": payload} 

516 

517 @classmethod 

518 def from_dict(cls, data: dict[str, Any]) -> ResponsesEvent: 

519 """Build an event from a wire dict, capturing unknown keys. 

520 

521 Accepts both the SSE envelope (``Payload``) and the bare payload. 

522 """ 

523 source = data.get("Payload") 

524 if isinstance(source, dict): 

525 data = source 

526 known = { 

527 "type", 

528 "sequence_number", 

529 "response", 

530 "item", 

531 "item_id", 

532 "output_index", 

533 "content_index", 

534 "part", 

535 "delta", 

536 "error", 

537 } 

538 response = data.get("response") 

539 item = data.get("item") 

540 return cls( 

541 type=data.get("type", "response.in_progress"), 

542 sequence_number=data.get("sequence_number", 0), 

543 response=( 

544 ResponsesResponse.from_dict(response) 

545 if isinstance(response, dict) 

546 else None 

547 ), 

548 item=ResponsesItem.from_dict(item) if isinstance(item, dict) else None, 

549 item_id=data.get("item_id"), 

550 output_index=data.get("output_index"), 

551 content_index=data.get("content_index"), 

552 part=data.get("part"), 

553 delta=data.get("delta"), 

554 error=data.get("error"), 

555 passthrough={k: v for k, v in data.items() if k not in known}, 

556 )