Coverage for src / lexigram / contracts / ai / relay / dto / openai_chat.py: 44%

186 statements  

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

1"""OpenAI Chat Completions 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 "OpenAIChatChoice", 

12 "OpenAIChatMessage", 

13 "OpenAIChatRequest", 

14 "OpenAIChatResponse", 

15 "OpenAIChatStreamChoice", 

16 "OpenAIChatStreamChunk", 

17 "OpenAIChatStreamDelta", 

18] 

19 

20 

21@dataclass(frozen=True) 

22class OpenAIChatMessage: 

23 """A message in OpenAI Chat Completions format. 

24 

25 Attributes: 

26 role: ``system``, ``user``, ``assistant``, ``tool``, ``function``. 

27 content: String content, or ``None`` for tool-call turns. 

28 name: Optional author name. 

29 tool_call_id: Id of the tool call this message answers. 

30 tool_calls: Tool calls on an assistant message, or ``None``. 

31 passthrough: Unknown fields preserved verbatim. 

32 """ 

33 

34 role: str 

35 content: str | None = None 

36 name: str | None = None 

37 tool_call_id: str | None = None 

38 tool_calls: list[dict[str, Any]] | None = None 

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

40 

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

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

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

44 

45 data["content"] = self.content 

46 if self.name is not None: 

47 data["name"] = self.name 

48 if self.tool_call_id is not None: 

49 data["tool_call_id"] = self.tool_call_id 

50 if self.tool_calls is not None: 

51 data["tool_calls"] = self.tool_calls 

52 return data 

53 

54 @classmethod 

55 def from_dict(cls, data: dict[str, Any]) -> OpenAIChatMessage: 

56 """Build a message from a wire dict, capturing unknown keys.""" 

57 known = {"role", "content", "name", "tool_call_id", "tool_calls"} 

58 return cls( 

59 role=data.get("role", "user"), 

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

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

62 tool_call_id=data.get("tool_call_id"), 

63 tool_calls=data.get("tool_calls"), 

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

65 ) 

66 

67 

68@dataclass(frozen=True) 

69class OpenAIChatRequest: 

70 """OpenAI Chat Completions request body. 

71 

72 Attributes: 

73 model: Model name. 

74 messages: Message list. 

75 temperature: Sampling temperature (``None`` = omitted upstream). 

76 top_p: Nucleus sampling (``None`` = omitted upstream). 

77 max_tokens: Max output tokens (``None`` = omitted upstream). 

78 max_completion_tokens: Max completion tokens (``None`` = omitted 

79 upstream; normalized against ``max_tokens`` by the mapper). 

80 stream: Whether the caller wants a stream. 

81 stream_options: ``{"include_usage": bool}`` or ``None``. 

82 tools: Raw tool definitions, or ``None``. 

83 tool_choice: Tool choice directive, or ``None``. 

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

85 stop: Stop string or list of strings, or ``None``. 

86 response_format: JSON-mode config, or ``None``. 

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

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

89 passthrough: Unknown fields preserved verbatim. 

90 """ 

91 

92 model: str 

93 messages: list[OpenAIChatMessage] 

94 temperature: float | None = None 

95 top_p: float | None = None 

96 max_tokens: int | None = None 

97 max_completion_tokens: int | None = None 

98 stream: bool = False 

99 stream_options: dict[str, Any] | None = None 

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

101 tool_choice: Any | None = None 

102 parallel_tool_calls: bool | None = None 

103 stop: str | list[str] | None = None 

104 response_format: dict[str, Any] | None = None 

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

106 service_tier: str | None = None 

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

108 

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

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

111 data: dict[str, Any] = { 

112 **self.passthrough, 

113 "model": self.model, 

114 "messages": [m.to_dict() for m in self.messages], 

115 } 

116 if self.temperature is not None: 

117 data["temperature"] = self.temperature 

118 if self.top_p is not None: 

119 data["top_p"] = self.top_p 

120 if self.max_tokens is not None: 

121 data["max_tokens"] = self.max_tokens 

122 if self.max_completion_tokens is not None: 

123 data["max_completion_tokens"] = self.max_completion_tokens 

124 data["stream"] = self.stream 

125 if self.stream_options is not None: 

126 data["stream_options"] = self.stream_options 

127 if self.tools is not None: 

128 data["tools"] = self.tools 

129 if self.tool_choice is not None: 

130 data["tool_choice"] = self.tool_choice 

131 if self.parallel_tool_calls is not None: 

132 data["parallel_tool_calls"] = self.parallel_tool_calls 

133 if self.stop is not None: 

134 data["stop"] = self.stop 

135 if self.response_format is not None: 

136 data["response_format"] = self.response_format 

137 if self.reasoning is not None: 

138 data["reasoning"] = self.reasoning 

139 if self.service_tier is not None: 

140 data["service_tier"] = self.service_tier 

141 return data 

142 

143 @classmethod 

144 def from_dict(cls, data: dict[str, Any]) -> OpenAIChatRequest: 

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

146 

147 Raises: 

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

149 is absent. 

150 """ 

