codex-lb 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,534 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from collections.abc import AsyncIterator, Iterable, Mapping
6
+ from dataclasses import dataclass, field
7
+ from typing import cast
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+ from app.core.errors import openai_error
12
+ from app.core.types import JsonValue
13
+
14
+
15
+ class ChatToolCallFunction(BaseModel):
16
+ model_config = ConfigDict(extra="forbid")
17
+
18
+ name: str | None = None
19
+ arguments: str | None = None
20
+
21
+
22
+ class ChatToolCallDelta(BaseModel):
23
+ model_config = ConfigDict(extra="forbid")
24
+
25
+ index: int
26
+ id: str | None = None
27
+ type: str = "function"
28
+ function: ChatToolCallFunction | None = None
29
+
30
+
31
+ class ChatChunkDelta(BaseModel):
32
+ model_config = ConfigDict(extra="forbid")
33
+
34
+ role: str | None = None
35
+ content: str | None = None
36
+ tool_calls: list[ChatToolCallDelta] | None = None
37
+
38
+
39
+ class ChatChunkChoice(BaseModel):
40
+ model_config = ConfigDict(extra="forbid")
41
+
42
+ index: int
43
+ delta: ChatChunkDelta
44
+ finish_reason: str | None = None
45
+
46
+
47
+ class ChatCompletionChunk(BaseModel):
48
+ model_config = ConfigDict(extra="forbid")
49
+
50
+ id: str
51
+ object: str = "chat.completion.chunk"
52
+ created: int
53
+ model: str
54
+ choices: list[ChatChunkChoice]
55
+
56
+
57
+ class ChatMessageToolCall(BaseModel):
58
+ model_config = ConfigDict(extra="forbid")
59
+
60
+ id: str | None = None
61
+ type: str = "function"
62
+ function: ChatToolCallFunction | None = None
63
+
64
+
65
+ class ChatCompletionMessage(BaseModel):
66
+ model_config = ConfigDict(extra="forbid")
67
+
68
+ role: str
69
+ content: str | None = None
70
+ tool_calls: list[ChatMessageToolCall] | None = None
71
+
72
+
73
+ class ChatCompletionChoice(BaseModel):
74
+ model_config = ConfigDict(extra="forbid")
75
+
76
+ index: int
77
+ message: ChatCompletionMessage
78
+ finish_reason: str | None = None
79
+
80
+
81
+ class ChatCompletionUsage(BaseModel):
82
+ model_config = ConfigDict(extra="forbid")
83
+
84
+ prompt_tokens: int | None = None
85
+ completion_tokens: int | None = None
86
+ total_tokens: int | None = None
87
+
88
+
89
+ class ChatCompletion(BaseModel):
90
+ model_config = ConfigDict(extra="forbid")
91
+
92
+ id: str
93
+ object: str = "chat.completion"
94
+ created: int
95
+ model: str
96
+ choices: list[ChatCompletionChoice]
97
+ usage: ChatCompletionUsage | None = None
98
+
99
+
100
+ @dataclass
101
+ class ToolCallIndex:
102
+ indexes: dict[str, int] = field(default_factory=dict)
103
+ next_index: int = 0
104
+
105
+ def index_for(self, call_id: str | None, name: str | None) -> int:
106
+ key = _tool_call_key(call_id, name)
107
+ if key is None:
108
+ return 0
109
+ if key not in self.indexes:
110
+ self.indexes[key] = self.next_index
111
+ self.next_index += 1
112
+ return self.indexes[key]
113
+
114
+
115
+ @dataclass
116
+ class _ChatChunkState:
117
+ tool_index: ToolCallIndex = field(default_factory=ToolCallIndex)
118
+ saw_tool_call: bool = False
119
+ sent_role: bool = False
120
+
121
+
122
+ @dataclass
123
+ class ToolCallDelta:
124
+ index: int
125
+ call_id: str | None
126
+ name: str | None
127
+ arguments: str | None
128
+ tool_type: str | None
129
+
130
+ def to_chunk_call(self) -> ChatToolCallDelta:
131
+ function = _build_tool_call_function(self.name, self.arguments)
132
+ return ChatToolCallDelta(
133
+ index=self.index,
134
+ id=self.call_id,
135
+ type=self.tool_type or "function",
136
+ function=function,
137
+ )
138
+
139
+
140
+ @dataclass
141
+ class ToolCallState:
142
+ index: int
143
+ call_id: str | None = None
144
+ name: str | None = None
145
+ arguments: str = ""
146
+ tool_type: str = "function"
147
+
148
+ def apply_delta(self, delta: ToolCallDelta) -> None:
149
+ if delta.call_id:
150
+ self.call_id = delta.call_id
151
+ if delta.name:
152
+ self.name = delta.name
153
+ if delta.arguments:
154
+ self.arguments += delta.arguments
155
+ if delta.tool_type:
156
+ self.tool_type = delta.tool_type
157
+
158
+ def to_message_tool_call(self) -> ChatMessageToolCall | None:
159
+ function = _build_tool_call_function(self.name, self.arguments or None)
160
+ if self.call_id is None and function is None:
161
+ return None
162
+ return ChatMessageToolCall(
163
+ id=self.call_id,
164
+ type=self.tool_type or "function",
165
+ function=function,
166
+ )
167
+
168
+
169
+ def _build_tool_call_function(name: str | None, arguments: str | None) -> ChatToolCallFunction | None:
170
+ if name is None and arguments is None:
171
+ return None
172
+ return ChatToolCallFunction(name=name, arguments=arguments)
173
+
174
+
175
+ def _parse_data(line: str) -> dict[str, JsonValue] | None:
176
+ if line.startswith("data:"):
177
+ data = line[5:].strip()
178
+ if not data or data == "[DONE]":
179
+ return None
180
+ try:
181
+ payload = json.loads(data)
182
+ except json.JSONDecodeError:
183
+ return None
184
+ if isinstance(payload, dict):
185
+ return cast(dict[str, JsonValue], payload)
186
+ return None
187
+
188
+
189
+ def iter_chat_chunks(
190
+ lines: Iterable[str],
191
+ model: str,
192
+ *,
193
+ created: int | None = None,
194
+ state: _ChatChunkState | None = None,
195
+ ) -> Iterable[str]:
196
+ created = created or int(time.time())
197
+ state = state or _ChatChunkState()
198
+ for line in lines:
199
+ payload = _parse_data(line)
200
+ if not payload:
201
+ continue
202
+ event_type = payload.get("type")
203
+ if event_type == "response.output_text.delta":
204
+ delta = payload.get("delta")
205
+ role = None
206
+ if not state.sent_role:
207
+ role = "assistant"
208
+ chunk = ChatCompletionChunk(
209
+ id="chatcmpl_temp",
210
+ created=created,
211
+ model=model,
212
+ choices=[
213
+ ChatChunkChoice(
214
+ index=0,
215
+ delta=ChatChunkDelta(
216
+ role=role,
217
+ content=delta if isinstance(delta, str) else None,
218
+ ),
219
+ finish_reason=None,
220
+ )
221
+ ],
222
+ )
223
+ yield _dump_chunk(chunk)
224
+ if role is not None:
225
+ state.sent_role = True
226
+ tool_delta = _tool_call_delta_from_payload(payload, state.tool_index)
227
+ if tool_delta is not None:
228
+ state.saw_tool_call = True
229
+ role = None
230
+ if not state.sent_role:
231
+ role = "assistant"
232
+ chunk = ChatCompletionChunk(
233
+ id="chatcmpl_temp",
234
+ created=created,
235
+ model=model,
236
+ choices=[
237
+ ChatChunkChoice(
238
+ index=0,
239
+ delta=ChatChunkDelta(
240
+ role=role,
241
+ tool_calls=[tool_delta.to_chunk_call()],
242
+ ),
243
+ finish_reason=None,
244
+ )
245
+ ],
246
+ )
247
+ yield _dump_chunk(chunk)
248
+ if role is not None:
249
+ state.sent_role = True
250
+ if event_type in ("response.failed", "error"):
251
+ error = None
252
+ if event_type == "response.failed":
253
+ response = payload.get("response")
254
+ if isinstance(response, dict):
255
+ maybe_error = response.get("error")
256
+ if isinstance(maybe_error, dict):
257
+ error = maybe_error
258
+ else:
259
+ maybe_error = payload.get("error")
260
+ if isinstance(maybe_error, dict):
261
+ error = maybe_error
262
+ if error is not None:
263
+ error_payload = {"error": error}
264
+ yield _dump_sse(error_payload)
265
+ yield "data: [DONE]\n\n"
266
+ return
267
+ if event_type == "response.completed":
268
+ finish_reason = "tool_calls" if state.saw_tool_call else "stop"
269
+ done = ChatCompletionChunk(
270
+ id="chatcmpl_temp",
271
+ created=created,
272
+ model=model,
273
+ choices=[
274
+ ChatChunkChoice(
275
+ index=0,
276
+ delta=ChatChunkDelta(),
277
+ finish_reason=finish_reason,
278
+ )
279
+ ],
280
+ )
281
+ yield _dump_chunk(done)
282
+ yield "data: [DONE]\n\n"
283
+ return
284
+
285
+
286
+ async def stream_chat_chunks(stream: AsyncIterator[str], model: str) -> AsyncIterator[str]:
287
+ created = int(time.time())
288
+ state = _ChatChunkState()
289
+ async for line in stream:
290
+ for chunk in iter_chat_chunks([line], model=model, created=created, state=state):
291
+ yield chunk
292
+ if chunk.strip() == "data: [DONE]":
293
+ return
294
+
295
+
296
+ async def collect_chat_completion(stream: AsyncIterator[str], model: str) -> dict[str, JsonValue]:
297
+ created = int(time.time())
298
+ content_parts: list[str] = []
299
+ response_id: str | None = None
300
+ usage: dict[str, JsonValue] | None = None
301
+ tool_index = ToolCallIndex()
302
+ tool_calls: list[ToolCallState] = []
303
+
304
+ async for line in stream:
305
+ payload = _parse_data(line)
306
+ if not payload:
307
+ continue
308
+ event_type = payload.get("type")
309
+ if event_type == "response.output_text.delta":
310
+ delta = payload.get("delta")
311
+ if isinstance(delta, str):
312
+ content_parts.append(delta)
313
+ tool_delta = _tool_call_delta_from_payload(payload, tool_index)
314
+ if tool_delta is not None:
315
+ _merge_tool_call_delta(tool_calls, tool_delta)
316
+ if event_type in ("response.failed", "error"):
317
+ error = None
318
+ if event_type == "response.failed":
319
+ response = payload.get("response")
320
+ if isinstance(response, dict):
321
+ maybe_error = response.get("error")
322
+ if isinstance(maybe_error, dict):
323
+ error = maybe_error
324
+ else:
325
+ maybe_error = payload.get("error")
326
+ if isinstance(maybe_error, dict):
327
+ error = maybe_error
328
+ if error is not None:
329
+ return {"error": error}
330
+ return cast(dict[str, JsonValue], openai_error("upstream_error", "Upstream error"))
331
+ if event_type == "response.completed":
332
+ response = payload.get("response")
333
+ if isinstance(response, dict):
334
+ response_id_value = response.get("id")
335
+ if isinstance(response_id_value, str):
336
+ response_id = response_id_value
337
+ usage_value = response.get("usage")
338
+ if isinstance(usage_value, dict):
339
+ usage = usage_value
340
+
341
+ message_content = "".join(content_parts)
342
+ message_tool_calls = _compact_tool_calls(tool_calls)
343
+ has_tool_calls = bool(message_tool_calls)
344
+ message = ChatCompletionMessage(
345
+ role="assistant",
346
+ content=message_content if message_content or not has_tool_calls else None,
347
+ tool_calls=message_tool_calls or None,
348
+ )
349
+ choice = ChatCompletionChoice(
350
+ index=0,
351
+ message=message,
352
+ finish_reason="tool_calls" if has_tool_calls else "stop",
353
+ )
354
+ completion = ChatCompletion(
355
+ id=response_id or "chatcmpl_temp",
356
+ created=created,
357
+ model=model,
358
+ choices=[choice],
359
+ usage=_map_usage(usage),
360
+ )
361
+ return _dump_completion(completion)
362
+
363
+
364
+ def _map_usage(usage: dict[str, JsonValue] | None) -> ChatCompletionUsage | None:
365
+ if not usage:
366
+ return None
367
+ prompt_tokens = usage.get("input_tokens")
368
+ completion_tokens = usage.get("output_tokens")
369
+ total_tokens = usage.get("total_tokens")
370
+ if not isinstance(prompt_tokens, int):
371
+ prompt_tokens = None
372
+ if not isinstance(completion_tokens, int):
373
+ completion_tokens = None
374
+ if not isinstance(total_tokens, int):
375
+ total_tokens = None
376
+ if prompt_tokens is None and completion_tokens is None and total_tokens is None:
377
+ return None
378
+ return ChatCompletionUsage(
379
+ prompt_tokens=prompt_tokens,
380
+ completion_tokens=completion_tokens,
381
+ total_tokens=total_tokens,
382
+ )
383
+
384
+
385
+ def _dump_chunk(chunk: ChatCompletionChunk) -> str:
386
+ payload = chunk.model_dump(mode="json", exclude_none=True)
387
+ return _dump_sse(payload)
388
+
389
+
390
+ def _dump_completion(completion: ChatCompletion) -> dict[str, JsonValue]:
391
+ payload = completion.model_dump(mode="json", exclude_none=True)
392
+ return cast(dict[str, JsonValue], payload)
393
+
394
+
395
+ def _dump_sse(payload: dict[str, JsonValue]) -> str:
396
+ return f"data: {json.dumps(payload)}\n\n"
397
+
398
+
399
+ def _tool_call_delta_from_payload(payload: Mapping[str, JsonValue], indexer: ToolCallIndex) -> ToolCallDelta | None:
400
+ if not _is_tool_call_event(payload):
401
+ return None
402
+ fields = _extract_tool_call_fields(payload)
403
+ if fields is None:
404
+ return None
405
+ call_id, name, arguments, tool_type = fields
406
+ index = indexer.index_for(call_id, name)
407
+ return ToolCallDelta(
408
+ index=index,
409
+ call_id=call_id,
410
+ name=name,
411
+ arguments=arguments,
412
+ tool_type=tool_type,
413
+ )
414
+
415
+
416
+ def _is_tool_call_event(payload: Mapping[str, JsonValue]) -> bool:
417
+ event_type = payload.get("type")
418
+ if isinstance(event_type, str) and ("tool_call" in event_type or "function_call" in event_type):
419
+ return True
420
+ item = _as_mapping(payload.get("item"))
421
+ if item is not None:
422
+ item_type = item.get("type")
423
+ if isinstance(item_type, str) and ("tool" in item_type or "function" in item_type):
424
+ return True
425
+ if any(key in item for key in ("call_id", "tool_call_id", "arguments", "function", "name")):
426
+ return True
427
+ if any(key in payload for key in ("call_id", "tool_call_id")):
428
+ return True
429
+ if "arguments" in payload and ("name" in payload or "function" in payload):
430
+ return True
431
+ return False
432
+
433
+
434
+ def _extract_tool_call_fields(
435
+ payload: Mapping[str, JsonValue],
436
+ ) -> tuple[str | None, str | None, str | None, str | None] | None:
437
+ candidate = _select_tool_call_candidate(payload)
438
+ delta = candidate.get("delta")
439
+ delta_map = _as_mapping(delta)
440
+ delta_text = delta if isinstance(delta, str) else None
441
+
442
+ call_id = _first_str(
443
+ candidate.get("call_id"),
444
+ candidate.get("tool_call_id"),
445
+ candidate.get("id"),
446
+ )
447
+ if call_id is None and delta_map is not None:
448
+ call_id = _first_str(
449
+ delta_map.get("id"),
450
+ delta_map.get("call_id"),
451
+ delta_map.get("tool_call_id"),
452
+ )
453
+
454
+ name = _first_str(candidate.get("name"), candidate.get("tool_name"))
455
+ if name is None and delta_map is not None:
456
+ name = _first_str(delta_map.get("name"))
457
+ if name is None:
458
+ function = _as_mapping(candidate.get("function"))
459
+ if function is not None:
460
+ name = _first_str(function.get("name"))
461
+ if name is None and delta_map is not None:
462
+ function = _as_mapping(delta_map.get("function"))
463
+ if function is not None:
464
+ name = _first_str(function.get("name"))
465
+
466
+ arguments = None
467
+ if isinstance(candidate.get("arguments"), str):
468
+ arguments = cast(str, candidate.get("arguments"))
469
+ if arguments is None and isinstance(delta_text, str):
470
+ arguments = delta_text
471
+ if arguments is None and delta_map is not None:
472
+ if isinstance(delta_map.get("arguments"), str):
473
+ arguments = cast(str, delta_map.get("arguments"))
474
+ else:
475
+ function = _as_mapping(delta_map.get("function"))
476
+ if function is not None and isinstance(function.get("arguments"), str):
477
+ arguments = cast(str, function.get("arguments"))
478
+
479
+ tool_type = _first_str(candidate.get("tool_type"), candidate.get("type"))
480
+ if tool_type and tool_type.startswith("response."):
481
+ tool_type = None
482
+ if tool_type in ("tool_call", "function_call"):
483
+ tool_type = "function"
484
+
485
+ if call_id is None and name is None and arguments is None:
486
+ return None
487
+ return call_id, name, arguments, tool_type
488
+
489
+
490
+ def _select_tool_call_candidate(payload: Mapping[str, JsonValue]) -> Mapping[str, JsonValue]:
491
+ item = _as_mapping(payload.get("item"))
492
+ if item is not None:
493
+ item_type = item.get("type")
494
+ if isinstance(item_type, str) and ("tool" in item_type or "function" in item_type):
495
+ return item
496
+ if any(key in item for key in ("call_id", "tool_call_id", "arguments", "function", "name")):
497
+ return item
498
+ return payload
499
+
500
+
501
+ def _tool_call_key(call_id: str | None, name: str | None) -> str | None:
502
+ if call_id:
503
+ return f"id:{call_id}"
504
+ if name:
505
+ return f"name:{name}"
506
+ return None
507
+
508
+
509
+ def _as_mapping(value: JsonValue) -> Mapping[str, JsonValue] | None:
510
+ if isinstance(value, Mapping):
511
+ return cast(Mapping[str, JsonValue], value)
512
+ return None
513
+
514
+
515
+ def _first_str(*values: object) -> str | None:
516
+ for value in values:
517
+ if isinstance(value, str) and value:
518
+ return value
519
+ return None
520
+
521
+
522
+ def _merge_tool_call_delta(tool_calls: list[ToolCallState], delta: ToolCallDelta) -> None:
523
+ while len(tool_calls) <= delta.index:
524
+ tool_calls.append(ToolCallState(index=len(tool_calls)))
525
+ tool_calls[delta.index].apply_delta(delta)
526
+
527
+
528
+ def _compact_tool_calls(tool_calls: list[ToolCallState]) -> list[ChatMessageToolCall]:
529
+ cleaned: list[ChatMessageToolCall] = []
530
+ for call in tool_calls:
531
+ tool_call = call.to_message_tool_call()
532
+ if tool_call is not None:
533
+ cleaned.append(tool_call)
534
+ return cleaned
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import cast
4
+
5
+ from app.core.types import JsonValue
6
+
7
+
8
+ def coerce_messages(existing_instructions: str, messages: list[JsonValue]) -> tuple[str, list[JsonValue]]:
9
+ instruction_parts: list[str] = []
10
+ input_messages: list[JsonValue] = []
11
+ for message in messages:
12
+ if not isinstance(message, dict):
13
+ raise ValueError("Each message must be an object.")
14
+ message_dict = cast(dict[str, JsonValue], message)
15
+ role_value = message_dict.get("role")
16
+ role = role_value if isinstance(role_value, str) else None
17
+ if role in ("system", "developer"):
18
+ content_text = _content_to_text(message_dict.get("content"))
19
+ if content_text:
20
+ instruction_parts.append(content_text)
21
+ continue
22
+ input_messages.append(cast(JsonValue, message_dict))
23
+ merged = _merge_instructions(existing_instructions, instruction_parts)
24
+ return merged, input_messages
25
+
26
+
27
+ def _merge_instructions(existing: str, extra_parts: list[str]) -> str:
28
+ if not extra_parts:
29
+ return existing
30
+ extra = "\n".join([part for part in extra_parts if part])
31
+ if not extra:
32
+ return existing
33
+ if existing:
34
+ return f"{existing}\n{extra}"
35
+ return extra
36
+
37
+
38
+ def _content_to_text(content: object) -> str | None:
39
+ if content is None:
40
+ return None
41
+ if isinstance(content, str):
42
+ return content
43
+ if isinstance(content, list):
44
+ parts: list[str] = []
45
+ for part in content:
46
+ if isinstance(part, str):
47
+ parts.append(part)
48
+ elif isinstance(part, dict):
49
+ part_dict = cast(dict[str, JsonValue], part)
50
+ text = part_dict.get("text")
51
+ if isinstance(text, str):
52
+ parts.append(text)
53
+ return "\n".join([part for part in parts if part])
54
+ if isinstance(content, dict):
55
+ content_dict = cast(dict[str, JsonValue], content)
56
+ text = content_dict.get("text")
57
+ if isinstance(text, str):
58
+ return text
59
+ return None
60
+ return None
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class ModelLimits(BaseModel):
7
+ model_config = ConfigDict(extra="forbid")
8
+
9
+ context: int
10
+ output: int
11
+
12
+
13
+ class ModelModalities(BaseModel):
14
+ model_config = ConfigDict(extra="forbid")
15
+
16
+ input: list[str]
17
+ output: list[str]
18
+
19
+
20
+ class ModelVariant(BaseModel):
21
+ model_config = ConfigDict(extra="forbid")
22
+
23
+ reasoningEffort: str
24
+ reasoningSummary: str
25
+ textVerbosity: str
26
+
27
+
28
+ class ModelEntry(BaseModel):
29
+ model_config = ConfigDict(extra="forbid")
30
+
31
+ name: str
32
+ limit: ModelLimits
33
+ modalities: ModelModalities
34
+ variants: dict[str, ModelVariant]
35
+
36
+
37
+ MODEL_CATALOG: dict[str, ModelEntry] = {
38
+ "gpt-5.2": ModelEntry(
39
+ name="GPT 5.2",
40
+ limit=ModelLimits(context=272000, output=128000),
41
+ modalities=ModelModalities(input=["text", "image"], output=["text"]),
42
+ variants={
43
+ "none": ModelVariant(reasoningEffort="none", reasoningSummary="auto", textVerbosity="medium"),
44
+ "low": ModelVariant(reasoningEffort="low", reasoningSummary="auto", textVerbosity="medium"),
45
+ "medium": ModelVariant(reasoningEffort="medium", reasoningSummary="auto", textVerbosity="medium"),
46
+ "high": ModelVariant(reasoningEffort="high", reasoningSummary="detailed", textVerbosity="medium"),
47
+ "xhigh": ModelVariant(reasoningEffort="xhigh", reasoningSummary="detailed", textVerbosity="medium"),
48
+ },
49
+ ),
50
+ "gpt-5.2-codex": ModelEntry(
51
+ name="GPT 5.2 Codex",
52
+ limit=ModelLimits(context=272000, output=128000),
53
+ modalities=ModelModalities(input=["text", "image"], output=["text"]),
54
+ variants={
55
+ "low": ModelVariant(reasoningEffort="low", reasoningSummary="auto", textVerbosity="medium"),
56
+ "medium": ModelVariant(reasoningEffort="medium", reasoningSummary="auto", textVerbosity="medium"),
57
+ "high": ModelVariant(reasoningEffort="high", reasoningSummary="detailed", textVerbosity="medium"),
58
+ "xhigh": ModelVariant(reasoningEffort="xhigh", reasoningSummary="detailed", textVerbosity="medium"),
59
+ },
60
+ ),
61
+ "gpt-5.1-codex-max": ModelEntry(
62
+ name="GPT 5.1 Codex Max",
63
+ limit=ModelLimits(context=272000, output=128000),
64
+ modalities=ModelModalities(input=["text", "image"], output=["text"]),
65
+ variants={
66
+ "low": ModelVariant(reasoningEffort="low", reasoningSummary="detailed", textVerbosity="medium"),
67
+ "medium": ModelVariant(reasoningEffort="medium", reasoningSummary="detailed", textVerbosity="medium"),
68
+ "high": ModelVariant(reasoningEffort="high", reasoningSummary="detailed", textVerbosity="medium"),
69
+ "xhigh": ModelVariant(reasoningEffort="xhigh", reasoningSummary="detailed", textVerbosity="medium"),
70
+ },
71
+ ),
72
+ }
@@ -35,10 +35,10 @@ class ResponsesRequest(BaseModel):
35
35
  instructions: str
36
36
  input: list[JsonValue]
37
37
  tools: list[JsonValue] = Field(default_factory=list)
38
- tool_choice: str | None = None
38
+ tool_choice: str | dict[str, JsonValue] | None = None
39
39
  parallel_tool_calls: bool | None = None
40
40
  reasoning: ResponsesReasoning | None = None
41
- store: bool | None = None
41
+ store: bool = False
42
42
  stream: bool | None = None
43
43
  include: list[str] = Field(default_factory=list)
44
44
  prompt_cache_key: str | None = None
@@ -46,10 +46,10 @@ class ResponsesRequest(BaseModel):
46
46
 
47
47
  @field_validator("store")
48
48
  @classmethod
49
- def _ensure_store_false(cls, value: bool | None) -> bool | None:
49
+ def _ensure_store_false(cls, value: bool | None) -> bool:
50
50
  if value is True:
51
51
  raise ValueError("store must be false")
52
- return value
52
+ return False if value is None else value
53
53
 
54
54
  def to_payload(self) -> JsonObject:
55
55
  payload = self.model_dump(mode="json", exclude_none=True)