mirascope 2.0.0a3__py3-none-any.whl → 2.0.0a4__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.
Files changed (93) hide show
  1. mirascope/api/_generated/__init__.py +62 -6
  2. mirascope/api/_generated/client.py +8 -0
  3. mirascope/api/_generated/errors/__init__.py +11 -1
  4. mirascope/api/_generated/errors/conflict_error.py +15 -0
  5. mirascope/api/_generated/errors/forbidden_error.py +15 -0
  6. mirascope/api/_generated/errors/internal_server_error.py +15 -0
  7. mirascope/api/_generated/errors/not_found_error.py +15 -0
  8. mirascope/api/_generated/organizations/__init__.py +25 -0
  9. mirascope/api/_generated/organizations/client.py +380 -0
  10. mirascope/api/_generated/organizations/raw_client.py +876 -0
  11. mirascope/api/_generated/organizations/types/__init__.py +23 -0
  12. mirascope/api/_generated/organizations/types/organizations_create_response.py +24 -0
  13. mirascope/api/_generated/organizations/types/organizations_create_response_role.py +7 -0
  14. mirascope/api/_generated/organizations/types/organizations_get_response.py +24 -0
  15. mirascope/api/_generated/organizations/types/organizations_get_response_role.py +7 -0
  16. mirascope/api/_generated/organizations/types/organizations_list_response_item.py +24 -0
  17. mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +7 -0
  18. mirascope/api/_generated/organizations/types/organizations_update_response.py +24 -0
  19. mirascope/api/_generated/organizations/types/organizations_update_response_role.py +7 -0
  20. mirascope/api/_generated/projects/__init__.py +17 -0
  21. mirascope/api/_generated/projects/client.py +458 -0
  22. mirascope/api/_generated/projects/raw_client.py +1016 -0
  23. mirascope/api/_generated/projects/types/__init__.py +15 -0
  24. mirascope/api/_generated/projects/types/projects_create_response.py +30 -0
  25. mirascope/api/_generated/projects/types/projects_get_response.py +30 -0
  26. mirascope/api/_generated/projects/types/projects_list_response_item.py +30 -0
  27. mirascope/api/_generated/projects/types/projects_update_response.py +30 -0
  28. mirascope/api/_generated/reference.md +586 -0
  29. mirascope/api/_generated/types/__init__.py +20 -4
  30. mirascope/api/_generated/types/already_exists_error.py +24 -0
  31. mirascope/api/_generated/types/already_exists_error_tag.py +5 -0
  32. mirascope/api/_generated/types/database_error.py +24 -0
  33. mirascope/api/_generated/types/database_error_tag.py +5 -0
  34. mirascope/api/_generated/types/http_api_decode_error.py +1 -3
  35. mirascope/api/_generated/types/issue.py +1 -5
  36. mirascope/api/_generated/types/not_found_error_body.py +24 -0
  37. mirascope/api/_generated/types/not_found_error_tag.py +5 -0
  38. mirascope/api/_generated/types/permission_denied_error.py +24 -0
  39. mirascope/api/_generated/types/permission_denied_error_tag.py +7 -0
  40. mirascope/api/_generated/types/property_key.py +2 -2
  41. mirascope/api/_generated/types/{property_key_tag.py → property_key_key.py} +3 -5
  42. mirascope/api/_generated/types/{property_key_tag_tag.py → property_key_key_tag.py} +1 -1
  43. mirascope/llm/__init__.py +4 -0
  44. mirascope/llm/providers/__init__.py +6 -0
  45. mirascope/llm/providers/anthropic/__init__.py +6 -1
  46. mirascope/llm/providers/anthropic/_utils/__init__.py +15 -5
  47. mirascope/llm/providers/anthropic/_utils/beta_decode.py +271 -0
  48. mirascope/llm/providers/anthropic/_utils/beta_encode.py +216 -0
  49. mirascope/llm/providers/anthropic/_utils/decode.py +39 -7
  50. mirascope/llm/providers/anthropic/_utils/encode.py +156 -64
  51. mirascope/llm/providers/anthropic/beta_provider.py +322 -0
  52. mirascope/llm/providers/anthropic/model_id.py +10 -27
  53. mirascope/llm/providers/anthropic/model_info.py +87 -0
  54. mirascope/llm/providers/anthropic/provider.py +127 -145
  55. mirascope/llm/providers/base/_utils.py +15 -1
  56. mirascope/llm/providers/google/_utils/decode.py +55 -3
  57. mirascope/llm/providers/google/_utils/encode.py +14 -6
  58. mirascope/llm/providers/google/model_id.py +7 -13
  59. mirascope/llm/providers/google/model_info.py +62 -0
  60. mirascope/llm/providers/google/provider.py +8 -4
  61. mirascope/llm/providers/load_provider.py +8 -2
  62. mirascope/llm/providers/mlx/_utils.py +23 -1
  63. mirascope/llm/providers/mlx/encoding/transformers.py +17 -1
  64. mirascope/llm/providers/mlx/provider.py +4 -0
  65. mirascope/llm/providers/ollama/__init__.py +19 -0
  66. mirascope/llm/providers/ollama/provider.py +71 -0
  67. mirascope/llm/providers/openai/completions/__init__.py +6 -1
  68. mirascope/llm/providers/openai/completions/_utils/decode.py +57 -5
  69. mirascope/llm/providers/openai/completions/_utils/encode.py +9 -8
  70. mirascope/llm/providers/openai/completions/base_provider.py +513 -0
  71. mirascope/llm/providers/openai/completions/provider.py +13 -447
  72. mirascope/llm/providers/openai/model_info.py +57 -0
  73. mirascope/llm/providers/openai/provider.py +16 -4
  74. mirascope/llm/providers/openai/responses/_utils/decode.py +55 -4
  75. mirascope/llm/providers/openai/responses/_utils/encode.py +9 -9
  76. mirascope/llm/providers/openai/responses/provider.py +20 -21
  77. mirascope/llm/providers/provider_id.py +11 -1
  78. mirascope/llm/providers/provider_registry.py +3 -1
  79. mirascope/llm/providers/together/__init__.py +19 -0
  80. mirascope/llm/providers/together/provider.py +40 -0
  81. mirascope/llm/responses/__init__.py +3 -0
  82. mirascope/llm/responses/base_response.py +4 -0
  83. mirascope/llm/responses/base_stream_response.py +25 -1
  84. mirascope/llm/responses/finish_reason.py +1 -0
  85. mirascope/llm/responses/response.py +9 -0
  86. mirascope/llm/responses/root_response.py +5 -1
  87. mirascope/llm/responses/usage.py +95 -0
  88. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/METADATA +3 -3
  89. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/RECORD +91 -50
  90. mirascope/llm/providers/openai/shared/__init__.py +0 -7
  91. mirascope/llm/providers/openai/shared/_utils.py +0 -59
  92. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/WHEEL +0 -0
  93. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -1,456 +1,22 @@
