mirascope 2.0.0a4__py3-none-any.whl → 2.0.0a6__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 (215) hide show
  1. mirascope/__init__.py +10 -1
  2. mirascope/_stubs.py +363 -0
  3. mirascope/api/__init__.py +8 -0
  4. mirascope/api/_generated/__init__.py +119 -1
  5. mirascope/api/_generated/annotations/__init__.py +33 -0
  6. mirascope/api/_generated/annotations/client.py +474 -0
  7. mirascope/api/_generated/annotations/raw_client.py +1095 -0
  8. mirascope/api/_generated/annotations/types/__init__.py +31 -0
  9. mirascope/api/_generated/annotations/types/annotations_create_request_label.py +5 -0
  10. mirascope/api/_generated/annotations/types/annotations_create_response.py +35 -0
  11. mirascope/api/_generated/annotations/types/annotations_create_response_label.py +5 -0
  12. mirascope/api/_generated/annotations/types/annotations_get_response.py +35 -0
  13. mirascope/api/_generated/annotations/types/annotations_get_response_label.py +5 -0
  14. mirascope/api/_generated/annotations/types/annotations_list_request_label.py +5 -0
  15. mirascope/api/_generated/annotations/types/annotations_list_response.py +21 -0
  16. mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item.py +35 -0
  17. mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item_label.py +5 -0
  18. mirascope/api/_generated/annotations/types/annotations_update_request_label.py +5 -0
  19. mirascope/api/_generated/annotations/types/annotations_update_response.py +35 -0
  20. mirascope/api/_generated/annotations/types/annotations_update_response_label.py +5 -0
  21. mirascope/api/_generated/api_keys/__init__.py +7 -0
  22. mirascope/api/_generated/api_keys/client.py +429 -0
  23. mirascope/api/_generated/api_keys/raw_client.py +788 -0
  24. mirascope/api/_generated/api_keys/types/__init__.py +9 -0
  25. mirascope/api/_generated/api_keys/types/api_keys_create_response.py +28 -0
  26. mirascope/api/_generated/api_keys/types/api_keys_get_response.py +27 -0
  27. mirascope/api/_generated/api_keys/types/api_keys_list_response_item.py +27 -0
  28. mirascope/api/_generated/client.py +12 -0
  29. mirascope/api/_generated/core/client_wrapper.py +2 -14
  30. mirascope/api/_generated/core/datetime_utils.py +1 -3
  31. mirascope/api/_generated/core/file.py +2 -5
  32. mirascope/api/_generated/core/http_client.py +36 -112
  33. mirascope/api/_generated/core/jsonable_encoder.py +1 -3
  34. mirascope/api/_generated/core/pydantic_utilities.py +19 -74
  35. mirascope/api/_generated/core/query_encoder.py +1 -3
  36. mirascope/api/_generated/core/serialization.py +4 -10
  37. mirascope/api/_generated/docs/client.py +2 -6
  38. mirascope/api/_generated/docs/raw_client.py +4 -20
  39. mirascope/api/_generated/environments/__init__.py +17 -0
  40. mirascope/api/_generated/environments/client.py +500 -0
  41. mirascope/api/_generated/environments/raw_client.py +999 -0
  42. mirascope/api/_generated/environments/types/__init__.py +15 -0
  43. mirascope/api/_generated/environments/types/environments_create_response.py +24 -0
  44. mirascope/api/_generated/environments/types/environments_get_response.py +24 -0
  45. mirascope/api/_generated/environments/types/environments_list_response_item.py +24 -0
  46. mirascope/api/_generated/environments/types/environments_update_response.py +24 -0
  47. mirascope/api/_generated/errors/__init__.py +2 -0
  48. mirascope/api/_generated/errors/bad_request_error.py +1 -5
  49. mirascope/api/_generated/errors/conflict_error.py +1 -5
  50. mirascope/api/_generated/errors/forbidden_error.py +1 -5
  51. mirascope/api/_generated/errors/internal_server_error.py +1 -6
  52. mirascope/api/_generated/errors/not_found_error.py +1 -5
  53. mirascope/api/_generated/errors/unauthorized_error.py +11 -0
  54. mirascope/api/_generated/functions/__init__.py +29 -0
  55. mirascope/api/_generated/functions/client.py +433 -0
  56. mirascope/api/_generated/functions/raw_client.py +1049 -0
  57. mirascope/api/_generated/functions/types/__init__.py +29 -0
  58. mirascope/api/_generated/functions/types/functions_create_request_dependencies_value.py +20 -0
  59. mirascope/api/_generated/functions/types/functions_create_response.py +37 -0
  60. mirascope/api/_generated/functions/types/functions_create_response_dependencies_value.py +20 -0
  61. mirascope/api/_generated/functions/types/functions_find_by_hash_response.py +39 -0
  62. mirascope/api/_generated/functions/types/functions_find_by_hash_response_dependencies_value.py +20 -0
  63. mirascope/api/_generated/functions/types/functions_get_response.py +37 -0
  64. mirascope/api/_generated/functions/types/functions_get_response_dependencies_value.py +20 -0
  65. mirascope/api/_generated/functions/types/functions_list_response.py +21 -0
  66. mirascope/api/_generated/functions/types/functions_list_response_functions_item.py +41 -0
  67. mirascope/api/_generated/functions/types/functions_list_response_functions_item_dependencies_value.py +20 -0
  68. mirascope/api/_generated/health/client.py +2 -6
  69. mirascope/api/_generated/health/raw_client.py +5 -23
  70. mirascope/api/_generated/health/types/health_check_response.py +1 -3
  71. mirascope/api/_generated/organizations/__init__.py +2 -0
  72. mirascope/api/_generated/organizations/client.py +94 -27
  73. mirascope/api/_generated/organizations/raw_client.py +246 -128
  74. mirascope/api/_generated/organizations/types/__init__.py +2 -0
  75. mirascope/api/_generated/organizations/types/organizations_create_response.py +5 -3
  76. mirascope/api/_generated/organizations/types/organizations_create_response_role.py +1 -3
  77. mirascope/api/_generated/organizations/types/organizations_credits_response.py +19 -0
  78. mirascope/api/_generated/organizations/types/organizations_get_response.py +5 -3
  79. mirascope/api/_generated/organizations/types/organizations_get_response_role.py +1 -3
  80. mirascope/api/_generated/organizations/types/organizations_list_response_item.py +5 -3
  81. mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +1 -3
  82. mirascope/api/_generated/organizations/types/organizations_update_response.py +5 -3
  83. mirascope/api/_generated/organizations/types/organizations_update_response_role.py +1 -3
  84. mirascope/api/_generated/projects/__init__.py +2 -12
  85. mirascope/api/_generated/projects/client.py +38 -68
  86. mirascope/api/_generated/projects/raw_client.py +92 -163
  87. mirascope/api/_generated/projects/types/__init__.py +1 -6
  88. mirascope/api/_generated/projects/types/projects_create_response.py +4 -9
  89. mirascope/api/_generated/projects/types/projects_get_response.py +4 -9
  90. mirascope/api/_generated/projects/types/projects_list_response_item.py +4 -9
  91. mirascope/api/_generated/projects/types/projects_update_response.py +4 -9
  92. mirascope/api/_generated/reference.md +1862 -70
  93. mirascope/api/_generated/traces/__init__.py +22 -0
  94. mirascope/api/_generated/traces/client.py +398 -0
  95. mirascope/api/_generated/traces/raw_client.py +902 -18
  96. mirascope/api/_generated/traces/types/__init__.py +32 -0
  97. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item.py +4 -11
  98. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource.py +2 -6
  99. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item.py +1 -3
  100. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value.py +8 -24
  101. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_array_value.py +2 -6
  102. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value.py +3 -9
  103. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value_values_item.py +2 -6
  104. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item.py +3 -9
  105. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope.py +4 -8
  106. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item.py +2 -6
  107. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value.py +8 -24
  108. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_array_value.py +2 -6
  109. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value.py +3 -9
  110. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value_values_item.py +1 -3
  111. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item.py +6 -18
  112. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item.py +3 -9
  113. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value.py +8 -24
  114. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_array_value.py +2 -6
  115. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value.py +2 -6
  116. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value_values_item.py +1 -3
  117. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_status.py +2 -6
  118. mirascope/api/_generated/traces/types/traces_create_response.py +2 -5
  119. mirascope/api/_generated/traces/types/traces_create_response_partial_success.py +3 -9
  120. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py +54 -0
  121. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_functions_item.py +24 -0
  122. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_models_item.py +22 -0
  123. mirascope/api/_generated/traces/types/traces_get_trace_detail_response.py +33 -0
  124. mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py +90 -0
  125. mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item.py +26 -0
  126. mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item_operator.py +7 -0
  127. mirascope/api/_generated/traces/types/traces_search_request_sort_by.py +7 -0
  128. mirascope/api/_generated/traces/types/traces_search_request_sort_order.py +5 -0
  129. mirascope/api/_generated/traces/types/traces_search_response.py +26 -0
  130. mirascope/api/_generated/traces/types/traces_search_response_spans_item.py +41 -0
  131. mirascope/api/_generated/types/__init__.py +18 -0
  132. mirascope/api/_generated/types/already_exists_error.py +1 -3
  133. mirascope/api/_generated/types/click_house_error.py +22 -0
  134. mirascope/api/_generated/types/database_error.py +1 -3
  135. mirascope/api/_generated/types/http_api_decode_error.py +1 -3
  136. mirascope/api/_generated/types/internal_server_error_body.py +49 -0
  137. mirascope/api/_generated/types/issue.py +1 -3
  138. mirascope/api/_generated/types/issue_tag.py +1 -8
  139. mirascope/api/_generated/types/not_found_error_body.py +1 -3
  140. mirascope/api/_generated/types/number_from_string.py +3 -0
  141. mirascope/api/_generated/types/permission_denied_error.py +1 -3
  142. mirascope/api/_generated/types/permission_denied_error_tag.py +1 -3
  143. mirascope/api/_generated/types/property_key_key.py +1 -3
  144. mirascope/api/_generated/types/stripe_error.py +20 -0
  145. mirascope/api/_generated/types/unauthorized_error_body.py +21 -0
  146. mirascope/api/_generated/types/unauthorized_error_tag.py +5 -0
  147. mirascope/llm/__init__.py +6 -2
  148. mirascope/llm/content/tool_call.py +6 -0
  149. mirascope/llm/exceptions.py +28 -0
  150. mirascope/llm/formatting/__init__.py +2 -2
  151. mirascope/llm/formatting/format.py +120 -8
  152. mirascope/llm/formatting/types.py +1 -56
  153. mirascope/llm/mcp/__init__.py +2 -2
  154. mirascope/llm/mcp/mcp_client.py +130 -0
  155. mirascope/llm/providers/__init__.py +26 -5
  156. mirascope/llm/providers/anthropic/__init__.py +3 -21
  157. mirascope/llm/providers/anthropic/_utils/__init__.py +2 -0
  158. mirascope/llm/providers/anthropic/_utils/beta_decode.py +4 -2
  159. mirascope/llm/providers/anthropic/_utils/beta_encode.py +13 -12
  160. mirascope/llm/providers/anthropic/_utils/decode.py +4 -2
  161. mirascope/llm/providers/anthropic/_utils/encode.py +57 -14
  162. mirascope/llm/providers/anthropic/_utils/errors.py +46 -0
  163. mirascope/llm/providers/anthropic/beta_provider.py +6 -0
  164. mirascope/llm/providers/anthropic/provider.py +5 -0
  165. mirascope/llm/providers/base/__init__.py +5 -2
  166. mirascope/llm/providers/base/_utils.py +2 -7
  167. mirascope/llm/providers/base/base_provider.py +173 -58
  168. mirascope/llm/providers/base/params.py +63 -34
  169. mirascope/llm/providers/google/__init__.py +2 -17
  170. mirascope/llm/providers/google/_utils/__init__.py +2 -0
  171. mirascope/llm/providers/google/_utils/decode.py +17 -8
  172. mirascope/llm/providers/google/_utils/encode.py +105 -16
  173. mirascope/llm/providers/google/_utils/errors.py +49 -0
  174. mirascope/llm/providers/google/model_info.py +1 -0
  175. mirascope/llm/providers/google/provider.py +9 -5
  176. mirascope/llm/providers/mirascope/__init__.py +5 -0
  177. mirascope/llm/providers/mirascope/_utils.py +77 -0
  178. mirascope/llm/providers/mirascope/provider.py +318 -0
  179. mirascope/llm/providers/mlx/__init__.py +2 -17
  180. mirascope/llm/providers/mlx/_utils.py +9 -2
  181. mirascope/llm/providers/mlx/provider.py +8 -0
  182. mirascope/llm/providers/ollama/__init__.py +1 -13
  183. mirascope/llm/providers/openai/__init__.py +10 -1
  184. mirascope/llm/providers/openai/_utils/__init__.py +5 -0
  185. mirascope/llm/providers/openai/_utils/errors.py +46 -0
  186. mirascope/llm/providers/openai/completions/__init__.py +2 -20
  187. mirascope/llm/providers/openai/completions/_utils/decode.py +14 -3
  188. mirascope/llm/providers/openai/completions/_utils/encode.py +15 -12
  189. mirascope/llm/providers/openai/completions/base_provider.py +6 -6
  190. mirascope/llm/providers/openai/provider.py +14 -1
  191. mirascope/llm/providers/openai/responses/__init__.py +1 -17
  192. mirascope/llm/providers/openai/responses/_utils/decode.py +2 -2
  193. mirascope/llm/providers/openai/responses/_utils/encode.py +43 -15
  194. mirascope/llm/providers/openai/responses/provider.py +13 -7
  195. mirascope/llm/providers/provider_id.py +1 -0
  196. mirascope/llm/providers/provider_registry.py +59 -3
  197. mirascope/llm/providers/together/__init__.py +1 -13
  198. mirascope/llm/responses/base_stream_response.py +24 -20
  199. mirascope/llm/tools/decorator.py +8 -4
  200. mirascope/llm/tools/tool_schema.py +33 -6
  201. mirascope/llm/tools/tools.py +84 -16
  202. mirascope/ops/__init__.py +60 -109
  203. mirascope/ops/_internal/closure.py +62 -11
  204. mirascope/ops/_internal/instrumentation/llm/llm.py +1 -2
  205. mirascope/ops/_internal/traced_functions.py +23 -4
  206. mirascope/ops/_internal/versioned_functions.py +54 -43
  207. {mirascope-2.0.0a4.dist-info → mirascope-2.0.0a6.dist-info}/METADATA +7 -7
  208. mirascope-2.0.0a6.dist-info/RECORD +316 -0
  209. mirascope/llm/formatting/_utils.py +0 -78
  210. mirascope/llm/mcp/client.py +0 -118
  211. mirascope/llm/providers/_missing_import_stubs.py +0 -49
  212. mirascope/llm/providers/load_provider.py +0 -54
  213. mirascope-2.0.0a4.dist-info/RECORD +0 -247
  214. {mirascope-2.0.0a4.dist-info → mirascope-2.0.0a6.dist-info}/WHEEL +0 -0
  215. {mirascope-2.0.0a4.dist-info → mirascope-2.0.0a6.dist-info}/licenses/LICENSE +0 -0