151 known = { 

152 "model", 

153 "messages", 

154 "temperature", 

155 "top_p", 

156 "max_tokens", 

157 "max_completion_tokens", 

158 "stream", 

159 "stream_options", 

160 "tools", 

161 "tool_choice", 

162 "parallel_tool_calls", 

163 "stop", 

164 "response_format", 

165 "reasoning", 

166 "service_tier", 

167 } 

168 return cls( 

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

170 messages=[OpenAIChatMessage.from_dict(m) for m in data.get("messages", [])], 

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

172 top_p=data.get("top_p"), 

173 max_tokens=data.get("max_tokens"), 

174 max_completion_tokens=data.get("max_completion_tokens"), 

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

176 stream_options=data.get("stream_options"), 

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

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

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

180 stop=data.get("stop"), 

181 response_format=data.get("response_format"), 

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

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

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

185 ) 

186 

187 

188@dataclass(frozen=True) 

189class OpenAIChatChoice: 

190 """One choice in a non-streamed completion response. 

191 

192 Attributes: 

193 index: Choice index. 

194 message: Assistant message, or ``None``. 

195 finish_reason: ``stop``, ``length``, ``tool_calls``, etc. 

196 logprobs: Token log-probability info, or ``None``. 

197 passthrough: Unknown fields preserved verbatim. 

198 """ 

199 

200 index: int = 0 

201 message: OpenAIChatMessage | None = None 

202 finish_reason: str | None = None 

203 logprobs: Any | None = None 

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

205 

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

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

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

209 if self.message is not None: 

210 data["message"] = self.message.to_dict() 

211 if self.finish_reason is not None: 

212 data["finish_reason"] = self.finish_reason 

213 if self.logprobs is not None: 

214 data["logprobs"] = self.logprobs 

215 return data 

216 

217 @classmethod 

218 def from_dict(cls, data: dict[str, Any]) -> OpenAIChatChoice: 

219 """Build a choice from a wire dict, capturing unknown keys.""" 

220 known = {"index", "message", "finish_reason", "logprobs"} 

221 message = data.get("message") 

222 return cls( 

223 index=data.get("index", 0), 

224 message=OpenAIChatMessage.from_dict(message) 

225 if isinstance(message, dict) 

226 else None, 

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

228 logprobs=data.get("logprobs"), 

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

230 ) 

231 

232 

233@dataclass(frozen=True) 

234class OpenAIChatResponse: 

235 """Non-streamed Chat Completions response body. 

236 

237 Attributes: 

238 id: Completion id. 

239 model: Model name. 

240 choices: Completion choices. 

241 object: Object type (``chat.completion``). 

242 created: Unix timestamp. 

243 usage: Raw usage dict, or ``None``. 

244 system_fingerprint: System fingerprint, or ``None``. 

245 passthrough: Unknown fields preserved verbatim. 

246 """ 

247 

248 id: str 

249 model: str 

250 choices: list[OpenAIChatChoice] 

251 object: str = "chat.completion" 

252 created: int = 0 

253 usage: dict[str, Any] | None = None 

254 system_fingerprint: str | None = None 

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

256 

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

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

259 data: dict[str, Any] = { 

260 **self.passthrough, 

261 "id": self.id, 

262 "object": self.object, 

263 "created": self.created, 

264 "model": self.model, 

265 "choices": [c.to_dict() for c in self.choices], 

266 } 

267 if self.usage is not None: 

268 data["usage"] = self.usage 

269 if self.system_fingerprint is not None: 

270 data["system_fingerprint"] = self.system_fingerprint 

271 return data 

272 

273 @classmethod 

274 def from_dict(cls, data: dict[str, Any]) -> OpenAIChatResponse: 

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

276 

277 Raises: 

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

279 ``model`` is absent. 

280 """ 

281 known = { 

282 "id", 

283 "object", 

284 "created", 

285 "model", 

286 "choices", 

287 "usage", 

288 "system_fingerprint", 

289 } 

290 return cls( 

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

292 object=data.get("object", "chat.completion"), 

293 created=data.get("created", 0), 

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

295 choices=[OpenAIChatChoice.from_dict(c) for c in data.get("choices", [])], 

296 usage=data.get("usage"), 

297 system_fingerprint=data.get("system_fingerprint"), 

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

299 ) 

300 

301 

302@dataclass(frozen=True) 

303class OpenAIChatStreamDelta: 

304 """Delta payload inside one stream choice. 

305 

306 Attributes: 

307 role: Role announcement (``assistant``), or ``None``. 

308 content: Text delta, or ``None``. 

309 reasoning_content: Reasoning text delta, or ``None``. 

310 tool_calls: Partial tool-call fragments (raw wire shape), or ``None``. 

311 refusal: Refusal text delta, or ``None``. 