1
- """OpenAI client implementation."""
1
+ """OpenAI Completions API provider implementation."""
2
2
 
3
- from collections.abc import Sequence
4
- from typing import Literal
5
- from typing_extensions import Unpack
3
+ from ..model_id import model_name
4
+ from .base_provider import BaseOpenAICompletionsProvider
6
5
 
7
- from openai import AsyncOpenAI, OpenAI
8
6
 
9
- from ....context import Context, DepsT
10
- from ....formatting import Format, FormattableT
11
- from ....messages import Message
12
- from ....responses import (
13
- AsyncContextResponse,
14
- AsyncContextStreamResponse,
15
- AsyncResponse,
16
- AsyncStreamResponse,
17
- ContextResponse,
18
- ContextStreamResponse,
19
- Response,
20
- StreamResponse,
21
- )
22
- from ....tools import (
23
- AsyncContextTool,
24
- AsyncContextToolkit,
25
- AsyncTool,
26
- AsyncToolkit,
27
- ContextTool,
28
- ContextToolkit,
29
- Tool,
30
- Toolkit,
31
- )
32
- from ...base import BaseProvider, Params
33
- from ..model_id import OpenAIModelId, model_name
34
- from . import _utils
35
-
36
-
37
- class OpenAICompletionsProvider(BaseProvider[OpenAI]):
38
- """The client for the OpenAI LLM model."""
7
+ class OpenAICompletionsProvider(BaseOpenAICompletionsProvider):
8
+ """Provider for OpenAI's ChatCompletions API."""
39
9
 
40
10
  id = "openai:completions"
41
11
  default_scope = "openai/"
