mirascope 2.0.0a6__py3-none-any.whl → 2.0.2__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 (230) hide show
  1. mirascope/_utils.py +34 -0
  2. mirascope/api/_generated/__init__.py +186 -5
  3. mirascope/api/_generated/annotations/client.py +38 -6
  4. mirascope/api/_generated/annotations/raw_client.py +366 -47
  5. mirascope/api/_generated/annotations/types/annotations_create_response.py +19 -6
  6. mirascope/api/_generated/annotations/types/annotations_get_response.py +19 -6
  7. mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item.py +22 -7
  8. mirascope/api/_generated/annotations/types/annotations_update_response.py +19 -6
  9. mirascope/api/_generated/api_keys/__init__.py +12 -2
  10. mirascope/api/_generated/api_keys/client.py +107 -6
  11. mirascope/api/_generated/api_keys/raw_client.py +486 -38
  12. mirascope/api/_generated/api_keys/types/__init__.py +7 -1
  13. mirascope/api/_generated/api_keys/types/api_keys_list_all_for_org_response_item.py +40 -0
  14. mirascope/api/_generated/client.py +36 -0
  15. mirascope/api/_generated/docs/raw_client.py +71 -9
  16. mirascope/api/_generated/environment.py +3 -3
  17. mirascope/api/_generated/environments/__init__.py +6 -0
  18. mirascope/api/_generated/environments/client.py +158 -9
  19. mirascope/api/_generated/environments/raw_client.py +620 -52
  20. mirascope/api/_generated/environments/types/__init__.py +10 -0
  21. mirascope/api/_generated/environments/types/environments_get_analytics_response.py +60 -0
  22. mirascope/api/_generated/environments/types/environments_get_analytics_response_top_functions_item.py +24 -0
  23. mirascope/api/_generated/{organizations/types/organizations_credits_response.py → environments/types/environments_get_analytics_response_top_models_item.py} +6 -3
  24. mirascope/api/_generated/errors/__init__.py +6 -0
  25. mirascope/api/_generated/errors/bad_request_error.py +5 -2
  26. mirascope/api/_generated/errors/conflict_error.py +5 -2
  27. mirascope/api/_generated/errors/payment_required_error.py +15 -0
  28. mirascope/api/_generated/errors/service_unavailable_error.py +14 -0
  29. mirascope/api/_generated/errors/too_many_requests_error.py +15 -0
  30. mirascope/api/_generated/functions/__init__.py +10 -0
  31. mirascope/api/_generated/functions/client.py +222 -8
  32. mirascope/api/_generated/functions/raw_client.py +975 -134
  33. mirascope/api/_generated/functions/types/__init__.py +28 -4
  34. mirascope/api/_generated/functions/types/functions_get_by_env_response.py +53 -0
  35. mirascope/api/_generated/functions/types/functions_get_by_env_response_dependencies_value.py +22 -0
  36. mirascope/api/_generated/functions/types/functions_list_by_env_response.py +25 -0
  37. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +56 -0
  38. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item_dependencies_value.py +22 -0
  39. mirascope/api/_generated/health/raw_client.py +74 -10
  40. mirascope/api/_generated/organization_invitations/__init__.py +33 -0
  41. mirascope/api/_generated/organization_invitations/client.py +546 -0
  42. mirascope/api/_generated/organization_invitations/raw_client.py +1519 -0
  43. mirascope/api/_generated/organization_invitations/types/__init__.py +53 -0
  44. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response.py +34 -0
  45. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response_role.py +7 -0
  46. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_request_role.py +7 -0
  47. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response.py +48 -0
  48. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_role.py +7 -0
  49. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_status.py +7 -0
  50. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response.py +48 -0
  51. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_role.py +7 -0
  52. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_status.py +7 -0
  53. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item.py +48 -0
  54. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_role.py +7 -0
  55. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_status.py +7 -0
  56. mirascope/api/_generated/organization_memberships/__init__.py +19 -0
  57. mirascope/api/_generated/organization_memberships/client.py +302 -0
  58. mirascope/api/_generated/organization_memberships/raw_client.py +736 -0
  59. mirascope/api/_generated/organization_memberships/types/__init__.py +27 -0
  60. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item.py +33 -0
  61. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item_role.py +7 -0
  62. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_request_role.py +7 -0
  63. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response.py +31 -0
  64. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response_role.py +7 -0
  65. mirascope/api/_generated/organizations/__init__.py +26 -2
  66. mirascope/api/_generated/organizations/client.py +442 -20
  67. mirascope/api/_generated/organizations/raw_client.py +1763 -164
  68. mirascope/api/_generated/organizations/types/__init__.py +48 -2
  69. mirascope/api/_generated/organizations/types/organizations_create_payment_intent_response.py +24 -0
  70. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_request_target_plan.py +7 -0
  71. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response.py +47 -0
  72. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item.py +33 -0
  73. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item_resource.py +7 -0
  74. mirascope/api/_generated/organizations/types/organizations_router_balance_response.py +24 -0
  75. mirascope/api/_generated/organizations/types/organizations_subscription_response.py +53 -0
  76. mirascope/api/_generated/organizations/types/organizations_subscription_response_current_plan.py +7 -0
  77. mirascope/api/_generated/organizations/types/organizations_subscription_response_payment_method.py +26 -0
  78. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change.py +34 -0
  79. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change_target_plan.py +7 -0
  80. mirascope/api/_generated/organizations/types/organizations_update_subscription_request_target_plan.py +7 -0
  81. mirascope/api/_generated/organizations/types/organizations_update_subscription_response.py +35 -0
  82. mirascope/api/_generated/project_memberships/__init__.py +25 -0
  83. mirascope/api/_generated/project_memberships/client.py +437 -0
  84. mirascope/api/_generated/project_memberships/raw_client.py +1039 -0
  85. mirascope/api/_generated/project_memberships/types/__init__.py +29 -0
  86. mirascope/api/_generated/project_memberships/types/project_memberships_create_request_role.py +7 -0
  87. mirascope/api/_generated/project_memberships/types/project_memberships_create_response.py +35 -0
  88. mirascope/api/_generated/project_memberships/types/project_memberships_create_response_role.py +7 -0
  89. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item.py +33 -0
  90. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item_role.py +7 -0
  91. mirascope/api/_generated/project_memberships/types/project_memberships_update_request_role.py +7 -0
  92. mirascope/api/_generated/project_memberships/types/project_memberships_update_response.py +35 -0
  93. mirascope/api/_generated/project_memberships/types/project_memberships_update_response_role.py +7 -0
  94. mirascope/api/_generated/projects/raw_client.py +415 -58
  95. mirascope/api/_generated/reference.md +2767 -397
  96. mirascope/api/_generated/tags/__init__.py +19 -0
  97. mirascope/api/_generated/tags/client.py +504 -0
  98. mirascope/api/_generated/tags/raw_client.py +1288 -0
  99. mirascope/api/_generated/tags/types/__init__.py +17 -0
  100. mirascope/api/_generated/tags/types/tags_create_response.py +41 -0
  101. mirascope/api/_generated/tags/types/tags_get_response.py +41 -0
  102. mirascope/api/_generated/tags/types/tags_list_response.py +23 -0
  103. mirascope/api/_generated/tags/types/tags_list_response_tags_item.py +41 -0
  104. mirascope/api/_generated/tags/types/tags_update_response.py +41 -0
  105. mirascope/api/_generated/token_cost/__init__.py +7 -0
  106. mirascope/api/_generated/token_cost/client.py +160 -0
  107. mirascope/api/_generated/token_cost/raw_client.py +264 -0
  108. mirascope/api/_generated/token_cost/types/__init__.py +8 -0
  109. mirascope/api/_generated/token_cost/types/token_cost_calculate_request_usage.py +54 -0
  110. mirascope/api/_generated/token_cost/types/token_cost_calculate_response.py +52 -0
  111. mirascope/api/_generated/traces/__init__.py +20 -0
  112. mirascope/api/_generated/traces/client.py +543 -0
  113. mirascope/api/_generated/traces/raw_client.py +1366 -96
  114. mirascope/api/_generated/traces/types/__init__.py +28 -0
  115. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py +6 -0
  116. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response.py +33 -0
  117. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response_spans_item.py +88 -0
  118. mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py +0 -2
  119. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response.py +25 -0
  120. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response_traces_item.py +44 -0
  121. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item.py +26 -0
  122. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item_operator.py +7 -0
  123. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_by.py +7 -0
  124. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_order.py +7 -0
  125. mirascope/api/_generated/traces/types/traces_search_by_env_response.py +26 -0
  126. mirascope/api/_generated/traces/types/traces_search_by_env_response_spans_item.py +50 -0
  127. mirascope/api/_generated/traces/types/traces_search_response_spans_item.py +10 -1
  128. mirascope/api/_generated/types/__init__.py +32 -2
  129. mirascope/api/_generated/types/bad_request_error_body.py +50 -0
  130. mirascope/api/_generated/types/date.py +3 -0
  131. mirascope/api/_generated/types/immutable_resource_error.py +22 -0
  132. mirascope/api/_generated/types/internal_server_error_body.py +3 -3
  133. mirascope/api/_generated/types/plan_limit_exceeded_error.py +32 -0
  134. mirascope/api/_generated/types/plan_limit_exceeded_error_tag.py +7 -0
  135. mirascope/api/_generated/types/pricing_unavailable_error.py +23 -0
  136. mirascope/api/_generated/types/rate_limit_error.py +31 -0
  137. mirascope/api/_generated/types/rate_limit_error_tag.py +5 -0
  138. mirascope/api/_generated/types/service_unavailable_error_body.py +24 -0
  139. mirascope/api/_generated/types/service_unavailable_error_tag.py +7 -0
  140. mirascope/api/_generated/types/subscription_past_due_error.py +31 -0
  141. mirascope/api/_generated/types/subscription_past_due_error_tag.py +7 -0
  142. mirascope/api/settings.py +19 -1
  143. mirascope/llm/__init__.py +53 -10
  144. mirascope/llm/calls/__init__.py +2 -1
  145. mirascope/llm/calls/calls.py +29 -20
  146. mirascope/llm/calls/decorator.py +21 -7
  147. mirascope/llm/content/tool_output.py +22 -5
  148. mirascope/llm/exceptions.py +284 -71
  149. mirascope/llm/formatting/__init__.py +17 -0
  150. mirascope/llm/formatting/format.py +112 -35
  151. mirascope/llm/formatting/output_parser.py +178 -0
  152. mirascope/llm/formatting/partial.py +80 -7
  153. mirascope/llm/formatting/primitives.py +192 -0
  154. mirascope/llm/formatting/types.py +20 -8
  155. mirascope/llm/messages/__init__.py +3 -0
  156. mirascope/llm/messages/_utils.py +34 -0
  157. mirascope/llm/models/__init__.py +5 -0
  158. mirascope/llm/models/models.py +137 -69
  159. mirascope/llm/{providers/base → models}/params.py +7 -57
  160. mirascope/llm/models/thinking_config.py +61 -0
  161. mirascope/llm/prompts/_utils.py +0 -32
  162. mirascope/llm/prompts/decorator.py +16 -5
  163. mirascope/llm/prompts/prompts.py +160 -92
  164. mirascope/llm/providers/__init__.py +1 -4
  165. mirascope/llm/providers/anthropic/_utils/__init__.py +2 -0
  166. mirascope/llm/providers/anthropic/_utils/beta_decode.py +18 -9
  167. mirascope/llm/providers/anthropic/_utils/beta_encode.py +62 -13
  168. mirascope/llm/providers/anthropic/_utils/decode.py +18 -9
  169. mirascope/llm/providers/anthropic/_utils/encode.py +26 -7
  170. mirascope/llm/providers/anthropic/_utils/errors.py +2 -2
  171. mirascope/llm/providers/anthropic/beta_provider.py +64 -18
  172. mirascope/llm/providers/anthropic/provider.py +91 -33
  173. mirascope/llm/providers/base/__init__.py +0 -4
  174. mirascope/llm/providers/base/_utils.py +55 -6
  175. mirascope/llm/providers/base/base_provider.py +116 -37
  176. mirascope/llm/providers/google/_utils/__init__.py +2 -0
  177. mirascope/llm/providers/google/_utils/decode.py +20 -7
  178. mirascope/llm/providers/google/_utils/encode.py +26 -7
  179. mirascope/llm/providers/google/_utils/errors.py +3 -2
  180. mirascope/llm/providers/google/provider.py +64 -18
  181. mirascope/llm/providers/mirascope/_utils.py +13 -17
  182. mirascope/llm/providers/mirascope/provider.py +49 -18
  183. mirascope/llm/providers/mlx/_utils.py +7 -2
  184. mirascope/llm/providers/mlx/encoding/base.py +5 -2
  185. mirascope/llm/providers/mlx/encoding/transformers.py +5 -2
  186. mirascope/llm/providers/mlx/mlx.py +23 -6
  187. mirascope/llm/providers/mlx/provider.py +42 -13
  188. mirascope/llm/providers/openai/_utils/errors.py +2 -2
  189. mirascope/llm/providers/openai/completions/_utils/encode.py +20 -16
  190. mirascope/llm/providers/openai/completions/base_provider.py +40 -11
  191. mirascope/llm/providers/openai/provider.py +40 -10
  192. mirascope/llm/providers/openai/responses/_utils/__init__.py +2 -0
  193. mirascope/llm/providers/openai/responses/_utils/decode.py +19 -6
  194. mirascope/llm/providers/openai/responses/_utils/encode.py +22 -10
  195. mirascope/llm/providers/openai/responses/provider.py +56 -18
  196. mirascope/llm/providers/provider_registry.py +93 -19
  197. mirascope/llm/responses/__init__.py +6 -1
  198. mirascope/llm/responses/_utils.py +102 -12
  199. mirascope/llm/responses/base_response.py +5 -2
  200. mirascope/llm/responses/base_stream_response.py +115 -25
  201. mirascope/llm/responses/response.py +2 -1
  202. mirascope/llm/responses/root_response.py +89 -17
  203. mirascope/llm/responses/stream_response.py +6 -9
  204. mirascope/llm/tools/decorator.py +9 -4
  205. mirascope/llm/tools/tool_schema.py +17 -6
  206. mirascope/llm/tools/toolkit.py +35 -27
  207. mirascope/llm/tools/tools.py +45 -20
  208. mirascope/ops/__init__.py +4 -0
  209. mirascope/ops/_internal/closure.py +4 -1
  210. mirascope/ops/_internal/configuration.py +82 -31
  211. mirascope/ops/_internal/exporters/exporters.py +55 -35
  212. mirascope/ops/_internal/exporters/utils.py +37 -0
  213. mirascope/ops/_internal/instrumentation/llm/common.py +530 -0
  214. mirascope/ops/_internal/instrumentation/llm/cost.py +190 -0
  215. mirascope/ops/_internal/instrumentation/llm/encode.py +1 -1
  216. mirascope/ops/_internal/instrumentation/llm/llm.py +116 -1242
  217. mirascope/ops/_internal/instrumentation/llm/model.py +1798 -0
  218. mirascope/ops/_internal/instrumentation/llm/response.py +521 -0
  219. mirascope/ops/_internal/instrumentation/llm/serialize.py +300 -0
  220. mirascope/ops/_internal/protocols.py +83 -1
  221. mirascope/ops/_internal/traced_calls.py +18 -0
  222. mirascope/ops/_internal/traced_functions.py +125 -10
  223. mirascope/ops/_internal/tracing.py +78 -1
  224. mirascope/ops/_internal/utils.py +60 -4
  225. mirascope/ops/_internal/versioned_functions.py +1 -1
  226. {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/METADATA +12 -11
  227. mirascope-2.0.2.dist-info/RECORD +424 -0
  228. {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/licenses/LICENSE +1 -1
  229. mirascope-2.0.0a6.dist-info/RECORD +0 -316
  230. {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/WHEEL +0 -0
@@ -1,70 +1,292 @@
1
1
  """Mirascope llm exception hierarchy for unified error handling across providers."""
2
2
 
3
+ import json
3
4
  from typing import TYPE_CHECKING
4
5
 
6
+ from pydantic import ValidationError
7
+
5
8
  if TYPE_CHECKING:
6
- from .formatting import FormattingMode
7
9
  from .providers import ModelId, ProviderId
8
10
 
9
11
 
10
- class MirascopeLLMError(Exception):
12
+ class Error(Exception):
11
13
  """Base exception for all Mirascope LLM errors."""
12
14
 
13
- original_exception: Exception | None
14
- provider: "ProviderId | None"
15
15
 
16
+ class ProviderError(Error):
17
+ """Base class for errors that originate from a provider SDK.
16
18
 
17
- class APIError(MirascopeLLMError):
18
- """Base class for API-related errors."""
19
+ This wraps exceptions from provider libraries (OpenAI, Anthropic, etc.)
20
+ and provides a unified interface for error handling.
21
+ """
19
22
 
20
- status_code: int | None
23
+ provider: "ProviderId"
24
+ """The provider that raised this error."""
25
+
26
+ original_exception: Exception | None
27
+ """The original exception from the provider SDK, if available."""
21
28
 
22
- def __init__(self, message: str, status_code: int | None = None) -> None:
29
+ def __init__(
30
+ self,
31
+ message: str,
32
+ provider: "ProviderId",
33
+ original_exception: Exception | None = None,
34
+ ) -> None:
23
35
  super().__init__(message)
24
- self.status_code = status_code
36
+ self.provider = provider
37
+ self.original_exception = original_exception
38
+ if original_exception is not None:
39
+ self.__cause__ = original_exception
25
40
 
26
41
 
27
- class ConnectionError(MirascopeLLMError):
28
- """Raised when unable to connect to the API (network issues, timeouts)."""
42
+ class APIError(ProviderError):
43
+ """Base class for HTTP-level API errors."""
44
+
45
+ status_code: int | None
46
+ """The HTTP status code, if available."""
47
+
48
+ def __init__(
49
+ self,
50
+ message: str,
51
+ provider: "ProviderId",
52
+ status_code: int | None = None,
53
+ original_exception: Exception | None = None,
54
+ ) -> None:
55
+ super().__init__(message, provider, original_exception)
56
+ self.status_code = status_code
29
57
 
30
58
 
31
59
  class AuthenticationError(APIError):
32
60
  """Raised for authentication failures (401, invalid API keys)."""
33
61
 
34
- def __init__(self, message: str, status_code: int | None = None) -> None:
35
- super().__init__(message, status_code=status_code or 401)
62
+ def __init__(
63
+ self,
64
+ message: str,
65
+ provider: "ProviderId",
66
+ status_code: int | None = None,
67
+ original_exception: Exception | None = None,
68
+ ) -> None:
69
+ super().__init__(
70
+ message,
71
+ provider,
72
+ status_code=status_code or 401,
73
+ original_exception=original_exception,
74
+ )
36
75
 
37
76
 
38
77
  class PermissionError(APIError):
39
78
  """Raised for permission/authorization failures (403)."""
40
79
 
41
- def __init__(self, message: str, status_code: int | None = None) -> None:
42
- super().__init__(message, status_code=status_code or 403)
80
+ def __init__(
81
+ self,
82
+ message: str,
83
+ provider: "ProviderId",
84
+ status_code: int | None = None,
85
+ original_exception: Exception | None = None,
86
+ ) -> None:
87
+ super().__init__(
88
+ message,
89
+ provider,
90
+ status_code=status_code or 403,
91
+ original_exception=original_exception,
92
+ )
43
93
 
44
94
 
45
95
  class BadRequestError(APIError):
46
96
  """Raised for malformed requests (400, 422)."""
47
97
 
48
- def __init__(self, message: str, status_code: int | None = None) -> None:
49
- super().__init__(message, status_code=status_code or 400)
98
+ def __init__(
99
+ self,
100
+ message: str,
101
+ provider: "ProviderId",
102
+ status_code: int | None = None,
103
+ original_exception: Exception | None = None,
104
+ ) -> None:
105
+ super().__init__(
106
+ message,
107
+ provider,
108
+ status_code=status_code or 400,
109
+ original_exception=original_exception,
110
+ )
50
111
 
51
112
 
52
113
  class NotFoundError(APIError):
53
114
  """Raised when requested resource is not found (404)."""
54
115
 
55
- def __init__(self, message: str, status_code: int | None = None) -> None:
56
- super().__init__(message, status_code=status_code or 404)
116
+ def __init__(
117
+ self,
118
+ message: str,
119
+ provider: "ProviderId",
120
+ status_code: int | None = None,
121
+ original_exception: Exception | None = None,
122
+ ) -> None:
123
+ super().__init__(
124
+ message,
125
+ provider,
126
+ status_code=status_code or 404,
127
+ original_exception=original_exception,
128
+ )
129
+
130
+
131
+ class RateLimitError(APIError):
132
+ """Raised when rate limits are exceeded (429)."""
133
+
134
+ def __init__(
135
+ self,
136
+ message: str,
137
+ provider: "ProviderId",
138
+ status_code: int | None = None,
139
+ original_exception: Exception | None = None,
140
+ ) -> None:
141
+ super().__init__(
142
+ message,
143
+ provider,
144
+ status_code=status_code or 429,
145
+ original_exception=original_exception,
146
+ )
147
+
148
+
149
+ class ServerError(APIError):
150
+ """Raised for server-side errors (500+)."""
151
+
152
+ def __init__(
153
+ self,
154
+ message: str,
155
+ provider: "ProviderId",
156
+ status_code: int | None = None,
157
+ original_exception: Exception | None = None,
158
+ ) -> None:
159
+ super().__init__(
160
+ message,
161
+ provider,
162
+ status_code=status_code or 500,
163
+ original_exception=original_exception,
164
+ )
165
+
166
+
167
+ class ConnectionError(ProviderError):
168
+ """Raised when unable to connect to the API (network issues, timeouts)."""
169
+
170
+
171
+ class TimeoutError(ProviderError):
172
+ """Raised when requests timeout or deadline exceeded."""
173
+
174
+
175
+ class ResponseValidationError(ProviderError):
176
+ """Raised when API response fails validation.
177
+
178
+ This wraps the APIResponseValidationErrors that OpenAI and Anthropic both return.
179
+ """
180
+
181
+
182
+ class ToolError(Error):
183
+ """Base class for errors that occur during tool execution."""
184
+
185
+
186
+ class ToolExecutionError(ToolError):
187
+ """Raised if an uncaught exception is thrown while executing a tool."""
188
+
189
+ tool_exception: Exception
190
+ """The exception that was thrown while executing the tool."""
191
+
192
+ def __init__(self, tool_exception: Exception | str) -> None:
193
+ if isinstance(tool_exception, str):
194
+ # Support string for snapshot reconstruction
195
+ message = tool_exception
196
+ tool_exception = ValueError(message)
197
+ else:
198
+ message = str(tool_exception)
199
+ super().__init__(message)
200
+ self.tool_exception = tool_exception
201
+ self.__cause__ = tool_exception
202
+
203
+ def __eq__(self, other: object) -> bool:
204
+ if not isinstance(other, ToolExecutionError):
205
+ return False
206
+ # Needed for snapshot tests.
207
+ return str(self) == str(other)
208
+
209
+
210
+ class ToolNotFoundError(ToolError):
211
+ """Raised if a tool call does not match any registered tool."""
212
+
213
+ tool_name: str
214
+ """The name of the tool that was not found."""
215
+
216
+ def __init__(self, tool_name: str) -> None:
217
+ super().__init__(f"Tool '{tool_name}' not found in registered tools")
218
+ self.tool_name = tool_name
219
+
220
+ def __repr__(self) -> str:
221
+ return f"ToolNotFoundError({self.tool_name!r})"
57
222
 
223
+ def __eq__(self, other: object) -> bool:
224
+ if not isinstance(other, ToolNotFoundError):
225
+ return NotImplemented
226
+ return self.tool_name == other.tool_name
58
227
 
59
- class ToolNotFoundError(MirascopeLLMError):
60
- """Raised if a tool_call cannot be converted to any corresponding tool."""
228
+ def __hash__(self) -> int:
229
+ return hash((type(self), self.tool_name))
61
230
 
62
231
 
63
- class FeatureNotSupportedError(MirascopeLLMError):
232
+ class ParseError(Error):
233
+ """Raised when response.parse() fails to parse the response content.
234
+
235
+ This wraps errors from JSON extraction, JSON parsing, Pydantic validation,
236
+ or custom OutputParser functions.
237
+ """
238
+
239
+ original_exception: Exception
240
+ """The original exception that caused the parse failure."""
241
+
242
+ def __init__(
243
+ self,
244
+ message: str,
245
+ original_exception: Exception,
246
+ ) -> None:
247
+ super().__init__(message)
248
+ self.original_exception = original_exception
249
+ self.__cause__ = original_exception
250
+
251
+ def retry_message(self) -> str:
252
+ """Generate a message suitable for retrying with the LLM.
253
+
254
+ Returns a user-friendly message describing what went wrong,
255
+ suitable for including in a retry prompt.
256
+ """
257
+
258
+ if isinstance(self.original_exception, ValidationError):
259
+ return (
260
+ f"Your response failed schema validation:\n"
261
+ f"{self.original_exception}\n\n"
262
+ "Please correct these issues and respond again."
263
+ )
264
+ elif isinstance(self.original_exception, json.JSONDecodeError):
265
+ # JSON syntax error
266
+ return (
267
+ "Your response could not be parsed because no valid JSON object "
268
+ "was found. Please ensure your response contains a JSON object "
269
+ "with opening '{' and closing '}' braces."
270
+ )
271
+ else:
272
+ # ValueError from JSON extraction, or OutputParser error
273
+ return (
274
+ f"Your response could not be parsed: {self.original_exception}\n\n"
275
+ "Please ensure your response matches the expected format."
276
+ )
277
+
278
+ def __eq__(self, other: object) -> bool:
279
+ if not isinstance(other, ParseError):
280
+ return False
281
+ return str(self) == str(other)
282
+
283
+
284
+ class FeatureNotSupportedError(Error):
64
285
  """Raised if a Mirascope feature is unsupported by chosen provider.
65
286
 
66
287
  If compatibility is model-specific, then `model_id` should be specified.
67
- If the feature is not supported by the provider at all, then it may be `None`."""
288
+ If the feature is not supported by the provider at all, then it may be `None`.
289
+ """
68
290
 
69
291
  provider_id: "ProviderId"
70
292
  model_id: "ModelId | None"
@@ -86,54 +308,7 @@ class FeatureNotSupportedError(MirascopeLLMError):
86
308
  self.model_id = model_id
87
309
 
88
310
 
89
- class FormattingModeNotSupportedError(FeatureNotSupportedError):
90
- """Raised when trying to use a formatting mode that is not supported by the chosen model."""
91
-
92
- formatting_mode: "FormattingMode"
93
-
94
- def __init__(
95
- self,
96
- formatting_mode: "FormattingMode",
97
- provider_id: "ProviderId",
98
- model_id: "ModelId | None" = None,
99
- message: str | None = None,
100
- ) -> None:
101
- if message is None:
102
- model_msg = f" for model '{model_id}'" if model_id is not None else ""
103
- message = f"Formatting mode '{formatting_mode}' is not supported by provider '{provider_id}'{model_msg}"
104
- super().__init__(
105
- feature=f"formatting_mode:{formatting_mode}",
106
- provider_id=provider_id,
107
- model_id=model_id,
108
- message=message,
109
- )
110
- self.formatting_mode = formatting_mode
111
-
112
-
113
- class RateLimitError(APIError):
114
- """Raised when rate limits are exceeded (429)."""
115
-
116
- def __init__(self, message: str, status_code: int | None = None) -> None:
117
- super().__init__(message, status_code=status_code or 429)
118
-
119
-
120
- class ServerError(APIError):
121
- """Raised for server-side errors (500+)."""
122
-
123
- def __init__(self, message: str, status_code: int | None = None) -> None:
124
- super().__init__(message, status_code=status_code or 500)
125
-
126
-
127
- class TimeoutError(MirascopeLLMError):
128
- """Raised when requests timeout or deadline exceeded."""
129
-
130
-
131
- # This wraps the APIResponseValidationErrors that OpenAI and Anthropic both return.
132
- class ResponseValidationError(MirascopeLLMError):
133
- """Raised when API response fails validation."""
134
-
135
-
136
- class NoRegisteredProviderError(MirascopeLLMError):
311
+ class NoRegisteredProviderError(Error):
137
312
  """Raised when no provider is registered for a given model_id."""
138
313
 
139
314
  model_id: str
@@ -145,3 +320,41 @@ class NoRegisteredProviderError(MirascopeLLMError):
145
320
  )
146
321
  super().__init__(message)
147
322
  self.model_id = model_id
323
+
324
+
325
+ class MissingAPIKeyError(Error):
326
+ """Raised when no API key is available for a provider.
327
+
328
+ This error is raised during auto-registration when the required API key
329
+ environment variable is not set. If a Mirascope fallback is available,
330
+ the error message will suggest using MIRASCOPE_API_KEY as an alternative.
331
+ """
332
+
333
+ provider_id: str
334
+ """The provider that requires an API key."""
335
+
336
+ env_var: str
337
+ """The environment variable that should contain the API key."""
338
+
339
+ def __init__(
340
+ self,
341
+ provider_id: str,
342
+ env_var: str,
343
+ has_mirascope_fallback: bool = False,
344
+ ) -> None:
345
+ if has_mirascope_fallback:
346
+ message = (
347
+ f"No API key found for {provider_id}. Either:\n"
348
+ f" 1. Set {env_var} environment variable, or\n"
349
+ f" 2. Set MIRASCOPE_API_KEY for cross-provider support "
350
+ f"via Mirascope Router\n"
351
+ f" (Learn more: https://mirascope.com/docs/router)"
352
+ )
353
+ else:
354
+ message = (
355
+ f"No API key found for {provider_id}. "
356
+ f"Set the {env_var} environment variable."
357
+ )
358
+ super().__init__(message)
359
+ self.provider_id = provider_id
360
+ self.env_var = env_var
@@ -3,11 +3,21 @@
3
3
  This module provides a way to define structured output formats for LLM responses.
4
4
  The `@format` decorator can be applied to classes to specify how LLM
5
5
  outputs should be structured and parsed.
6
+
7
+ The `@output_parser` decorator can be used to create custom parsers for non-JSON
8
+ formats like XML, YAML, CSV, or any custom text structure.
6
9
  """
7
10
 
8
11
  from .format import Format, format, resolve_format
9
12
  from .from_call_args import FromCallArgs
13
+ from .output_parser import OutputParser, is_output_parser, output_parser
10
14
  from .partial import Partial
15
+ from .primitives import (
16
+ PrimitiveType,
17
+ PrimitiveWrapperModel,
18
+ create_wrapper_model,
19
+ is_primitive_type,
20
+ )
11
21
  from .types import FormattableT, FormattingMode
12
22
 
13
23
  __all__ = [
@@ -16,7 +26,14 @@ __all__ = [
16
26
  "FormattableT",
17
27
  "FormattingMode",
18
28
  "FromCallArgs",
29
+ "OutputParser",
19
30
  "Partial",
31
+ "PrimitiveType",
32
+ "PrimitiveWrapperModel",
33
+ "create_wrapper_model",
20
34
  "format",
35
+ "is_output_parser",
36
+ "is_primitive_type",
37
+ "output_parser",
21
38
  "resolve_format",
22
39
  ]
@@ -1,5 +1,7 @@
1
1
  """The `llm.format` decorator for defining response formats as classes."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import inspect
4
6
  import json
5
7
  from dataclasses import dataclass
@@ -7,6 +9,8 @@ from typing import Any, Generic, cast
7
9
 
8
10
  from ..tools import FORMAT_TOOL_NAME, ToolFn, ToolParameterSchema, ToolSchema
9
11
  from ..types import NoneType
12
+ from .output_parser import OutputParser, is_output_parser
13
+ from .primitives import create_wrapper_model, is_primitive_type
10
14
  from .types import FormattableT, FormattingMode, HasFormattingInstructions
11
15
 
12
16
  TOOL_MODE_INSTRUCTIONS = f"""Always respond to the user's query using the {FORMAT_TOOL_NAME} tool for structured output."""
@@ -49,35 +53,45 @@ class Format(Generic[FormattableT]):
49
53
  """JSON schema representation of the structured output format."""
50
54
 
51
55
  mode: FormattingMode
52
- """The decorator-provided mode of the response format.
53
-
56
+ """The decorator-provided mode of the response format.
57
+
54
58
  Determines how the LLM call may be modified in order to extract the expected format.
55
59
  """
56
60
 
57
- formattable: type[FormattableT]
58
- """The `Formattable` type that this `Format` describes.
59
-
60
- While the `FormattbleT` typevar allows for `None`, a `Format` will never be
61
- constructed when the `FormattableT` is `None`, so you may treat this as
62
- a `RequiredFormattableT` in practice.
61
+ formattable: type[FormattableT] | OutputParser[FormattableT]
62
+ """The formattable type or custom output parser.
63
+
64
+ Can be one of:
65
+ - type[BaseModel]: A Pydantic model class for structured output
66
+ - PrimitiveType: A primitive type (str, int, list, etc.) for simple output
67
+ - OutputParser[FormattableT]: A custom parser created with @llm.output_parser
68
+
69
+ The type determines how the response will be parsed in response.parse().
70
+ OutputParser uses Any for the response type since it works with any response.
63
71
  """
64
72
 
65
73
  @property
66
74
  def formatting_instructions(self) -> str | None:
67
75
  """The formatting instructions that will be added to the LLM system prompt.
68
76
 
69
- If the format type has a `formatting_instructions` class method, the output of that
70
- call will be used for instructions. Otherwise, instructions may be auto-generated
71
- based on the formatting mode.
77
+ If the format has a custom `OutputParser`, its formatting instructions will be used.
78
+ Otherwise, if the format type has a `formatting_instructions` class method,
79
+ the output of that call will be used. Otherwise, instructions may be
80
+ auto-generated based on the formatting mode.
72
81
  """
73
- if isinstance(self.formattable, HasFormattingInstructions):
82
+ if is_output_parser(self.formattable) or isinstance(
83
+ self.formattable, HasFormattingInstructions
84
+ ):
74
85
  return self.formattable.formatting_instructions()
86
+
75
87
  if self.mode == "tool":
76
88
  return TOOL_MODE_INSTRUCTIONS
77
89
  elif self.mode == "json":
78
90
  json_schema = json.dumps(self.schema, indent=2)
79
91
  instructions = JSON_MODE_INSTRUCTIONS.format(json_schema=json_schema)
80
92
  return inspect.cleandoc(instructions)
93
+ elif self.mode == "parser":
94
+ return None # pragma: no cover
81
95
 
82
96
  def create_tool_schema(
83
97
  self,
@@ -121,30 +135,39 @@ class Format(Generic[FormattableT]):
121
135
  name=FORMAT_TOOL_NAME,
122
136
  description=description,
123
137
  parameters=parameters,
124
- strict=True,
138
+ strict=None, # Provider determines whether to use strict mode.
125
139
  )
126
140
 
127
141
 
128
142
  def format(
129
- formattable: type[FormattableT] | None,
143
+ formattable: type[FormattableT] | OutputParser[FormattableT] | None,
130
144
  *,
131
145
  mode: FormattingMode,
132
146
  ) -> Format[FormattableT] | None:
133
- """Returns a `Format` that describes structured output for a Formattable type.
147
+ """Returns a `Format` that describes structured output or custom parsing.
148
+
149
+ This function converts a Formattable type (e.g. Pydantic `BaseModel` or primitive type)
150
+ or an `OutputParser` into a `Format` object that describes how the output should be
151
+ formatted and parsed. Calling `llm.format` is optional, as all the APIs that expect
152
+ a `Format` can also take the Formattable type or `OutputParser` directly. However,
153
+ calling `llm.format` is necessary in order to specify the formatting mode for
154
+ `BaseModel`/primitive types.
134
155
 
135
- This function converts a Formattable type (e.g. Pydantic BaseModel) into a `Format`
136
- object that describes how the object should be formatted. Calling `llm.format`
137
- is optional, as all the APIs that expect a `Format` can also take the Formattable
138
- type directly. However, calling `llm.format` is necessary in order to specify the
139
- formatting mode that will be used.
156
+ Primitive types are automatically wrapped in a `BaseModel` with an "output" field
157
+ for schema generation, then unwrapped during parsing.
140
158
 
141
159
  Args:
142
- mode: The format mode to use, one of the following:
160
+ formattable: The type or parser to format:
161
+ - BaseModel type: Uses structured output with JSON schema
162
+ - Primitive type: Wrapped in schema for structured output
163
+ - OutputParser: Uses custom parsing with instructions
164
+ mode: The format mode to use (required):
143
165
  - "strict": Use model strict structured outputs, or fail if unavailable.
144
166
  - "tool": Use forced tool calling with a special tool that represents a
145
167
  formatted response.
146
168
  - "json": Use provider json mode if available, or modify prompt to request
147
169
  json if not.
170
+ - "parser": Must be used for OutputParser types.
148
171
 
149
172
  The Formattable type may provide custom formatting instructions via a
150
173
  `formatting_instructions(cls)` classmethod. If that method is present, it will be called,
@@ -155,34 +178,44 @@ def format(
155
178
  you can add the `formatting_instructions` classmethod and have it return `None`.
156
179
 
157
180
  Returns:
158
- A `Format` object describing the Formattable type.
181
+ A `Format` object describing the format type or parser.
159
182
 
160
183
  Example:
161
- Using with an LLM call:
184
+ Using with a BaseModel:
162
185
 
163
186
  ```python
164
187
  from pydantic import BaseModel
165
-
166
188
  from mirascope import llm
167
189
 
168
-
169
190
  class Book(BaseModel):
170
191
  title: str
171
192
  author: str
172
193
 
173
194
  format = llm.format(Book, mode="strict")
174
195
 
175
- @llm.call(
176
- provider_id="openai",
177
- model_id="openai/gpt-5-mini",
178
- format=format,
179
- )
196
+ @llm.call("openai/gpt-5-mini", format=format)
180
197
  def recommend_book(genre: str):
181
198
  return f"Recommend a {genre} book."
182
199
 
183
200
  response = recommend_book("fantasy")
184
201
  book: Book = response.parse()
185
- print(f"{book.title} by {book.author}")
202
+ ```
203
+
204
+ Example:
205
+
206
+ Using with an `OutputParser`:
207
+
208
+ ```python
209
+ @llm.output_parser(
210
+ formatting_instructions="Return XML: <book><title>...</title></book>"
211
+ )
212
+ def parse_book_xml(response: llm.AnyResponse) -> Book:
213
+ # ... parsing logic ...
214
+ return Book(...)
215
+
216
+ @llm.call("openai/gpt-5-mini", format=parse_book_xml)
217
+ def recommend_book(genre: str):
218
+ return f"Recommend a {genre} book."
186
219
  ```
187
220
  """
188
221
  # TODO: Add caching or memoization to this function (e.g. functools.lru_cache)
@@ -190,6 +223,34 @@ def format(
190
223
  if formattable is None or formattable is NoneType:
191
224
  return None
192
225
 
226
+ if is_output_parser(formattable):
227
+ if mode != "parser":
228
+ raise ValueError(f"mode must be 'parser' for OutputParser, got '{mode}'")
229
+ return Format[Any](
230
+ name=formattable.__name__,
231
+ description=formattable.__doc__,
232
+ schema={},
233
+ mode="parser",
234
+ formattable=formattable,
235
+ )
236
+
237
+ if is_primitive_type(formattable):
238
+ wrapper_model = create_wrapper_model(formattable)
239
+ schema = wrapper_model.model_json_schema()
240
+ name = (
241
+ formattable.__name__
242
+ if hasattr(formattable, "__name__")
243
+ else str(formattable)
244
+ )
245
+
246
+ return Format[FormattableT](
247
+ name=name,
248
+ description=None,
249
+ schema=schema,
250
+ mode=mode,
251
+ formattable=formattable,
252
+ )
253
+
193
254
  description = None
194
255
  if formattable.__doc__:
195
256
  description = inspect.cleandoc(formattable.__doc__)
@@ -206,11 +267,27 @@ def format(
206
267
 
207
268
 
208
269
  def resolve_format(
209
- formattable: type[FormattableT] | Format[FormattableT] | None,
270
+ formattable: (
271
+ type[FormattableT] | Format[FormattableT] | OutputParser[FormattableT] | None
272
+ ),
210
273
  default_mode: FormattingMode,
211
274
  ) -> Format[FormattableT] | None:
212
- """Resolve a `Format` (or None) from a possible `Format` or Formattable."""
275
+ """Resolve a `Format` (or None) from a possible `Format`, Formattable, or `OutputParser`.
276
+
277
+ Args:
278
+ formattable: The format specification:
279
+ - Format: Returned as-is
280
+ - BaseModel/primitive type: Converted to Format with default_mode
281
+ - OutputParser: Converted to Format with mode='parser'
282
+ default_mode: The mode to use for BaseModel/primitive types.
283
+
284
+ Returns:
285
+ A Format object or None.
286
+ """
213
287
  if isinstance(formattable, Format):
214
288
  return formattable
215
- else:
216
- return format(formattable, mode=default_mode)
289
+
290
+ if is_output_parser(formattable):
291
+ return format(formattable, mode="parser")
292
+
293
+ return format(formattable, mode=default_mode)