@@ -3,9 +3,10 @@
3
3
  from collections.abc import Sequence
4
4
  from typing_extensions import Unpack
5
5
 
6
- from openai import AsyncOpenAI, OpenAI
6
+ from openai import AsyncOpenAI, BadRequestError as OpenAIBadRequestError, OpenAI
7
7
 
8
8
  from ....context import Context, DepsT
9
+ from ....exceptions import BadRequestError, NotFoundError
9
10
  from ....formatting import Format, FormattableT
10
11
  from ....messages import Message
11
12
  from ....responses import (
@@ -29,6 +30,7 @@ from ....tools import (
29
30
  Toolkit,
30
31
  )
31
32
  from ...base import BaseProvider, Params
33
+ from .. import _utils as _shared_utils
32
34
  from ..model_id import OpenAIModelId, model_name
33
35
  from . import _utils
34
36
 
@@ -38,6 +40,12 @@ class OpenAIResponsesProvider(BaseProvider[OpenAI]):
38
40
 
39
41
  id = "openai:responses"
40
42
  default_scope = "openai/"
43
+ error_map = {
44
+ **_shared_utils.OPENAI_ERROR_MAP,
45
+ OpenAIBadRequestError: lambda e: NotFoundError
46
+ if hasattr(e, "code") and e.code == "model_not_found" # pyright: ignore[reportAttributeAccessIssue,reportUnknownMemberType]
47
+ else BadRequestError,
48
+ }
41
49
 
42
50
  def __init__(
43
51
  self,
@@ -49,6 +57,10 @@ class OpenAIResponsesProvider(BaseProvider[OpenAI]):
49
57
  self.client = OpenAI(api_key=api_key, base_url=base_url)
50
58
  self.async_client = AsyncOpenAI(api_key=api_key, base_url=base_url)
51
59
 
60
+ def get_error_status(self, e: Exception) -> int | None:
61
+ """Extract HTTP status code from OpenAI exception."""
62
+ return getattr(e, "status_code", None)
63
+
52
64
  def _call(
53
65
  self,
54
66
  *,
@@ -77,7 +89,6 @@ class OpenAIResponsesProvider(BaseProvider[OpenAI]):
77
89
  format=format,
78
90
  params=params,
79
91
  )
80
-
81
92
  openai_response = self.client.responses.create(**kwargs)
82
93
 
83
94
  assistant_message, finish_reason, usage = _utils.decode_response(
@@ -127,7 +138,6 @@ class OpenAIResponsesProvider(BaseProvider[OpenAI]):
127
138
  format=format,
128
139
  params=params,
129
140
  )
130
-
131
141
  openai_response = await self.async_client.responses.create(**kwargs)
132
142
 
133
143
  assistant_message, finish_reason, usage = _utils.decode_response(
@@ -177,7 +187,6 @@ class OpenAIResponsesProvider(BaseProvider[OpenAI]):
177
187
  format=format,
178
188
  params=params,
179
189
  )
180
-
181
190
  openai_stream = self.client.responses.create(
182
191
  **kwargs,
183
192
  stream=True,
@@ -227,7 +236,6 @@ class OpenAIResponsesProvider(BaseProvider[OpenAI]):
227
236
  format=format,
228
237
  params=params,
229
238
  )
230
-
231
239
  openai_stream = await self.async_client.responses.create(
232
240
  **kwargs,
233
241
  stream=True,
@@ -281,7 +289,6 @@ class OpenAIResponsesProvider(BaseProvider[OpenAI]):
281
289
  format=format,
282
290
  params=params,
283
291
  )
284
-
285
292
  openai_response = self.client.responses.create(**kwargs)
286
293
 
287
294
  assistant_message, finish_reason, usage = _utils.decode_response(
@@ -335,7 +342,6 @@ class OpenAIResponsesProvider(BaseProvider[OpenAI]):
335
342
  format=format,
336
343
  params=params,
337
344
  )
338
-
339
345
  openai_response = await self.async_client.responses.create(**kwargs)
340
346
 
341
347
  assistant_message, finish_reason, usage = _utils.decode_response(
@@ -6,6 +6,7 @@ KnownProviderId: TypeAlias = Literal[
6
6
  "anthropic", # Anthropic provider via AnthropicProvider
7
7
  "anthropic-beta", # Anthropic beta provider via AnthropicBetaProvider
8
8
  "google", # Google provider via GoogleProvider
9
+ "mirascope", # Mirascope Router provider via MirascopeProvider
9
10
  "mlx", # Local inference powered by `mlx-lm`, via MLXProvider
10
11
  "ollama", # Ollama provider via OllamaProvider
11
12
  "openai", # OpenAI provider via OpenAIProvider (prefers Responses routing when available)
@@ -1,16 +1,32 @@
1
1
  """Provider registry for managing provider instances and scopes."""
2
2
 
3
+ from functools import lru_cache
3
4
  from typing import overload
4
5
 
5
6
  from ..exceptions import NoRegisteredProviderError
7
+ from .anthropic import AnthropicProvider
6
8
  from .base import Provider
7
- from .load_provider import load_provider
9
+ from .google import GoogleProvider
10
+ from .mirascope import MirascopeProvider
11
+ from .mlx import MLXProvider
12
+ from .ollama import OllamaProvider
13
+ from .openai import OpenAIProvider
14
+ from .openai.completions.provider import OpenAICompletionsProvider
15
+ from .openai.responses.provider import OpenAIResponsesProvider
8
16
  from .provider_id import ProviderId
17
+ from .together import TogetherProvider
9
18
 
10
19
  # Global registry mapping scopes to providers
11
20
  # Scopes are matched by prefix (longest match wins)
12
21
  PROVIDER_REGISTRY: dict[str, Provider] = {}
13
22
 
23
+
24
+ def reset_provider_registry() -> None:
25
+ """Resets the provider registry, clearing all registered providers."""
26
+ PROVIDER_REGISTRY.clear()
27
+ provider_singleton.cache_clear()
28
+
29
+
14
30
  # Default auto-registration mapping for built-in providers
15
31
  # These providers will be automatically registered on first use
16
32
  DEFAULT_AUTO_REGISTER_SCOPES: dict[str, ProviderId] = {
@@ -23,6 +39,46 @@ DEFAULT_AUTO_REGISTER_SCOPES: dict[str, ProviderId] = {
23
39
  }
24
40
 
25
41
 
42
+ @lru_cache(maxsize=256)
43
+ def provider_singleton(
44
+ provider_id: ProviderId, *, api_key: str | None = None, base_url: str | None = None
45
+ ) -> Provider:
46
+ """Create a cached provider instance for the specified provider id.
47
+
48
+ Args:
49
+ provider_id: The provider name ("openai", "anthropic", or "google").
50
+ api_key: API key for authentication. If None, uses provider-specific env var.
51
+ base_url: Base URL for the API. If None, uses provider-specific env var.
52
+
53
+ Returns:
54
+ A cached provider instance for the specified provider with the given parameters.
55
+
56
+ Raises:
57
+ ValueError: If the provider_id is not supported.
58
+ """
59
+ match provider_id:
60
+ case "anthropic":
61
+ return AnthropicProvider(api_key=api_key, base_url=base_url)
62
+ case "google":
63
+ return GoogleProvider(api_key=api_key, base_url=base_url)
64
+ case "mirascope":
65
+ return MirascopeProvider(api_key=api_key, base_url=base_url)
66
+ case "mlx": # pragma: no cover (MLX is only available on macOS)
67
+ return MLXProvider()
68
+ case "ollama":
69
+ return OllamaProvider(api_key=api_key, base_url=base_url)
70
+ case "openai":
71
+ return OpenAIProvider(api_key=api_key, base_url=base_url)
72
+ case "openai:completions":
73
+ return OpenAICompletionsProvider(api_key=api_key, base_url=base_url)
74
+ case "openai:responses":
75
+ return OpenAIResponsesProvider(api_key=api_key, base_url=base_url)
76
+ case "together":
77
+ return TogetherProvider(api_key=api_key, base_url=base_url)
78
+ case _: # pragma: no cover
79
+ raise ValueError(f"Unknown provider: '{provider_id}'")
80
+
81
+
26
82
  @overload
27
83
  def register_provider(
28
84
  provider: Provider,
@@ -100,7 +156,7 @@ def register_provider(
100
156
  """
101
157
 
102
158
  if isinstance(provider, str):
103
- provider = load_provider(provider, api_key=api_key, base_url=base_url)
159
+ provider = provider_singleton(provider, api_key=api_key, base_url=base_url)
104
160
 
105
161
  if scope is None:
106
162
  scope = provider.default_scope
@@ -160,7 +216,7 @@ def get_provider_for_model(model_id: str) -> Provider:
160
216
  if matching_defaults:
161
217
  best_scope = max(matching_defaults, key=len)
162
218
  provider_id = DEFAULT_AUTO_REGISTER_SCOPES[best_scope]
163
- provider = load_provider(provider_id)
219
+ provider = provider_singleton(provider_id)
164
220
  # Auto-register for future calls
165
221
  PROVIDER_REGISTRY[best_scope] = provider
166
222
  return provider
@@ -1,18 +1,6 @@
1
1
  """Together AI provider implementation."""
2
2
 
3
- from typing import TYPE_CHECKING
4
-
5
- if TYPE_CHECKING:
6
- from .provider import TogetherProvider
7
- else:
8
- try:
9
- from .provider import TogetherProvider
10
- except ImportError: # pragma: no cover
11
- from .._missing_import_stubs import (
12
- create_provider_stub,
13
- )
14
-
15
- TogetherProvider = create_provider_stub("openai", "TogetherProvider")
3
+ from .provider import TogetherProvider
16
4
 
17
5
  __all__ = [
18
6
  "TogetherProvider",
@@ -227,7 +227,8 @@ class BaseStreamResponse(
227
227
  self.messages = list(input_messages) + [self._assistant_message]
228
228
 
229
229
  self._chunk_iterator = chunk_iterator
230
- self._current_content: Text | Thought | ToolCall | None = None
230
+ self._current_content: Text | Thought | None = None
231
+ self._current_tool_calls: dict[str, ToolCall] = {}
231
232
 
232
233
  self._processing_format_tool: bool = False
233
234
 
@@ -269,7 +270,7 @@ class BaseStreamResponse(
269
270
  self, chunk: TextStartChunk | TextChunk | TextEndChunk
270
271
  ) -> None:
271
272
  if chunk.type == "text_start_chunk":
272
- if self._current_content:
273
+ if self._current_content or self._current_tool_calls:
273
274
  raise RuntimeError(
274
275
  "Received text_start_chunk while processing another chunk"
275
276
  )
@@ -292,7 +293,7 @@ class BaseStreamResponse(
292
293
  self, chunk: ThoughtStartChunk | ThoughtChunk | ThoughtEndChunk
293
294
  ) -> None:
294
295
  if chunk.type == "thought_start_chunk":
295
- if self._current_content:
296
+ if self._current_content or self._current_tool_calls:
296
297
  raise RuntimeError(
297
298
  "Received thought_start_chunk while processing another chunk"
298
299
  )
@@ -323,35 +324,38 @@ class BaseStreamResponse(
323
324
  raise RuntimeError(
324
325
  "Received tool_call_start_chunk while processing another chunk"
325
326
  )
326
- self._current_content = ToolCall(
327
+ if chunk.id in self._current_tool_calls:
328
+ raise RuntimeError("Got tool_call_start_chunk with conflicting id")
329
+ # Create a new tool call and track it by ID
330
+ # Multiple tool calls can be in progress simultaneously (interleaved)
331
+ tool_call = ToolCall(
327
332
  id=chunk.id,
328
333
  name=chunk.name,
329
334
  args="",
330
335
  )
336
+ self._current_tool_calls[chunk.id] = tool_call
331
337
 
332
338
  elif chunk.type == "tool_call_chunk":
333
- if (
334
- self._current_content is None
335
- or self._current_content.type != "tool_call"
336
- ):
339
+ # Look up the tool call by ID
340
+ tool_call = self._current_tool_calls.get(chunk.id)
341
+ if tool_call is None:
337
342
  raise RuntimeError(
338
- "Received tool_call_chunk while not processing tool call."
343
+ f"Received tool_call_chunk for unknown tool call ID: {chunk.id}"
339
344
  )
340
- self._current_content.args += chunk.delta
345
+ tool_call.args += chunk.delta
341
346
 
342
347
  elif chunk.type == "tool_call_end_chunk":
343
- if (
344
- self._current_content is None
345
- or self._current_content.type != "tool_call"
346
- ):
348
+ # Finalize the tool call
349
+ tool_call = self._current_tool_calls.get(chunk.id)
350
+ if tool_call is None:
347
351
  raise RuntimeError(
348
- "Received tool_call_end_chunk while not processing tool call."
352
+ f"Received tool_call_end_chunk for unknown tool call ID: {chunk.id}"
349
353
  )
350
- if not self._current_content.args:
351
- self._current_content.args = "{}"
352
- self._content.append(self._current_content)
353
- self._tool_calls.append(self._current_content)
354
- self._current_content = None
354
+ if not tool_call.args:
355
+ tool_call.args = "{}"
356
+ self._content.append(tool_call)
357
+ self._tool_calls.append(tool_call)
358
+ del self._current_tool_calls[chunk.id]
355
359
 
356
360
  def _pretty_chunk(self, chunk: AssistantContentChunk, spacer: str) -> str:
357
361
  match chunk.type:
@@ -62,15 +62,19 @@ class ToolDecorator:
62
62
  is_async = _tool_utils.is_async_tool_fn(fn)
63
63
 
64
64
  if is_context and is_async:
65
- return AsyncContextTool[DepsT, JsonableCovariantT, P](
65
+ return AsyncContextTool[DepsT, JsonableCovariantT, P].from_function(
66
66
  fn, strict=self.strict
67
67
  )
68
68
  elif is_context:
69
- return ContextTool[DepsT, JsonableCovariantT, P](fn, strict=self.strict)
69
+ return ContextTool[DepsT, JsonableCovariantT, P].from_function(
70
+ fn, strict=self.strict
71
+ )
70
72
  elif is_async:
71
- return AsyncTool[P, JsonableCovariantT](fn, strict=self.strict)
73
+ return AsyncTool[P, JsonableCovariantT].from_function(
74
+ fn, strict=self.strict
75
+ )
72
76
  else:
73
- return Tool[P, JsonableCovariantT](fn, strict=self.strict)
77
+ return Tool[P, JsonableCovariantT].from_function(fn, strict=self.strict)
74
78
 
75
79
 
76
80
  @overload
@@ -175,10 +175,35 @@ class ToolSchema(Generic[ToolFnT]):
175
175
  def __init__(
176
176
  self,
177
177
  fn: ToolFnT,
178
+ name: str,
179
+ description: str,
180
+ parameters: ToolParameterSchema,
178
181
  *,
179
182
  strict: bool = False,
180
- is_context_tool: bool = False,
181
183
  ) -> None:
184
+ """Create a `ToolSchema` with the provided values.
185
+
186
+ Args:
187
+ fn: The function that implements the tool's functionality
188
+ name: The name of the tool
189
+ description: Description of what the tool does
190
+ parameters: JSON Schema describing the parameters accepted by the tool
191
+ strict: Whether the tool should use strict mode when supported
192
+ """
193
+ self.fn = fn
194
+ self.name = name
195
+ self.description = description
196
+ self.parameters = parameters
197
+ self.strict = strict
198
+
199
+ @classmethod
200
+ def from_function(
201
+ cls,
202
+ fn: AnyToolFn,
203
+ *,
204
+ strict: bool = False,
205
+ is_context_tool: bool = False,
206
+ ) -> ToolSchema[AnyToolFn]:
182
207
  """Create a `ToolSchema` by inspecting a function and its docstring.
183
208
 
184
209
  Uses Pydantic's create_model to dynamically build a model from the function
@@ -264,11 +289,13 @@ class ToolSchema(Generic[ToolFnT]):
264
289
  if "$defs" in schema:
265
290
  parameters.defs = schema["$defs"]
266
291
 
267
- self.fn = fn
268
- self.name = name
269
- self.description = description
270
- self.parameters = parameters
271
- self.strict = strict
292
+ return cls(
293
+ fn=cast(ToolFnT, fn),
294
+ name=name,
295
+ description=description,
296
+ parameters=parameters,
297
+ strict=strict,
298
+ )
272
299
 
273
300
  def can_execute(self, tool_call: ToolCall) -> bool:
274
301
  """Check if a `ToolCall` can be executed by tools with this `ToolSchema`.
@@ -40,10 +40,27 @@ class Tool(
40
40
  This class is not instantiated directly but created by the `@tool()` decorator.
41
41
  """
42
42
 
43
- def __init__(
44
- self, fn: ToolFn[AnyP, JsonableCovariantT], *, strict: bool = False
45
- ) -> None:
46
- super().__init__(fn, strict=strict, is_context_tool=False)
43
+ @classmethod
44
+ def from_function( # pyright: ignore[reportIncompatibleMethodOverride]
45
+ cls, fn: ToolFn[AnyP, JsonableCovariantT], *, strict: bool = False
46
+ ) -> Tool[AnyP, JsonableCovariantT]:
47
+ """Create a `Tool` by inspecting a function and its docstring.
48
+
49
+ Args:
50
+ fn: The function to extract schema from
51
+ strict: Whether the tool should use strict mode when supported
52
+
53
+ Returns:
54
+ a `Tool` representing the function
55
+ """
56
+ schema = ToolSchema.from_function(fn, strict=strict, is_context_tool=False)
57
+ return cls(
58
+ fn=cast(ToolFn[AnyP, JsonableCovariantT], schema.fn),
59
+ name=schema.name,
60
+ description=schema.description,
61
+ parameters=schema.parameters,
62
+ strict=schema.strict,
63
+ )
47
64
 
48
65
  def __call__(self, *args: AnyP.args, **kwargs: AnyP.kwargs) -> JsonableCovariantT:
49
66
  """Call the underlying function directly."""
@@ -69,10 +86,27 @@ class AsyncTool(
69
86
  This class is not instantiated directly but created by the `@tool()` decorator.
70
87
  """
71
88
 
72
- def __init__(
73
- self, fn: AsyncToolFn[AnyP, JsonableCovariantT], *, strict: bool = False
74
- ) -> None:
75
- super().__init__(fn, strict=strict, is_context_tool=False)
89
+ @classmethod
90
+ def from_function( # pyright: ignore[reportIncompatibleMethodOverride]
91
+ cls, fn: AsyncToolFn[AnyP, JsonableCovariantT], *, strict: bool = False
92
+ ) -> AsyncTool[AnyP, JsonableCovariantT]:
93
+ """Create an `AsyncTool` by inspecting a function and its docstring.
94
+
95
+ Args:
96
+ fn: The function to extract schema from
97
+ strict: Whether the tool should use strict mode when supported
98
+
99
+ Returns:
100
+ an `AsyncTool` representing the function
101
+ """
102
+ schema = ToolSchema.from_function(fn, strict=strict, is_context_tool=False)
103
+ return cls(
104
+ fn=cast(AsyncToolFn[AnyP, JsonableCovariantT], schema.fn),
105
+ name=schema.name,
106
+ description=schema.description,
107
+ parameters=schema.parameters,
108
+ strict=schema.strict,
109
+ )
76
110
 
77
111
  def __call__(
78
112
  self, *args: AnyP.args, **kwargs: AnyP.kwargs
@@ -100,13 +134,30 @@ class ContextTool(
100
134
  This class is not instantiated directly but created by the `@tool()` decorator.
101
135
  """
102
136
 
103
- def __init__(
104
- self,
137
+ @classmethod
138
+ def from_function( # pyright: ignore[reportIncompatibleMethodOverride]
139
+ cls,
105
140
  fn: ContextToolFn[DepsT, AnyP, JsonableCovariantT],
106
141
  *,
107
142
  strict: bool = False,
108
- ) -> None:
109
- super().__init__(fn, strict=strict, is_context_tool=True)
143
+ ) -> ContextTool[DepsT, JsonableCovariantT, AnyP]:
144
+ """Create a `ContextTool` by inspecting a function and its docstring.
145
+
146
+ Args:
147
+ fn: The function to extract schema from
148
+ strict: Whether the tool should use strict mode when supported
149
+
150
+ Returns:
151
+ a `ContextTool` representing the function
152
+ """
153
+ schema = ToolSchema.from_function(fn, strict=strict, is_context_tool=True)
154
+ return cls(
155
+ fn=cast(ContextToolFn[DepsT, AnyP, JsonableCovariantT], schema.fn),
156
+ name=schema.name,
157
+ description=schema.description,
158
+ parameters=schema.parameters,
159
+ strict=schema.strict,
160
+ )
110
161
 
111
162
  def __call__(
112
163
  self,
@@ -141,13 +192,30 @@ class AsyncContextTool(
141
192
  This class is not instantiated directly but created by the `@tool()` decorator.
142
193
  """
143
194
 
144
- def __init__(
145
- self,
195
+ @classmethod
196
+ def from_function( # pyright: ignore[reportIncompatibleMethodOverride]
197
+ cls,
146
198
  fn: AsyncContextToolFn[DepsT, AnyP, JsonableCovariantT],
147
199
  *,
148
200
  strict: bool = False,
149
- ) -> None:
150
- super().__init__(fn, strict=strict, is_context_tool=True)
201
+ ) -> AsyncContextTool[DepsT, JsonableCovariantT, AnyP]:
202
+ """Create an `AsyncContextTool` by inspecting a function and its docstring.
203
+
204
+ Args:
205
+ fn: The function to extract schema from
206
+ strict: Whether the tool should use strict mode when supported
207
+
208
+ Returns:
209
+ an `AsyncContextTool` representing the function
210
+ """
211
+ schema = ToolSchema.from_function(fn, strict=strict, is_context_tool=True)
212
+ return cls(
213
+ fn=cast(AsyncContextToolFn[DepsT, AnyP, JsonableCovariantT], schema.fn),
214
+ name=schema.name,
215
+ description=schema.description,
216
+ parameters=schema.parameters,
217
+ strict=schema.strict,
218
+ )
151
219
 
152
220
  def __call__(
153
221
  self,