Coverage for src/lilbee/server/chat_completions_api/models.py: 100%

124 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-14 11:46 +0000

1"""Pydantic models for the OpenAI Chat Completions wire shapes.""" 

2 

3from __future__ import annotations 

4 

5from enum import StrEnum 

6from typing import Annotated, Any, Literal 

7 

8from pydantic import BaseModel, ConfigDict, Field 

9 

10# Wire layer reuses the provider-layer enum so the two can never drift. 

11from lilbee.providers.base import FinishReason as FinishReason 

12 

13 

14class ToolChoiceMode(StrEnum): 

15 AUTO = "auto" 

16 NONE = "none" 

17 REQUIRED = "required" 

18 

19 

20class CompletionsTextContent(BaseModel): 

21 """Text part of a multi-part message content.""" 

22 

23 type: Literal["text"] 

24 text: str 

25 

26 

27class CompletionsImageUrl(BaseModel): 

28 url: str 

29 detail: Literal["auto", "low", "high"] | None = None 

30 

31 

32class CompletionsImageContent(BaseModel): 

33 """Image part of a multi-part message content.""" 

34 

35 type: Literal["image_url"] 

36 image_url: CompletionsImageUrl 

37 

38 

39CompletionsMessageContentPart = Annotated[ 

40 CompletionsTextContent | CompletionsImageContent, 

41 Field(discriminator="type"), 

42] 

43 

44 

45class CompletionsToolCallFunction(BaseModel): 

46 name: str 

47 arguments: str = "{}" 

48 

49 

50class CompletionsToolCall(BaseModel): 

51 """Assistant-side tool_call entry inside a request message.""" 

52 

53 id: str 

54 type: Literal["function"] = "function" 

55 function: CompletionsToolCallFunction 

56 

57 

58class CompletionsMessage(BaseModel): 

59 """One entry in the request ``messages`` list.""" 

60 

61 role: Literal["system", "user", "assistant", "tool"] 

62 content: str | list[CompletionsMessageContentPart] | None = None 

63 name: str | None = None 

64 tool_calls: list[CompletionsToolCall] | None = None 

65 tool_call_id: str | None = None 

66 

67 

68class CompletionsFunctionDef(BaseModel): 

69 name: str 

70 description: str | None = None 

71 parameters: dict[str, Any] = Field(default_factory=dict) 

72 

73 

74class CompletionsTool(BaseModel): 

75 type: Literal["function"] = "function" 

76 function: CompletionsFunctionDef 

77 

78 

79class CompletionsToolChoiceFunction(BaseModel): 

80 name: str 

81 

82 

83class CompletionsNamedToolChoice(BaseModel): 

84 """Explicit ``{type: "function", function: {name: ...}}`` tool_choice.""" 

85 

86 type: Literal["function"] 

87 function: CompletionsToolChoiceFunction 

88 

89 

90class StreamOptions(BaseModel): 

91 """OpenAI ``stream_options``. ``include_usage`` adds a final usage-only chunk.""" 

92 

93 include_usage: bool = False 

94 

95 

96class CompletionsRequest(BaseModel): 

97 """Top-level ``POST /v1/chat/completions`` request body. 

98 

99 OpenAI parameters fall into three groups on this surface: 

100 

101 - Honoured: ``model``, ``messages``, ``tools``, ``tool_choice``, 

102 ``temperature``, ``top_p``, ``top_k``, ``max_tokens``, ``stop``, ``seed``, 

103 ``frequency_penalty``, ``presence_penalty``, ``stream``, ``stream_options``. 

104 - Rejected with a 400: ``n`` greater than 1 (lilbee serves one choice). 

105 - Accepted but ignored (``extra="allow"`` keeps them off the parsed model and 

106 the route logs their keys at debug): ``n == 1``, ``response_format``, 

107 ``logprobs``, ``top_logprobs``, and any other unrecognised field. 

108 """ 

109 

110 model_config = ConfigDict(extra="allow") 

111 

112 model: str = Field(min_length=1) 

113 messages: list[CompletionsMessage] = Field(min_length=1) 