12
+ default_base_url = None
13
+ api_key_env_var = "OPENAI_API_KEY"
14
+ api_key_required = False
15
+ provider_name = "OpenAI"
42
16
 
43
- def __init__(
44
- self,
45
- *,
46
- api_key: str | None = None,
47
- base_url: str | None = None,
48
- wrapped_by_openai_provider: bool = False,
49
- ) -> None:
50
- """Initialize the OpenAI client."""
51
- self.client = OpenAI(api_key=api_key, base_url=base_url)
52
- self.async_client = AsyncOpenAI(api_key=api_key, base_url=base_url)
53
- self.active_provider_id: Literal["openai", "openai:completions"] = (
54
- "openai" if wrapped_by_openai_provider else "openai:completions"
55
- )
56
-
57
- def _call(
58
- self,
59
- *,
60
- model_id: OpenAIModelId,
61
- messages: Sequence[Message],
62
- tools: Sequence[Tool] | Toolkit | None = None,
63
- format: type[FormattableT] | Format[FormattableT] | None = None,
64
- **params: Unpack[Params],
65
- ) -> Response | Response[FormattableT]:
66
- """Generate an `llm.Response` by synchronously calling the OpenAI ChatCompletions API.
67
-
68
- Args:
69
- model_id: Model identifier to use.
70
- messages: Messages to send to the LLM.
71
- tools: Optional tools that the model may invoke.
72
- format: Optional response format specifier.
73
- **params: Additional parameters to configure output (e.g. temperature). See `llm.Params`.
74
-
75
- Returns:
76
- An `llm.Response` object containing the LLM-generated content.
77
- """
78
- input_messages, format, kwargs = _utils.encode_request(
79
- model_id=model_id,
80
- messages=messages,
81
- tools=tools,
82
- format=format,
83
- params=params,
84
- )
85
-
86
- openai_response = self.client.chat.completions.create(**kwargs)
87
-
88
- assistant_message, finish_reason = _utils.decode_response(
89
- openai_response, model_id, self.active_provider_id
90
- )
91
-
92
- return Response(
93
- raw=openai_response,
94
- provider_id=self.active_provider_id,
95
- model_id=model_id,
96
- provider_model_name=model_name(model_id, "completions"),
97
- params=params,
98
- tools=tools,
99
- input_messages=input_messages,
100
- assistant_message=assistant_message,
101
- finish_reason=finish_reason,
102
- format=format,
103
- )
104
-
105
- def _context_call(
106
- self,
107
- *,
108
- ctx: Context[DepsT],
109
- model_id: OpenAIModelId,
110
- messages: Sequence[Message],
111
- tools: Sequence[Tool | ContextTool[DepsT]]
112
- | ContextToolkit[DepsT]
113
- | None = None,
114
- format: type[FormattableT] | Format[FormattableT] | None = None,
115
- **params: Unpack[Params],
116
- ) -> ContextResponse[DepsT, None] | ContextResponse[DepsT, FormattableT]:
117
- """Generate an `llm.ContextResponse` by synchronously calling the OpenAI ChatCompletions API.
118
-
119
- Args:
120
- ctx: Context object with dependencies for tools.
121
- model_id: Model identifier to use.
122
- messages: Messages to send to the LLM.
123
- tools: Optional tools that the model may invoke.
124
- format: Optional response format specifier.
125
- **params: Additional parameters to configure output (e.g. temperature). See `llm.Params`.
126
-
127
- Returns:
128
- An `llm.ContextResponse` object containing the LLM-generated content.
129
- """
130
- input_messages, format, kwargs = _utils.encode_request(
131
- model_id=model_id,
132
- messages=messages,
133
- tools=tools,
134
- format=format,
135
- params=params,
136
- )
137
-
138
- openai_response = self.client.chat.completions.create(**kwargs)
139
-
140
- assistant_message, finish_reason = _utils.decode_response(
141
- openai_response, model_id, self.active_provider_id
142
- )
143
-
144
- return ContextResponse(
145
- raw=openai_response,
146
- provider_id=self.active_provider_id,
147
- model_id=model_id,
148
- provider_model_name=model_name(model_id, "completions"),
149
- params=params,
150
- tools=tools,
151
- input_messages=input_messages,
152
- assistant_message=assistant_message,
153
- finish_reason=finish_reason,
154
- format=format,
155
- )
156
-
157
- async def _call_async(
158
- self,
159
- *,
160
- model_id: OpenAIModelId,
161
- messages: Sequence[Message],
162
- tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
163
- format: type[FormattableT] | Format[FormattableT] | None = None,
164
- **params: Unpack[Params],
165
- ) -> AsyncResponse | AsyncResponse[FormattableT]:
166
- """Generate an `llm.AsyncResponse` by asynchronously calling the OpenAI ChatCompletions API.
167
-
168
- Args:
169
- model_id: Model identifier to use.
170
- messages: Messages to send to the LLM.
171
- tools: Optional tools that the model may invoke.
172
- format: Optional response format specifier.
173
- **params: Additional parameters to configure output (e.g. temperature). See `llm.Params`.
174
-
175
- Returns:
176
- An `llm.AsyncResponse` object containing the LLM-generated content.
177
- """
178
-
179
- input_messages, format, kwargs = _utils.encode_request(
180
- model_id=model_id,
181
- params=params,
182
- messages=messages,
183
- tools=tools,
184
- format=format,
185
- )
186
-
187
- openai_response = await self.async_client.chat.completions.create(**kwargs)
188
-
189
- assistant_message, finish_reason = _utils.decode_response(
190
- openai_response, model_id, self.active_provider_id
191
- )
192
-
193
- return AsyncResponse(
194
- raw=openai_response,
195
- provider_id=self.active_provider_id,
196
- model_id=model_id,
197
- provider_model_name=model_name(model_id, "completions"),
198
- params=params,
199
- tools=tools,
200
- input_messages=input_messages,
201
- assistant_message=assistant_message,
202
- finish_reason=finish_reason,
203
- format=format,
204
- )
205
-
206
- async def _context_call_async(
207
- self,
208
- *,
209
- ctx: Context[DepsT],
210
- model_id: OpenAIModelId,
211
- messages: Sequence[Message],
212
- tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
213
- | AsyncContextToolkit[DepsT]
214
- | None = None,
215
- format: type[FormattableT] | Format[FormattableT] | None = None,
216
- **params: Unpack[Params],
217
- ) -> AsyncContextResponse[DepsT, None] | AsyncContextResponse[DepsT, FormattableT]:
218
- """Generate an `llm.AsyncContextResponse` by asynchronously calling the OpenAI ChatCompletions API.
219
-
220
- Args:
221
- ctx: Context object with dependencies for tools.
222
- model_id: Model identifier to use.
223
- messages: Messages to send to the LLM.
224
- tools: Optional tools that the model may invoke.
225
- format: Optional response format specifier.
226
- **params: Additional parameters to configure output (e.g. temperature). See `llm.Params`.
227
-
228
- Returns:
229
- An `llm.AsyncContextResponse` object containing the LLM-generated content.
230
- """
231
- input_messages, format, kwargs = _utils.encode_request(
232
- model_id=model_id,
233
- params=params,
234
- messages=messages,
235
- tools=tools,
236
- format=format,
237
- )
238
-
239
- openai_response = await self.async_client.chat.completions.create(**kwargs)
17
+ def _provider_model_name(self, model_id: str) -> str:
18
+ """Get the model name for tracking in Response.
240
19
 
241
- assistant_message, finish_reason = _utils.decode_response(
242
- openai_response, model_id, self.active_provider_id
243
- )
244
-
245
- return AsyncContextResponse(
246
- raw=openai_response,
247
- provider_id=self.active_provider_id,
248
- model_id=model_id,
249
- provider_model_name=model_name(model_id, "completions"),
250
- params=params,
251
- tools=tools,
252
- input_messages=input_messages,
253
- assistant_message=assistant_message,
254
- finish_reason=finish_reason,
255
- format=format,
256
- )
257
-
258
- def _stream(
259
- self,
260
- *,
261
- model_id: OpenAIModelId,
262
- messages: Sequence[Message],
263
- tools: Sequence[Tool] | Toolkit | None = None,
264
- format: type[FormattableT] | Format[FormattableT] | None = None,
265
- **params: Unpack[Params],
266
- ) -> StreamResponse | StreamResponse[FormattableT]:
267
- """Generate an `llm.StreamResponse` by synchronously streaming from the OpenAI ChatCompletions API.
268
-
269
- Args:
270
- model_id: Model identifier to use.
271
- messages: Messages to send to the LLM.
272
- tools: Optional tools that the model may invoke.
273
- format: Optional response format specifier.
274
- **params: Additional parameters to configure output (e.g. temperature). See `llm.Params`.
275
-
276
- Returns:
277
- An `llm.StreamResponse` object for iterating over the LLM-generated content.
20
+ Returns the model name with :completions suffix for tracking which API was used.
278
21
  """
279
- input_messages, format, kwargs = _utils.encode_request(
280
- model_id=model_id,
281
- messages=messages,
282
- tools=tools,
283
- format=format,
284
- params=params,
285
- )
286
-
287
- openai_stream = self.client.chat.completions.create(
288
- **kwargs,
289
- stream=True,
290
- )
291
-
292
- chunk_iterator = _utils.decode_stream(openai_stream)
293
-
294
- return StreamResponse(
295
- provider_id=self.active_provider_id,
296
- model_id=model_id,
297
- provider_model_name=model_name(model_id, "completions"),
298
- params=params,
299
- tools=tools,
300
- input_messages=input_messages,
301
- chunk_iterator=chunk_iterator,
302
- format=format,
303
- )
304
-
305
- def _context_stream(
306
- self,
307
- *,
308
- ctx: Context[DepsT],
309
- model_id: OpenAIModelId,
310
- messages: Sequence[Message],
311
- tools: Sequence[Tool | ContextTool[DepsT]]
312
- | ContextToolkit[DepsT]
313
- | None = None,
314
- format: type[FormattableT] | Format[FormattableT] | None = None,
315
- **params: Unpack[Params],
316
- ) -> ContextStreamResponse[DepsT] | ContextStreamResponse[DepsT, FormattableT]:
317
- """Generate an `llm.ContextStreamResponse` by synchronously streaming from the OpenAI ChatCompletions API.
318
-
319
- Args:
320
- ctx: Context object with dependencies for tools.
321
- model_id: Model identifier to use.
322
- messages: Messages to send to the LLM.
323
- tools: Optional tools that the model may invoke.
324
- format: Optional response format specifier.
325
- **params: Additional parameters to configure output (e.g. temperature). See `llm.Params`.
326
-
327
- Returns:
328
- An `llm.ContextStreamResponse` object for iterating over the LLM-generated content.
329
- """
330
- input_messages, format, kwargs = _utils.encode_request(
331
- model_id=model_id,
332
- messages=messages,
333
- tools=tools,
334
- format=format,
335
- params=params,
336
- )
337
-
338
- openai_stream = self.client.chat.completions.create(
339
- **kwargs,
340
- stream=True,
341
- )
342
-
343
- chunk_iterator = _utils.decode_stream(openai_stream)
344
-
345
- return ContextStreamResponse(
346
- provider_id=self.active_provider_id,
347
- model_id=model_id,
348
- provider_model_name=model_name(model_id, "completions"),
349
- params=params,
350
- tools=tools,
351
- input_messages=input_messages,
352
- chunk_iterator=chunk_iterator,
353
- format=format,
354
- )
355
-
356
- async def _stream_async(
357
- self,
358
- *,
359
- model_id: OpenAIModelId,
360
- messages: Sequence[Message],
361
- tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
362
- format: type[FormattableT] | Format[FormattableT] | None = None,
363
- **params: Unpack[Params],
364
- ) -> AsyncStreamResponse | AsyncStreamResponse[FormattableT]:
365
- """Generate an `llm.AsyncStreamResponse` by asynchronously streaming from the OpenAI ChatCompletions API.
366
-
367
- Args:
368
- model_id: Model identifier to use.
369
- messages: Messages to send to the LLM.
370
- tools: Optional tools that the model may invoke.
371
- format: Optional response format specifier.
372
- **params: Additional parameters to configure output (e.g. temperature). See `llm.Params`.
373
-
374
- Returns:
375
- An `llm.AsyncStreamResponse` object for asynchronously iterating over the LLM-generated content.
376
- """
377
-
378
- input_messages, format, kwargs = _utils.encode_request(
379
- model_id=model_id,
380
- messages=messages,
381
- tools=tools,
382
- format=format,
383
- params=params,
384
- )
385
-
386
- openai_stream = await self.async_client.chat.completions.create(
387
- **kwargs,
388
- stream=True,
389
- )
390
-
391
- chunk_iterator = _utils.decode_async_stream(openai_stream)
392
-
393
- return AsyncStreamResponse(
394
- provider_id=self.active_provider_id,
395
- model_id=model_id,
396
- provider_model_name=model_name(model_id, "completions"),
397
- params=params,
398
- tools=tools,
399
- input_messages=input_messages,
400
- chunk_iterator=chunk_iterator,
401
- format=format,
402
- )
403
-
404
- async def _context_stream_async(
405
- self,
406
- *,
407
- ctx: Context[DepsT],
408
- model_id: OpenAIModelId,
409
- messages: Sequence[Message],
410
- tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
411
- | AsyncContextToolkit[DepsT]
412
- | None = None,
413
- format: type[FormattableT] | Format[FormattableT] | None = None,
414
- **params: Unpack[Params],
415
- ) -> (
416
- AsyncContextStreamResponse[DepsT]
417
- | AsyncContextStreamResponse[DepsT, FormattableT]
418
- ):
419
- """Generate an `llm.AsyncContextStreamResponse` by asynchronously streaming from the OpenAI ChatCompletions API.
420
-
421
- Args:
422
- ctx: Context object with dependencies for tools.
423
- model_id: Model identifier to use.
424
- messages: Messages to send to the LLM.
425
- tools: Optional tools that the model may invoke.
426
- format: Optional response format specifier.
427
- **params: Additional parameters to configure output (e.g. temperature). See `llm.Params`.
428
-
429
- Returns:
430
- An `llm.AsyncContextStreamResponse` object for asynchronously iterating over the LLM-generated content.
431
- """
432
- input_messages, format, kwargs = _utils.encode_request(
433
- model_id=model_id,
434
- messages=messages,
435
- tools=tools,
436
- format=format,
437
- params=params,
438
- )
439
-
440
- openai_stream = await self.async_client.chat.completions.create(
441
- **kwargs,
442
- stream=True,
443
- )
444
-
445
- chunk_iterator = _utils.decode_async_stream(openai_stream)
446
-
447
- return AsyncContextStreamResponse(
448
- provider_id=self.active_provider_id,
449
- model_id=model_id,
450
- provider_model_name=model_name(model_id, "completions"),
451
- params=params,
452
- tools=tools,
453
- input_messages=input_messages,
454
- chunk_iterator=chunk_iterator,
455
- format=format,
456
- )
22
+ return model_name(model_id, "completions")
@@ -131,6 +131,19 @@ OpenAIKnownModels = Literal[
131
131
  "openai/gpt-5.1-codex-max:responses",
132
132
  "openai/gpt-5.1-codex-mini",
133
133
  "openai/gpt-5.1-codex-mini:responses",
134
+ "openai/gpt-5.2",
135
+ "openai/gpt-5.2:completions",
136
+ "openai/gpt-5.2:responses",
137
+ "openai/gpt-5.2-2025-12-11",
138
+ "openai/gpt-5.2-2025-12-11:completions",
139
+ "openai/gpt-5.2-2025-12-11:responses",
140
+ "openai/gpt-5.2-chat-latest",
141
+ "openai/gpt-5.2-chat-latest:completions",
142
+ "openai/gpt-5.2-chat-latest:responses",
143
+ "openai/gpt-5.2-pro",
144
+ "openai/gpt-5.2-pro:responses",
145
+ "openai/gpt-5.2-pro-2025-12-11",
146
+ "openai/gpt-5.2-pro-2025-12-11:responses",
134
147
  "openai/o1",
135
148
  "openai/o1:completions",
136
149
  "openai/o1:responses",
@@ -168,9 +181,11 @@ OpenAIKnownModels = Literal[
168
181
 
169
182
 
170
183
  MODELS_WITHOUT_AUDIO_SUPPORT: set[str] = {
184
+ "chatgpt-4o-latest",
171
185
  "gpt-3.5-turbo",
172
186
  "gpt-3.5-turbo-0125",
173
187
  "gpt-3.5-turbo-1106",
188
+ "gpt-3.5-turbo-16k",
174
189
  "gpt-4",
175
190
  "gpt-4-0125-preview",
176
191
  "gpt-4-0613",
@@ -179,15 +194,19 @@ MODELS_WITHOUT_AUDIO_SUPPORT: set[str] = {
179
194
  "gpt-4-turbo-2024-04-09",
180
195
  "gpt-4-turbo-preview",
181
196
  "gpt-4.1",
197
+ "gpt-4.1-2025-04-14",
182
198
  "gpt-4.1-mini",
199
+ "gpt-4.1-mini-2025-04-14",
183
200
  "gpt-4.1-nano",
184
201
  "gpt-4.1-nano-2025-04-14",
185
202
  "gpt-4o",
186
203
  "gpt-4o-2024-05-13",
187
204
  "gpt-4o-2024-08-06",
205
+ "gpt-4o-2024-11-20",
188
206
  "gpt-4o-mini",
189
207
  "gpt-4o-mini-2024-07-18",
190
208
  "gpt-4o-mini-search-preview",
209
+ "gpt-4o-mini-search-preview-2025-03-11",
191
210
  "gpt-4o-search-preview",
192
211
  "gpt-4o-search-preview-2025-03-11",
193
212
  "gpt-5",
@@ -200,6 +219,9 @@ MODELS_WITHOUT_AUDIO_SUPPORT: set[str] = {
200
219
  "gpt-5-search-api",
201
220
  "gpt-5-search-api-2025-10-14",
202
221
  "gpt-5.1-chat-latest",
222
+ "gpt-5.2",
223
+ "gpt-5.2-2025-12-11",
224
+ "gpt-5.2-chat-latest",
203
225
  "o1",
204
226
  "o1-2024-12-17",
205
227
  "o3",
@@ -244,3 +266,38 @@ NON_REASONING_MODELS: set[str] = {
244
266
 
245
267
  Models not in this set are assumed to support reasoning (optimistic default).
246
268
  """