312 passthrough: Unknown fields preserved verbatim. 

313 """ 

314 

315 role: str | None = None 

316 content: str | None = None 

317 reasoning_content: str | None = None 

318 tool_calls: list[dict[str, Any]] | None = None 

319 refusal: str | None = None 

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

321 

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

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

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

325 if self.role is not None: 

326 data["role"] = self.role 

327 if self.content is not None: 

328 data["content"] = self.content 

329 if self.reasoning_content is not None: 

330 data["reasoning_content"] = self.reasoning_content 

331 if self.tool_calls is not None: 

332 data["tool_calls"] = self.tool_calls 

333 if self.refusal is not None: 

334 data["refusal"] = self.refusal 

335 return data 

336 

337 @classmethod 

338 def from_dict(cls, data: dict[str, Any]) -> OpenAIChatStreamDelta: 

339 """Build a delta from a wire dict, capturing unknown keys.""" 

340 known = {"role", "content", "reasoning_content", "tool_calls", "refusal"} 

341 return cls( 

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

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

344 reasoning_content=data.get("reasoning_content"), 

345 tool_calls=data.get("tool_calls"), 

346 refusal=data.get("refusal"), 

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

348 ) 

349 

350 

351@dataclass(frozen=True) 

352class OpenAIChatStreamChoice: 

353 """One choice inside a stream chunk. 

354 

355 Attributes: 

356 index: Choice index. 

357 delta: Delta payload, or ``None``. 

358 finish_reason: Terminal finish reason, or ``None``. 

359 logprobs: Token log-probability info, or ``None``. 

360 passthrough: Unknown fields preserved verbatim. 

361 """ 

362 

363 index: int = 0 

364 delta: OpenAIChatStreamDelta | None = None 

365 finish_reason: str | None = None 

366 logprobs: Any | None = None 

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

368 

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

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

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

372 if self.delta is not None: 

373 data["delta"] = self.delta.to_dict() 

374 if self.finish_reason is not None: 

375 data["finish_reason"] = self.finish_reason 

376 if self.logprobs is not None: 

377 data["logprobs"] = self.logprobs 

378 return data 

379 

380 @classmethod 

381 def from_dict(cls, data: dict[str, Any]) -> OpenAIChatStreamChoice: 

382 """Build a choice from a wire dict, capturing unknown keys.""" 

383 known = {"index", "delta", "finish_reason", "logprobs"} 

384 delta = data.get("delta") 

385 return cls( 

386 index=data.get("index", 0), 

387 delta=OpenAIChatStreamDelta.from_dict(delta) 

388 if isinstance(delta, dict) 

389 else None, 

390 finish_reason=data.get("finish_reason"), 

391 logprobs=data.get("logprobs"), 

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

393 ) 

394 

395 

396@dataclass(frozen=True) 

397class OpenAIChatStreamChunk: 

398 """One SSE chunk of a streamed Chat Completions response. 

399 

400 Attributes: 

401 id: Completion id. 

402 model: Model name. 

403 choices: Stream choices. 

404 object: Object type (``chat.completion.chunk``). 

405 created: Unix timestamp. 

406 usage: Final usage dict (usage-only terminal chunk), or ``None``. 

407 system_fingerprint: System fingerprint, or ``None``. 

408 passthrough: Unknown fields preserved verbatim. 

409 """ 

410 

411 id: str 

412 model: str 

413 choices: list[OpenAIChatStreamChoice] 

414 object: str = "chat.completion.chunk" 

415 created: int = 0 

416 usage: dict[str, Any] | None = None 

417 system_fingerprint: str | None = None 

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

419 

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

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

422 data: dict[str, Any] = { 

423 **self.passthrough, 

424 "id": self.id, 

425 "object": self.object, 

426 "created": self.created, 

427 "model": self.model, 

428 "choices": [c.to_dict() for c in self.choices], 

429 } 

430 if self.usage is not None: 

431 data["usage"] = self.usage 

432 if self.system_fingerprint is not None: 

433 data["system_fingerprint"] = self.system_fingerprint 

434 return data 

435 

436 @classmethod 

437 def from_dict(cls, data: dict[str, Any]) -> OpenAIChatStreamChunk: 

438 """Build a chunk from a wire dict, capturing unknown keys. 

439 

440 Raises: 

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

442 ``model`` is absent. 

443 """ 

444 known = { 

445 "id", 

446 "object", 

447 "created", 

448 "model", 

449 "choices", 

450 "usage", 

451 "system_fingerprint", 

452 } 

453 return cls( 

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

455 object=data.get("object", "chat.completion.chunk"), 

456 created=data.get("created", 0), 

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

458 choices=[ 

459 OpenAIChatStreamChoice.from_dict(c) for c in data.get("choices", []) 

460 ], 

461 usage=data.get("usage"), 

462 system_fingerprint=data.get("system_fingerprint"), 

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

464 )