114 tools: list[CompletionsTool] | None = None 

115 tool_choice: ToolChoiceMode | CompletionsNamedToolChoice | None = None 

116 temperature: float | None = Field(default=None, ge=0.0, le=2.0) 

117 top_p: float | None = Field(default=None, ge=0.0, le=1.0) 

118 top_k: int | None = Field(default=None, ge=1) 

119 max_tokens: int | None = Field(default=None, ge=1) 

120 seed: int | None = None 

121 frequency_penalty: float | None = Field(default=None, ge=-2.0, le=2.0) 

122 presence_penalty: float | None = Field(default=None, ge=-2.0, le=2.0) 

123 n: int | None = Field(default=None, ge=1) 

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

125 stream: bool = False 

126 stream_options: StreamOptions | None = None 

127 

128 

129class CompletionsResponseToolCallFunction(BaseModel): 

130 name: str 

131 arguments: str 

132 

133 

134class CompletionsResponseToolCall(BaseModel): 

135 id: str 

136 type: Literal["function"] = "function" 

137 function: CompletionsResponseToolCallFunction 

138 

139 

140class CompletionsResponseMessage(BaseModel): 

141 role: Literal["assistant"] = "assistant" 

142 content: str | None = None 

143 # A reasoning model's thinking, reported separately so ``content`` stays clean. 

144 reasoning_content: str | None = None 

145 tool_calls: list[CompletionsResponseToolCall] | None = None 

146 

147 

148class CompletionsResponseChoice(BaseModel): 

149 index: int = 0 

150 message: CompletionsResponseMessage 

151 finish_reason: FinishReason 

152 

153 

154class CompletionsUsage(BaseModel): 

155 prompt_tokens: int = 0 

156 completion_tokens: int = 0 

157 total_tokens: int = 0 

158 

159 

160class CompletionsResponse(BaseModel): 

161 """Non-streaming ``/v1/chat/completions`` response body.""" 

162 

163 id: str 

164 object: Literal["chat.completion"] = "chat.completion" 

165 created: int 

166 model: str 

167 choices: list[CompletionsResponseChoice] 

168 usage: CompletionsUsage 

169 

170 

171class CompletionsStreamToolCallFunction(BaseModel): 

172 name: str | None = None 

173 arguments: str | None = None 

174 

175 

176class CompletionsStreamToolCall(BaseModel): 

177 index: int 

178 id: str | None = None 

179 type: Literal["function"] | None = None 

180 function: CompletionsStreamToolCallFunction | None = None 

181 

182 

183class CompletionsStreamDelta(BaseModel): 

184 role: Literal["assistant"] | None = None 

185 content: str | None = None 

186 reasoning_content: str | None = None 

187 tool_calls: list[CompletionsStreamToolCall] | None = None 

188 

189 

190class CompletionsStreamChoice(BaseModel): 

191 index: int = 0 

192 delta: CompletionsStreamDelta 

193 finish_reason: FinishReason | None = None 

194 

195 

196class CompletionsStreamChunk(BaseModel): 

197 """Single SSE frame from streaming ``/v1/chat/completions``. 

198 

199 The final frame (when usage is known) carries an empty ``choices`` list and a 

200 populated ``usage`` block, matching OpenAI's ``stream_options.include_usage``. 

201 """ 

202 

203 id: str 

204 object: Literal["chat.completion.chunk"] = "chat.completion.chunk" 

205 created: int 

206 model: str 

207 choices: list[CompletionsStreamChoice] 

208 usage: CompletionsUsage | None = None 

209 

210 

211class ModelEntry(BaseModel): 

212 id: str 

213 object: Literal["model"] = "model" 

214 owned_by: str = "lilbee" 

215 created: int 

216 context_window: int | None = None 

217 """The context the active chat engine serves, so a client can trim history to 

218 fit. None when the engine is not up yet or the window is unknown.""" 

219 

220 

221class ModelsListResponse(BaseModel): 

222 """``GET /v1/models`` response envelope.""" 

223 

224 object: Literal["list"] = "list" 

225 data: list[ModelEntry]