269
+
270
+ MODELS_WITHOUT_JSON_SCHEMA_SUPPORT: set[str] = {
271
+ "chatgpt-4o-latest",
272
+ "gpt-3.5-turbo",
273
+ "gpt-3.5-turbo-0125",
274
+ "gpt-3.5-turbo-1106",
275
+ "gpt-3.5-turbo-16k",
276
+ "gpt-4",
277
+ "gpt-4-0125-preview",
278
+ "gpt-4-0613",
279
+ "gpt-4-1106-preview",
280
+ "gpt-4-turbo",
281
+ "gpt-4-turbo-2024-04-09",
282
+ "gpt-4-turbo-preview",
283
+ "gpt-4o-2024-05-13",
284
+ }
285
+ """Models that do not support JSON schema (structured outputs).
286
+
287
+ Models not in this set are assumed to support JSON schema (optimistic default).
288
+ """
289
+
290
+ MODELS_WITHOUT_JSON_OBJECT_SUPPORT: set[str] = {
291
+ "gpt-4",
292
+ "gpt-4-0613",
293
+ "gpt-4o-mini-search-preview",
294
+ "gpt-4o-mini-search-preview-2025-03-11",
295
+ "gpt-4o-search-preview",
296
+ "gpt-4o-search-preview-2025-03-11",
297
+ "gpt-5-search-api",
298
+ "gpt-5-search-api-2025-10-14",
299
+ }
300
+ """Models that do not support JSON object mode.
301
+
302
+ Models not in this set are assumed to support JSON object mode (optimistic default).
303
+ """
@@ -90,6 +90,18 @@ def choose_api_mode(model_id: OpenAIModelId, messages: Sequence[Message]) -> str
90
90
  return "completions"
