mirascope 2.0.0a6__py3-none-any.whl → 2.0.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.
Files changed (226) hide show
  1. mirascope/api/_generated/__init__.py +186 -5
  2. mirascope/api/_generated/annotations/client.py +38 -6
  3. mirascope/api/_generated/annotations/raw_client.py +366 -47
  4. mirascope/api/_generated/annotations/types/annotations_create_response.py +19 -6
  5. mirascope/api/_generated/annotations/types/annotations_get_response.py +19 -6
  6. mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item.py +22 -7
  7. mirascope/api/_generated/annotations/types/annotations_update_response.py +19 -6
  8. mirascope/api/_generated/api_keys/__init__.py +12 -2
  9. mirascope/api/_generated/api_keys/client.py +107 -6
  10. mirascope/api/_generated/api_keys/raw_client.py +486 -38
  11. mirascope/api/_generated/api_keys/types/__init__.py +7 -1
  12. mirascope/api/_generated/api_keys/types/api_keys_list_all_for_org_response_item.py +40 -0
  13. mirascope/api/_generated/client.py +36 -0
  14. mirascope/api/_generated/docs/raw_client.py +71 -9
  15. mirascope/api/_generated/environment.py +3 -3
  16. mirascope/api/_generated/environments/__init__.py +6 -0
  17. mirascope/api/_generated/environments/client.py +158 -9
  18. mirascope/api/_generated/environments/raw_client.py +620 -52
  19. mirascope/api/_generated/environments/types/__init__.py +10 -0
  20. mirascope/api/_generated/environments/types/environments_get_analytics_response.py +60 -0
  21. mirascope/api/_generated/environments/types/environments_get_analytics_response_top_functions_item.py +24 -0
  22. mirascope/api/_generated/{organizations/types/organizations_credits_response.py → environments/types/environments_get_analytics_response_top_models_item.py} +6 -3
  23. mirascope/api/_generated/errors/__init__.py +6 -0
  24. mirascope/api/_generated/errors/bad_request_error.py +5 -2
  25. mirascope/api/_generated/errors/conflict_error.py +5 -2
  26. mirascope/api/_generated/errors/payment_required_error.py +15 -0
  27. mirascope/api/_generated/errors/service_unavailable_error.py +14 -0
  28. mirascope/api/_generated/errors/too_many_requests_error.py +15 -0
  29. mirascope/api/_generated/functions/__init__.py +10 -0
  30. mirascope/api/_generated/functions/client.py +222 -8
  31. mirascope/api/_generated/functions/raw_client.py +975 -134
  32. mirascope/api/_generated/functions/types/__init__.py +28 -4
  33. mirascope/api/_generated/functions/types/functions_get_by_env_response.py +53 -0
  34. mirascope/api/_generated/functions/types/functions_get_by_env_response_dependencies_value.py +22 -0
  35. mirascope/api/_generated/functions/types/functions_list_by_env_response.py +25 -0
  36. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +56 -0
  37. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item_dependencies_value.py +22 -0
  38. mirascope/api/_generated/health/raw_client.py +74 -10
  39. mirascope/api/_generated/organization_invitations/__init__.py +33 -0
  40. mirascope/api/_generated/organization_invitations/client.py +546 -0
  41. mirascope/api/_generated/organization_invitations/raw_client.py +1519 -0
  42. mirascope/api/_generated/organization_invitations/types/__init__.py +53 -0
  43. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response.py +34 -0
  44. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response_role.py +7 -0
  45. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_request_role.py +7 -0
  46. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response.py +48 -0
  47. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_role.py +7 -0
  48. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_status.py +7 -0
  49. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response.py +48 -0
  50. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_role.py +7 -0
  51. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_status.py +7 -0
  52. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item.py +48 -0
  53. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_role.py +7 -0
  54. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_status.py +7 -0
  55. mirascope/api/_generated/organization_memberships/__init__.py +19 -0
  56. mirascope/api/_generated/organization_memberships/client.py +302 -0
  57. mirascope/api/_generated/organization_memberships/raw_client.py +736 -0
  58. mirascope/api/_generated/organization_memberships/types/__init__.py +27 -0
  59. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item.py +33 -0
  60. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item_role.py +7 -0
  61. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_request_role.py +7 -0
  62. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response.py +31 -0
  63. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response_role.py +7 -0
  64. mirascope/api/_generated/organizations/__init__.py +26 -2
  65. mirascope/api/_generated/organizations/client.py +442 -20
  66. mirascope/api/_generated/organizations/raw_client.py +1763 -164
  67. mirascope/api/_generated/organizations/types/__init__.py +48 -2
  68. mirascope/api/_generated/organizations/types/organizations_create_payment_intent_response.py +24 -0
  69. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_request_target_plan.py +7 -0
  70. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response.py +47 -0
  71. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item.py +33 -0
  72. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item_resource.py +7 -0
  73. mirascope/api/_generated/organizations/types/organizations_router_balance_response.py +24 -0
  74. mirascope/api/_generated/organizations/types/organizations_subscription_response.py +53 -0
  75. mirascope/api/_generated/organizations/types/organizations_subscription_response_current_plan.py +7 -0
  76. mirascope/api/_generated/organizations/types/organizations_subscription_response_payment_method.py +26 -0
  77. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change.py +34 -0
  78. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change_target_plan.py +7 -0
  79. mirascope/api/_generated/organizations/types/organizations_update_subscription_request_target_plan.py +7 -0
  80. mirascope/api/_generated/organizations/types/organizations_update_subscription_response.py +35 -0
  81. mirascope/api/_generated/project_memberships/__init__.py +25 -0
  82. mirascope/api/_generated/project_memberships/client.py +437 -0
  83. mirascope/api/_generated/project_memberships/raw_client.py +1039 -0
  84. mirascope/api/_generated/project_memberships/types/__init__.py +29 -0
  85. mirascope/api/_generated/project_memberships/types/project_memberships_create_request_role.py +7 -0
  86. mirascope/api/_generated/project_memberships/types/project_memberships_create_response.py +35 -0
  87. mirascope/api/_generated/project_memberships/types/project_memberships_create_response_role.py +7 -0
  88. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item.py +33 -0
  89. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item_role.py +7 -0
  90. mirascope/api/_generated/project_memberships/types/project_memberships_update_request_role.py +7 -0
  91. mirascope/api/_generated/project_memberships/types/project_memberships_update_response.py +35 -0
  92. mirascope/api/_generated/project_memberships/types/project_memberships_update_response_role.py +7 -0
  93. mirascope/api/_generated/projects/raw_client.py +415 -58
  94. mirascope/api/_generated/reference.md +2767 -397
  95. mirascope/api/_generated/tags/__init__.py +19 -0
  96. mirascope/api/_generated/tags/client.py +504 -0
  97. mirascope/api/_generated/tags/raw_client.py +1288 -0
  98. mirascope/api/_generated/tags/types/__init__.py +17 -0
  99. mirascope/api/_generated/tags/types/tags_create_response.py +41 -0
  100. mirascope/api/_generated/tags/types/tags_get_response.py +41 -0
  101. mirascope/api/_generated/tags/types/tags_list_response.py +23 -0
  102. mirascope/api/_generated/tags/types/tags_list_response_tags_item.py +41 -0
  103. mirascope/api/_generated/tags/types/tags_update_response.py +41 -0
  104. mirascope/api/_generated/token_cost/__init__.py +7 -0
  105. mirascope/api/_generated/token_cost/client.py +160 -0
  106. mirascope/api/_generated/token_cost/raw_client.py +264 -0
  107. mirascope/api/_generated/token_cost/types/__init__.py +8 -0
  108. mirascope/api/_generated/token_cost/types/token_cost_calculate_request_usage.py +54 -0
  109. mirascope/api/_generated/token_cost/types/token_cost_calculate_response.py +52 -0
  110. mirascope/api/_generated/traces/__init__.py +20 -0
  111. mirascope/api/_generated/traces/client.py +543 -0
  112. mirascope/api/_generated/traces/raw_client.py +1366 -96
  113. mirascope/api/_generated/traces/types/__init__.py +28 -0
  114. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py +6 -0
  115. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response.py +33 -0
  116. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response_spans_item.py +88 -0
  117. mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py +0 -2
  118. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response.py +25 -0
  119. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response_traces_item.py +44 -0
  120. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item.py +26 -0
  121. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item_operator.py +7 -0
  122. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_by.py +7 -0
  123. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_order.py +7 -0
  124. mirascope/api/_generated/traces/types/traces_search_by_env_response.py +26 -0
  125. mirascope/api/_generated/traces/types/traces_search_by_env_response_spans_item.py +50 -0
  126. mirascope/api/_generated/traces/types/traces_search_response_spans_item.py +10 -1
  127. mirascope/api/_generated/types/__init__.py +32 -2
  128. mirascope/api/_generated/types/bad_request_error_body.py +50 -0
  129. mirascope/api/_generated/types/date.py +3 -0
  130. mirascope/api/_generated/types/immutable_resource_error.py +22 -0
  131. mirascope/api/_generated/types/internal_server_error_body.py +3 -3
  132. mirascope/api/_generated/types/plan_limit_exceeded_error.py +32 -0
  133. mirascope/api/_generated/types/plan_limit_exceeded_error_tag.py +7 -0
  134. mirascope/api/_generated/types/pricing_unavailable_error.py +23 -0
  135. mirascope/api/_generated/types/rate_limit_error.py +31 -0
  136. mirascope/api/_generated/types/rate_limit_error_tag.py +5 -0
  137. mirascope/api/_generated/types/service_unavailable_error_body.py +24 -0
  138. mirascope/api/_generated/types/service_unavailable_error_tag.py +7 -0
  139. mirascope/api/_generated/types/subscription_past_due_error.py +31 -0
  140. mirascope/api/_generated/types/subscription_past_due_error_tag.py +7 -0
  141. mirascope/api/settings.py +19 -1
  142. mirascope/llm/__init__.py +53 -10
  143. mirascope/llm/calls/__init__.py +2 -1
  144. mirascope/llm/calls/calls.py +3 -1
  145. mirascope/llm/calls/decorator.py +21 -7
  146. mirascope/llm/content/tool_output.py +22 -5
  147. mirascope/llm/exceptions.py +284 -71
  148. mirascope/llm/formatting/__init__.py +17 -0
  149. mirascope/llm/formatting/format.py +112 -35
  150. mirascope/llm/formatting/output_parser.py +178 -0
  151. mirascope/llm/formatting/partial.py +80 -7
  152. mirascope/llm/formatting/primitives.py +192 -0
  153. mirascope/llm/formatting/types.py +20 -8
  154. mirascope/llm/messages/__init__.py +3 -0
  155. mirascope/llm/messages/_utils.py +34 -0
  156. mirascope/llm/models/__init__.py +5 -0
  157. mirascope/llm/models/models.py +137 -69
  158. mirascope/llm/{providers/base → models}/params.py +7 -57
  159. mirascope/llm/models/thinking_config.py +61 -0
  160. mirascope/llm/prompts/_utils.py +0 -32
  161. mirascope/llm/prompts/decorator.py +16 -5
  162. mirascope/llm/prompts/prompts.py +131 -68
  163. mirascope/llm/providers/__init__.py +1 -4
  164. mirascope/llm/providers/anthropic/_utils/__init__.py +2 -0
  165. mirascope/llm/providers/anthropic/_utils/beta_decode.py +18 -9
  166. mirascope/llm/providers/anthropic/_utils/beta_encode.py +62 -13
  167. mirascope/llm/providers/anthropic/_utils/decode.py +18 -9
  168. mirascope/llm/providers/anthropic/_utils/encode.py +26 -7
  169. mirascope/llm/providers/anthropic/_utils/errors.py +2 -2
  170. mirascope/llm/providers/anthropic/beta_provider.py +64 -18
  171. mirascope/llm/providers/anthropic/provider.py +91 -33
  172. mirascope/llm/providers/base/__init__.py +0 -4
  173. mirascope/llm/providers/base/_utils.py +55 -6
  174. mirascope/llm/providers/base/base_provider.py +116 -37
  175. mirascope/llm/providers/google/_utils/__init__.py +2 -0
  176. mirascope/llm/providers/google/_utils/decode.py +20 -7
  177. mirascope/llm/providers/google/_utils/encode.py +26 -7
  178. mirascope/llm/providers/google/_utils/errors.py +3 -2
  179. mirascope/llm/providers/google/provider.py +64 -18
  180. mirascope/llm/providers/mirascope/_utils.py +13 -17
  181. mirascope/llm/providers/mirascope/provider.py +49 -18
  182. mirascope/llm/providers/mlx/_utils.py +7 -2
  183. mirascope/llm/providers/mlx/encoding/base.py +5 -2
  184. mirascope/llm/providers/mlx/encoding/transformers.py +5 -2
  185. mirascope/llm/providers/mlx/mlx.py +23 -6
  186. mirascope/llm/providers/mlx/provider.py +42 -13
  187. mirascope/llm/providers/openai/_utils/errors.py +2 -2
  188. mirascope/llm/providers/openai/completions/_utils/encode.py +20 -16
  189. mirascope/llm/providers/openai/completions/base_provider.py +40 -11
  190. mirascope/llm/providers/openai/provider.py +40 -10
  191. mirascope/llm/providers/openai/responses/_utils/__init__.py +2 -0
  192. mirascope/llm/providers/openai/responses/_utils/decode.py +19 -6
  193. mirascope/llm/providers/openai/responses/_utils/encode.py +22 -10
  194. mirascope/llm/providers/openai/responses/provider.py +56 -18
  195. mirascope/llm/providers/provider_registry.py +93 -19
  196. mirascope/llm/responses/__init__.py +6 -1
  197. mirascope/llm/responses/_utils.py +102 -12
  198. mirascope/llm/responses/base_response.py +5 -2
  199. mirascope/llm/responses/base_stream_response.py +115 -25
  200. mirascope/llm/responses/response.py +2 -1
  201. mirascope/llm/responses/root_response.py +89 -17
  202. mirascope/llm/responses/stream_response.py +6 -9
  203. mirascope/llm/tools/decorator.py +9 -4
  204. mirascope/llm/tools/tool_schema.py +12 -6
  205. mirascope/llm/tools/toolkit.py +35 -27
  206. mirascope/llm/tools/tools.py +45 -20
  207. mirascope/ops/__init__.py +4 -0
  208. mirascope/ops/_internal/configuration.py +82 -31
  209. mirascope/ops/_internal/exporters/exporters.py +64 -11
  210. mirascope/ops/_internal/instrumentation/llm/common.py +530 -0
  211. mirascope/ops/_internal/instrumentation/llm/cost.py +190 -0
  212. mirascope/ops/_internal/instrumentation/llm/encode.py +1 -1
  213. mirascope/ops/_internal/instrumentation/llm/llm.py +116 -1242
  214. mirascope/ops/_internal/instrumentation/llm/model.py +1798 -0
  215. mirascope/ops/_internal/instrumentation/llm/response.py +521 -0
  216. mirascope/ops/_internal/instrumentation/llm/serialize.py +300 -0
  217. mirascope/ops/_internal/protocols.py +83 -1
  218. mirascope/ops/_internal/traced_calls.py +4 -0
  219. mirascope/ops/_internal/traced_functions.py +118 -8
  220. mirascope/ops/_internal/tracing.py +78 -1
  221. mirascope/ops/_internal/utils.py +52 -4
  222. {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/METADATA +12 -11
  223. mirascope-2.0.1.dist-info/RECORD +423 -0
  224. {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/licenses/LICENSE +1 -1
  225. mirascope-2.0.0a6.dist-info/RECORD +0 -316
  226. {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,178 @@
1
+ """The `llm.output_parser` decorator for creating custom output parsers."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
5
+ from typing_extensions import TypeIs
6
+
7
+ if TYPE_CHECKING:
8
+ from ..responses import AnyResponse
9
+
10
+ OutputT = TypeVar("OutputT")
11
+
12
+
13
+ class OutputParser(Generic[OutputT]):
14
+ """Represents a custom output parser created with @llm.output_parser.
15
+
16
+ This class wraps a parsing function and stores formatting instructions.
17
+ It is created by the @llm.output_parser decorator and used as a format
18
+ argument in LLM calls.
19
+
20
+ Unlike BaseModel and primitive type formats that use structured outputs
21
+ (JSON schema, tools, strict mode), OutputParser works with raw text responses
22
+ and custom parsing logic.
23
+
24
+ Example:
25
+ ```python
26
+ @llm.output_parser(
27
+ formatting_instructions="Return XML: <book><title>...</title></book>"
28
+ )
29
+ def parse_book_xml(response: llm.AnyResponse) -> Book:
30
+ text = "".join(part.text for part in response.texts)
31
+ root = ET.fromstring(text)
32
+ return Book(title=root.find("title").text, ...)
33
+
34
+ @llm.call("openai/gpt-4o", format=parse_book_xml)
35
+ def recommend_book(genre: str):
36
+ return f"Recommend a {genre} book."
37
+
38
+ response = recommend_book("fantasy")
39
+ book = response.parse() # Returns Book instance
40
+ ```
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ func: Callable[["AnyResponse"], OutputT],
46
+ formatting_instructions: str,
47
+ ) -> None:
48
+ """Initialize the OutputParser.
49
+
50
+ Args:
51
+ func: The parsing function that takes a Response and returns parsed output.
52
+ formatting_instructions: Instructions for the LLM on how to format output.
53
+ """
54
+ self.func = func
55
+ self._formatting_instructions = formatting_instructions
56
+ self.__name__ = func.__name__
57
+ self.__doc__ = func.__doc__
58
+
59
+ def formatting_instructions(self) -> str:
60
+ """Return the formatting instructions for the LLM.
61
+
62
+ These instructions are added to the system prompt to guide the LLM
63
+ on how to format its output for parsing.
64
+
65
+ Returns:
66
+ The formatting instructions string.
67
+ """
68
+ return self._formatting_instructions
69
+
70
+ def __call__(self, response: "AnyResponse") -> OutputT:
71
+ """Parse the response using the wrapped function.
72
+
73
+ Args:
74
+ response: The response object from the LLM call.
75
+
76
+ Returns:
77
+ The parsed output of type OutputT.
78
+
79
+ Raises:
80
+ Any exception raised by the wrapped parsing function.
81
+ """
82
+ return self.func(response)
83
+
84
+
85
+ def output_parser(
86
+ *,
87
+ formatting_instructions: str,
88
+ ) -> Callable[[Callable[["AnyResponse"], OutputT]], OutputParser[OutputT]]:
89
+ """Decorator to create an output parser for custom format parsing.
90
+
91
+ Use this decorator to create custom parsers for non-JSON formats like
92
+ XML, YAML, CSV, or any custom text structure. The decorated function
93
+ receives the full Response object and returns the parsed output.
94
+
95
+ This is the recommended way to handle custom output formats that don't
96
+ fit the JSON/BaseModel paradigm. The formatting instructions guide the
97
+ LLM on how to structure its output, and the parsing function extracts
98
+ the data you need.
99
+
100
+ Args:
101
+ formatting_instructions: Instructions for the LLM on how to format
102
+ the output. These will be added to the system prompt.
103
+
104
+ Returns:
105
+ Decorator that converts a function into an OutputParser.
106
+
107
+ Example:
108
+
109
+ XML parsing:
110
+ ```python
111
+ @llm.output_parser(
112
+ formatting_instructions='''
113
+ Return the book information in this XML structure:
114
+ <book>
115
+ <title>Book Title</title>
116
+ <author>Author Name</author>
117
+ <rating>5</rating>
118
+ </book>
119
+ '''
120
+ )
121
+ def parse_book_xml(response: llm.AnyResponse) -> Book:
122
+ import xml.etree.ElementTree as ET
123
+ text = "".join(part.text for part in response.texts)
124
+ root = ET.fromstring(text)
125
+ return Book(
126
+ title=root.find("title").text,
127
+ author=root.find("author").text,
128
+ rating=int(root.find("rating").text),
129
+ )
130
+ ```
131
+
132
+ Example:
133
+
134
+ CSV parsing:
135
+ ```python
136
+ @llm.output_parser(
137
+ formatting_instructions='''
138
+ Return book information as CSV format with header:
139
+ title,author,rating
140
+ Book 1,Author 1,5
141
+ Book 2,Author 2,4
142
+ '''
143
+ )
144
+ def parse_books_csv(response: llm.AnyResponse) -> list[Book]:
145
+ text = "".join(part.text for part in response.texts)
146
+ lines = text.strip().split('\\n')[1:] # Skip header
147
+ return [
148
+ Book(
149
+ title=line.split(',')[0].strip(),
150
+ author=line.split(',')[1].strip(),
151
+ rating=int(line.split(',')[2]),
152
+ )
153
+ for line in lines
154
+ ]
155
+ ```
156
+ """
157
+
158
+ def decorator(
159
+ func: Callable[["AnyResponse"], OutputT],
160
+ ) -> OutputParser[OutputT]:
161
+ return OutputParser(func, formatting_instructions)
162
+
163
+ return decorator
164
+
165
+
166
+ def is_output_parser(obj: Any) -> TypeIs[OutputParser[Any]]: # noqa: ANN401
167
+ """Check if an object is an OutputParser.
168
+
169
+ This is a type guard function that narrows the type of `obj` to
170
+ `OutputParser[Any, Any]` when it returns True.
171
+
172
+ Args:
173
+ obj: The object to check.
174
+
175
+ Returns:
176
+ True if the object is an OutputParser instance, False otherwise.
177
+ """
178
+ return isinstance(obj, OutputParser)
@@ -8,10 +8,16 @@ serves as an acknowledgment of the original author's contribution to this projec
8
8
  --------------------------------------------------------------------------------
9
9
  """
10
10
 
11
- from typing import Generic, NoReturn
11
+ import inspect
12
+ from typing import Any, Generic, NoReturn, Union, cast, get_args, get_origin
13
+
14
+ from pydantic import BaseModel, create_model
12
15
 
13
16
  from .format import FormattableT
14
17
 
18
+ # Cache for generated partial models to avoid recreation
19
+ _partial_model_cache: dict[type[Any], type[Any]] = {}
20
+
15
21
 
16
22
  class Partial(Generic[FormattableT]):
17
23
  """Generate a new class with all attributes optionals.
@@ -34,7 +40,9 @@ class Partial(Generic[FormattableT]):
34
40
  Raises:
35
41
  TypeError: Direct instantiation not allowed.
36
42
  """
37
- raise TypeError("Cannot instantiate abstract Partial class.")
43
+ raise TypeError(
44
+ "Cannot instantiate abstract Partial class."
45
+ ) # pragma: no cover
38
46
 
39
47
  def __init_subclass__(
40
48
  cls,
@@ -46,13 +54,78 @@ class Partial(Generic[FormattableT]):
46
54
  Raises:
47
55
  TypeError: Subclassing not allowed.
48
56
  """
49
- raise TypeError(f"Cannot subclass {cls.__module__}.Partial")
57
+ raise TypeError(f"Cannot subclass {cls.__module__}.Partial") # pragma: no cover
50
58
 
51
59
  def __class_getitem__(
52
60
  cls,
53
61
  wrapped_class: type[FormattableT],
54
62
  ) -> type[FormattableT]:
55
- """Convert model to a partial model with all fields being optionals."""
56
- # TODO: Implement proper partial model generation
57
- # For now, return the original class to avoid import errors
58
- return wrapped_class
63
+ """Convert model to a partial model with all fields being optionals.
64
+
65
+ Recursively converts all fields in a Pydantic BaseModel to optional,
66
+ handling nested models and generic types like list[Book].
67
+
68
+ Args:
69
+ wrapped_class: The BaseModel class to make partial
70
+
71
+ Returns:
72
+ A new BaseModel class with all fields optional (or original if not BaseModel)
73
+
74
+ Example:
75
+ >>> class Author(BaseModel):
76
+ ... first_name: str
77
+ ... last_name: str
78
+ >>> class Book(BaseModel):
79
+ ... title: str
80
+ ... author: Author
81
+ >>> PartialBook = Partial[Book]
82
+ >>> partial = PartialBook(title="The Name")
83
+ >>> partial.author # None
84
+ """
85
+ # Return non-BaseModel types unchanged
86
+ if not (
87
+ inspect.isclass(wrapped_class) and issubclass(wrapped_class, BaseModel)
88
+ ):
89
+ return wrapped_class
90
+
91
+ # Check cache to avoid regenerating
92
+ if wrapped_class in _partial_model_cache:
93
+ return cast(type[FormattableT], _partial_model_cache[wrapped_class])
94
+
95
+ # Recursively make all fields optional
96
+ partial_fields: dict[str, Any] = {}
97
+ for field_name, field_info in wrapped_class.model_fields.items():
98
+ field_type = field_info.annotation
99
+
100
+ # Recursively handle nested BaseModel fields
101
+ if inspect.isclass(field_type) and issubclass(field_type, BaseModel):
102
+ field_type = Partial[field_type]
103
+
104
+ # Handle generic types with BaseModel args (e.g., list[Book], dict[str, Book])
105
+ origin = get_origin(field_type)
106
+ if origin is not None:
107
+ args = get_args(field_type)
108
+ # Recursively convert BaseModel args to partial
109
+ new_args = tuple(
110
+ Partial[arg]
111
+ if inspect.isclass(arg) and issubclass(arg, BaseModel)
112
+ else arg
113
+ for arg in args
114
+ )
115
+ # Reconstruct generic type with new args
116
+ if new_args != args:
117
+ field_type = origin[new_args]
118
+
119
+ # Make field optional with None default
120
+ optional_type = Union[field_type, None] # noqa: UP007
121
+ partial_fields[field_name] = (optional_type, None)
122
+
123
+ # Create new model with "Partial" prefix
124
+ partial_model = create_model(
125
+ f"Partial{wrapped_class.__name__}", __base__=BaseModel, **partial_fields
126
+ )
127
+
128
+ # Cache the generated model
129
+ _partial_model_cache[wrapped_class] = partial_model
130
+
131
+ return cast(type[FormattableT], partial_model)
@@ -0,0 +1,192 @@
1
+ """Utilities for handling primitive types in formatting."""
2
+
3
+ import inspect
4
+ from enum import Enum
5
+ from types import UnionType
6
+ from typing import (
7
+ Annotated,
8
+ Any,
9
+ Literal,
10
+ Protocol,
11
+ TypeAlias,
12
+ Union,
13
+ cast,
14
+ get_args,
15
+ get_origin,
16
+ )
17
+ from typing_extensions import TypeIs
18
+
19
+ from pydantic import create_model
20
+
21
+ PrimitiveType: TypeAlias = (
22
+ str
23
+ | int
24
+ | float
25
+ | bool
26
+ | bytes
27
+ | list[Any]
28
+ | set[Any]
29
+ | tuple[Any, ...]
30
+ | dict[Any, Any]
31
+ )
32
+ """Primitive types that can be used with format parameter.
33
+
34
+ These types are automatically wrapped in a BaseModel for schema generation,
35
+ then unwrapped after validation to return the primitive value.
36
+ """
37
+
38
+
39
+ class PrimitiveWrapperModel(Protocol):
40
+ """Protocol for wrapper models with an output field."""
41
+
42
+ output: Any
43
+ model_fields: Any
44
+
45
+ def __init__(self, *, output: Any) -> None: ... # noqa: ANN401
46
+
47
+ @classmethod
48
+ def model_json_schema(cls) -> dict[str, Any]: ...
49
+
50
+ @classmethod
51
+ def model_validate_json(cls, json_data: str) -> "PrimitiveWrapperModel": ...
52
+
53
+
54
+ def is_primitive_type(
55
+ type_: Any, # noqa: ANN401
56
+ ) -> TypeIs[type[PrimitiveType]]:
57
+ """Check if a type is a primitive type that needs wrapping.
58
+
59
+ Returns True for:
60
+ - Basic primitives: str, int, float, bool, bytes, list, set, tuple, dict
61
+ - Enum types
62
+ - Generic types with primitive origins: list[Book], dict[str, int]
63
+ - Literal types
64
+ - Union types (including Optional)
65
+ - Annotated types
66
+
67
+ Returns False for:
68
+ - BaseModel subclasses (already have model_json_schema)
69
+ - None/NoneType
70
+
71
+ Args:
72
+ type_: The type to check
73
+
74
+ Returns:
75
+ True if the type is a primitive that needs wrapping
76
+
77
+ Example:
78
+ >>> is_primitive_type(str)
79
+ True
80
+ >>> is_primitive_type(list[int])
81
+ True
82
+ >>> from pydantic import BaseModel
83
+ >>> class Book(BaseModel):
84
+ ... title: str
85
+ >>> is_primitive_type(Book)
86
+ False
87
+ """
88
+ primitive_types: set[type[PrimitiveType]] = {
89
+ str,
90
+ int,
91
+ float,
92
+ bool,
93
+ bytes,
94
+ list,
95
+ set,
96
+ tuple,
97
+ dict,
98
+ }
99
+ special_types: set[Any] = {Annotated, Literal, Union, UnionType}
100
+
101
+ return (
102
+ (inspect.isclass(type_) and issubclass(type_, Enum))
103
+ or type_ in primitive_types
104
+ or get_origin(type_) in primitive_types.union(special_types)
105
+ )
106
+
107
+
108
+ def _get_type_name(type_: Any) -> str: # noqa: ANN401
109
+ """Extract a clean name from a type for use in model naming.
110
+
111
+ Handles Annotated types by extracting the underlying type,
112
+ and generates clean names for generic types.
113
+
114
+ Args:
115
+ type_: The type to extract a name from
116
+
117
+ Returns:
118
+ A clean string suitable for use in a Python class name
119
+
120
+ Example:
121
+ >>> _get_type_name(str)
122
+ 'str'
123
+ >>> _get_type_name(list[int])
124
+ 'list_int_'
125
+ """
126
+ # Import Annotated locally
127
+ # Check if this is an Annotated type
128
+ if get_origin(type_) in {Annotated}:
129
+ # For Annotated types, use the first arg (the actual type)
130
+ return _get_type_name(get_args(type_)[0])
131
+
132
+ # If the type has a __name__ attribute, use it
133
+ if hasattr(type_, "__name__"):
134
+ return type_.__name__
135
+
136
+ # For complex generics like list[Book], use string representation
137
+ type_str = str(type_)
138
+
139
+ # Clean up the string to make it a valid Python identifier
140
+ # Replace brackets and commas with underscores
141
+ clean = (
142
+ type_str.replace("[", "_")
143
+ .replace("]", "_")
144
+ .replace(", ", "_")
145
+ .replace(" ", "")
146
+ .replace("'", "")
147
+ .replace('"', "")
148
+ )
149
+
150
+ return clean
151
+
152
+
153
+ def create_wrapper_model(
154
+ primitive_type: Any, # noqa: ANN401
155
+ ) -> type[PrimitiveWrapperModel]:
156
+ """Create a wrapper BaseModel for a primitive type.
157
+
158
+ The wrapper has a single field called "output" containing the primitive value.
159
+ Uses Pydantic's create_model() to generate the wrapper dynamically.
160
+
161
+ Args:
162
+ primitive_type: The primitive type to wrap
163
+
164
+ Returns:
165
+ A dynamically created BaseModel with an "output" field
166
+
167
+ Example:
168
+ >>> wrapper = create_wrapper_model(str)
169
+ >>> instance = wrapper(output="hello")
170
+ >>> instance.output
171
+ 'hello'
172
+
173
+ >>> from pydantic import BaseModel
174
+ >>> class Book(BaseModel):
175
+ ... title: str
176
+ >>> wrapper = create_wrapper_model(list[Book])
177
+ >>> books = [Book(title="Test")]
178
+ >>> instance = wrapper(output=books)
179
+ >>> len(instance.output)
180
+ 1
181
+ """
182
+ # Get a clean name for the wrapper class
183
+ type_name = _get_type_name(primitive_type)
184
+
185
+ # Create wrapper model with "output" field (required)
186
+ wrapper = create_model(
187
+ f"{type_name}Output",
188
+ __doc__=f"Wrapper for primitive type {type_name}",
189
+ output=(primitive_type, ...), # ... means required
190
+ )
191
+
192
+ return cast(type[PrimitiveWrapperModel], wrapper)
@@ -5,13 +5,22 @@ from typing_extensions import TypeVar
5
5
 
6
6
  from pydantic import BaseModel
7
7
 
8
- # TODO: Support primitive types (e.g. `format=list[Book]`)
9
- FormattableT = TypeVar("FormattableT", bound=BaseModel | None, default=None)
8
+ from .primitives import PrimitiveType
9
+
10
+ FormattableT = TypeVar(
11
+ "FormattableT", bound=BaseModel | PrimitiveType | None, default=None
12
+ )
10
13
  """Type variable for structured response format types.
11
14
 
12
15
  This TypeVar represents the type of structured output format that LLM responses
13
- can be parsed into, or None if no format is specified.
14
- If format is specified, it must extend Pydantic BaseModel.
16
+ can be parsed into, or None if no format is specified.
17
+
18
+ Supported format types:
19
+ - Pydantic BaseModel subclasses
20
+ - Primitive types: str, int, float, bool, bytes, list, set, tuple, dict
21
+ - Generic collections: list[Book], dict[str, int], etc.
22
+ - Union, Literal, and Annotated types
23
+ - Enum types
15
24
  """
16
25
 
17
26
 
@@ -19,15 +28,14 @@ FormattingMode = Literal[
19
28
  "strict",
20
29
  "json",
21
30
  "tool",
31
+ "parser",
22
32
  ]
23
33
  """Available modes for response format generation.
24
34
 
25
35
  - "strict": Use strict mode for structured outputs, asking the LLM to strictly adhere
26
36
  to a given JSON schema. Not all providers or models support it, and may not be
27
- compatible with tool calling. When making a call using this mode, an
28
- `llm.FormattingModeNotSupportedError` error may be raised (if "strict" mode is wholly
29
- unsupported), or an `llm.FeatureNotSupportedError` may be raised (if trying to use
30
- strict along with tools and that is unsupported).
37
+ compatible with tool calling. When making a call using this mode, an
38
+ `llm.FeatureNotSupportedError` error may be raised if the mode is unsupported.
31
39
 
32
40
  - "json": Use JSON mode for structured outputs. In contrast to strict mode, we ask the
33
41
  LLM to output JSON as text, though without guarantees that the model will output
@@ -42,6 +50,10 @@ FormattingMode = Literal[
42
50
  content (abstracting over the tool call). If other tools are present, they will
43
51
  be handled as regular tool calls.
44
52
 
53
+ - "parser": Use custom parsing with formatting instructions. No schema generation or
54
+ structured output features. The LLM receives only formatting instructions and the
55
+ response is parsed using a custom parser function created with `@llm.output_parser`.
56
+
45
57
  Note: When `llm.format` is not used, the provider will automatically choose a mode at call time.
46
58
  """
47
59
 
@@ -5,6 +5,7 @@ as a unified `Message` class with different roles (system, user, assistant) and
5
5
  content arrays that can include text, images, audio, documents, and tool interactions.
6
6
  """
7
7
 
8
+ from ._utils import is_messages, promote_to_messages
8
9
  from .message import (
9
10
  AssistantContent,
10
11
  AssistantMessage,
@@ -27,6 +28,8 @@ __all__ = [
27
28
  "UserContent",
28
29
  "UserMessage",
29
30
  "assistant",
31
+ "is_messages",
32
+ "promote_to_messages",
30
33
  "system",
31
34
  "user",
32
35
  ]
@@ -0,0 +1,34 @@
1
+ """Utility functions for message handling."""
2
+
3
+ from collections.abc import Sequence
4
+ from typing_extensions import TypeIs
5
+
6
+ from .message import (
7
+ AssistantMessage,
8
+ Message,
9
+ SystemMessage,
10
+ UserContent,
11
+ UserMessage,
12
+ user,
13
+ )
14
+
15
+
16
+ def is_messages(
17
+ content: UserContent | Sequence[Message],
18
+ ) -> TypeIs[Sequence[Message]]:
19
+ if isinstance(content, list):
20
+ if not content:
21
+ raise ValueError("Empty array may not be used as message content")
22
+ return isinstance(content[0], SystemMessage | UserMessage | AssistantMessage)
23
+ return False
24
+
25
+
26
+ def promote_to_messages(content: UserContent | Sequence[Message]) -> Sequence[Message]:
27
+ """Promote a prompt result to a list of messages.
28
+
29
+ If the result is already a list of Messages, returns it as-is.
30
+ If the result is str/UserContentPart/Sequence of content parts, wraps it in a user message.
31
+ """
32
+ if is_messages(content):
33
+ return content
34
+ return [user(content)]
@@ -7,9 +7,14 @@ creates a default one.
7
7
  """
8
8
 
9
9
  from .models import Model, model, model_from_context, use_model
10
+ from .params import Params
11
+ from .thinking_config import ThinkingConfig, ThinkingLevel
10
12
 
11
13
  __all__ = [
12
14
  "Model",
15
+ "Params",
16
+ "ThinkingConfig",
17
+ "ThinkingLevel",
13
18
  "model",
14
19
  "model_from_context",
15
20
  "use_model",