91
91
 
92
92
 
93
+ class OpenAIRoutedCompletionsProvider(OpenAICompletionsProvider):
94
+ """OpenAI completions client that reports provider_id as 'openai'."""
95
+
96
+ id = "openai"
97
+
98
+
99
+ class OpenAIRoutedResponsesProvider(OpenAIResponsesProvider):
100
+ """OpenAI responses client that reports provider_id as 'openai'."""
101
+
102
+ id = "openai"
103
+
104
+
93
105
  class OpenAIProvider(BaseProvider[OpenAI]):
94
106
  """Unified provider for OpenAI that routes to Completions or Responses API based on model_id."""
95
107
 
@@ -100,11 +112,11 @@ class OpenAIProvider(BaseProvider[OpenAI]):
100
112
  self, *, api_key: str | None = None, base_url: str | None = None
101
113
  ) -> None:
102
114
  """Initialize the OpenAI provider with both subclients."""
103
- self._completions_provider = OpenAICompletionsProvider(
104
- api_key=api_key, base_url=base_url, wrapped_by_openai_provider=True
115
+ self._completions_provider = OpenAIRoutedCompletionsProvider(
116
+ api_key=api_key, base_url=base_url
105
117
  )
106
- self._responses_provider = OpenAIResponsesProvider(
107
- api_key=api_key, base_url=base_url, wrapped_by_openai_provider=True
118
+ self._responses_provider = OpenAIRoutedResponsesProvider(
119
+ api_key=api_key, base_url=base_url
108
120
  )
109
121
  # Use completions client's underlying OpenAI client as the main one
110
122
  self.client = self._completions_provider.client
@@ -29,6 +29,8 @@ from .....responses import (
29
29
  FinishReasonChunk,
30
30
  RawMessageChunk,
31
31
  RawStreamEventChunk,
32
+ Usage,
33
+ UsageDeltaChunk,
32
34
  )
33
35
  from ...model_id import OpenAIModelId, model_name
34
36
 
@@ -38,6 +40,33 @@ INCOMPLETE_DETAILS_TO_FINISH_REASON = {
38
40
  }
39
41
 
40
42
 
43
+ def _decode_usage(
44
+ usage: openai_types.ResponseUsage | None,
45
+ ) -> Usage | None:
46
+ """Convert OpenAI ResponseUsage to Mirascope Usage."""
47
+ if usage is None: # pragma: no cover
48
+ return None
49
+
50
+ return Usage(
51
+ input_tokens=usage.input_tokens,
52
+ output_tokens=usage.output_tokens,
53
+ cache_read_tokens=(
54
+ usage.input_tokens_details.cached_tokens
55
+ if usage.input_tokens_details
56
+ else None
57
+ )
58
+ or 0,
59
+ cache_write_tokens=0,
60
+ reasoning_tokens=(
61
+ usage.output_tokens_details.reasoning_tokens
62
+ if usage.output_tokens_details
63
+ else None
64
+ )
65
+ or 0,
66
+ raw=usage,
67
+ )
68
+
69
+
41
70
  def _serialize_output_item(
42
71
  item: openai_types.ResponseOutputItem,
43
72
  ) -> dict[str, Any]:
@@ -48,9 +77,9 @@ def _serialize_output_item(
48
77
  def decode_response(
49
78
  response: openai_types.Response,
50
79
  model_id: OpenAIModelId,
51
- provider_id: Literal["openai", "openai:responses"],
52
- ) -> tuple[AssistantMessage, FinishReason | None]:
53
- """Convert OpenAI Responses Response to mirascope AssistantMessage."""
80
+ provider_id: str,
81
+ ) -> tuple[AssistantMessage, FinishReason | None, Usage | None]:
82
+ """Convert OpenAI Responses Response to mirascope AssistantMessage and usage."""
54
83
  parts: list[AssistantContentPart] = []
55
84
  finish_reason: FinishReason | None = None
56
85
  refused = False
@@ -100,7 +129,8 @@ def decode_response(
100
129
  ],
101
130
  )
102
131
 
103
- return assistant_message, finish_reason
132
+ usage = _decode_usage(response.usage)
133
+ return assistant_message, finish_reason, usage
104
134
 
105
135
 
106
136
  class _OpenAIResponsesChunkProcessor:
@@ -176,6 +206,27 @@ class _OpenAIResponsesChunkProcessor:
176
206
  if self.refusal_encountered:
177
207
  yield FinishReasonChunk(finish_reason=FinishReason.REFUSAL)
178
208
 
209
+ # Emit usage delta if present
210
+ if event.response.usage:
211
+ usage = event.response.usage
212
+ yield UsageDeltaChunk(
213
+ input_tokens=usage.input_tokens,
214
+ output_tokens=usage.output_tokens,
215
+ cache_read_tokens=(
216
+ usage.input_tokens_details.cached_tokens
217
+ if usage.input_tokens_details
218
+ else None
219
+ )
220
+ or 0,
221
+ cache_write_tokens=0,
222
+ reasoning_tokens=(
223
+ usage.output_tokens_details.reasoning_tokens
224
+ if usage.output_tokens_details
225
+ else None
226
+ )
227
+ or 0,
228
+ )
229
+
179
230
 
180
231
  def decode_stream(
181
232
  openai_stream: Stream[ResponseStreamEvent],