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
@@ -20,7 +20,12 @@ from ..content import (
20
20
  ToolCallEndChunk,
21
21
  ToolCallStartChunk,
22
22
  )
23
- from ..formatting import Format, FormattableT, Partial
23
+ from ..formatting import (
24
+ Format,
25
+ FormattableT,
26
+ Partial,
27
+ is_output_parser,
28
+ )
24
29
  from ..messages import AssistantMessage, Message
25
30
  from ..tools import FORMAT_TOOL_NAME, ToolkitT
26
31
  from ..types import Jsonable
@@ -39,7 +44,11 @@ from .streams import (
39
44
  from .usage import Usage, UsageDeltaChunk
40
45
 
41
46
  if TYPE_CHECKING:
42
- from ..providers import ModelId, Params, ProviderId
47
+ from ..models import Params
48
+ from ..providers import ModelId, ProviderId
49
+
50
+
51
+ StreamResponseT = TypeVar("StreamResponseT", bound="BaseStreamResponse[Any, Any, Any]")
43
52
 
44
53
 
45
54
  @dataclass(kw_only=True)
@@ -505,6 +514,26 @@ class BaseSyncStreamResponse(BaseStreamResponse[ChunkIterator, ToolkitT, Formatt
505
514
  for _ in self.chunk_stream():
506
515
  pass
507
516
 
517
+ def text_stream(self, sep: str = "\n") -> Iterator[str]:
518
+ """Stream only the text content from the response.
519
+
520
+ Args:
521
+ sep: Separator to yield between text parts. Defaults to newline.
522
+
523
+ Returns:
524
+ Iterator[str]: Iterator yielding text delta strings
525
+
526
+ Yields text deltas as they arrive, ignoring thoughts, tool calls, and other
527
+ content types. Ideal for displaying text to users in real-time.
528
+
529
+ If you concatenate the strings from `.text_stream()`, it will be equivalent to
530
+ calling `.text(sep=sep)` on the fully consumed response.
531
+ """
532
+ for stream in self.streams():
533
+ if stream.content_type == "text":
534
+ yield from stream
535
+ yield sep
536
+
508
537
  def pretty_stream(self) -> Iterator[str]:
509
538
  """Stream a readable representation of the stream_response as text.
510
539
 
@@ -527,26 +556,46 @@ class BaseSyncStreamResponse(BaseStreamResponse[ChunkIterator, ToolkitT, Formatt
527
556
  printed = True
528
557
  yield pretty
529
558
 
530
- if not printed:
531
- yield "**[No Content]**"
532
-
533
559
  def structured_stream(
534
560
  self,
535
561
  ) -> Iterator[Partial[FormattableT]]:
536
- """Returns an iterator that yields partial structured objects as content streams.
562
+ """Drive the stream forward, yielding partial formatted outputs as they arrive.
537
563
 
538
- Returns:
539
- Iterator[Partial[FormatT]]: Synchronous iterator yielding partial structured objects
564
+ This method consumes the underlying stream chunk by chunk, yielding parsed
565
+ partial outputs each time new content arrives. Each yielded value is a
566
+ Partial[FormattableT] with optional fields that may be None until fully received.
540
567
 
541
- This method yields Partial[FormatT] objects as the response content is streamed,
542
- allowing you to access partial structured data before the response is fully complete.
543
- Each yielded object represents the current state of the parsed structure with all
544
- fields optional.
568
+ Example:
569
+ >>> response = recommend_book.stream("fantasy")
570
+ >>> for partial in response.structured_stream():
571
+ >>> print(f"Title so far: {partial.title}")
572
+ >>> book = response.parse() # Get final complete result
545
573
 
546
574
  Fully iterating through this iterator will fully consume the underlying stream,
547
575
  updating the Response with all collected content.
576
+
577
+ Yields:
578
+ Partial[FormattableT]: Partial objects with fields populated as they arrive.
579
+ Fields not yet received will be None.
580
+
581
+ Raises:
582
+ ValueError: If format parameter not set.
583
+ NotImplementedError: If format uses OutputParser (not supported).
548
584
  """
549
- raise NotImplementedError()
585
+ if self.format is None:
586
+ raise ValueError("structured_stream() requires format parameter")
587
+
588
+ if is_output_parser(self.format.formattable):
589
+ raise NotImplementedError(
590
+ "structured_stream() not supported for OutputParser. "
591
+ "Use BaseModel or primitive types."
592
+ )
593
+
594
+ for chunk in self.chunk_stream():
595
+ if chunk.type == "text_chunk":
596
+ partial = self.parse(partial=True)
597
+ if partial:
598
+ yield partial
550
599
 
551
600
 
552
601
  class BaseAsyncStreamResponse(
@@ -686,6 +735,27 @@ class BaseAsyncStreamResponse(
686
735
  async for _ in self.chunk_stream():
687
736
  pass
688
737
 
738
+ async def text_stream(self, sep: str = "\n") -> AsyncIterator[str]:
739
+ """Stream only the text content from the response.
740
+
741
+ Args:
742
+ sep: Separator to yield between text parts. Defaults to newline.
743
+
744
+ Returns:
745
+ AsyncIterator[str]: Async iterator yielding text delta strings
746
+
747
+ Yields text deltas as they arrive, ignoring thoughts, tool calls, and other
748
+ content types. Ideal for displaying text to users in real-time.
749
+
750
+ If you concatenate the strings from `.text_stream()`, it will be equivalent to
751
+ calling `.text(sep=sep)` on the fully consumed response.
752
+ """
753
+ async for stream in self.streams():
754
+ if stream.content_type == "text":
755
+ async for delta in stream:
756
+ yield delta
757
+ yield sep
758
+
689
759
  async def pretty_stream(self) -> AsyncIterator[str]:
690
760
  """Stream a readable representation of the stream_response as text.
691
761
 
@@ -708,23 +778,43 @@ class BaseAsyncStreamResponse(
708
778
  printed = True
709
779
  yield pretty
710
780
 
711
- if not printed:
712
- yield "**[No Content]**"
713
-
714
- def structured_stream(
781
+ async def structured_stream(
715
782
  self,
716
783
  ) -> AsyncIterator[Partial[FormattableT]]:
717
- """Returns an async iterator that yields partial structured objects as content streams.
784
+ """Drive the stream forward, yielding partial formatted outputs as they arrive.
718
785
 
719
- Returns:
720
- AsyncIterator[Partial[FormatT]]: Async iterator yielding partial structured objects
786
+ This method consumes the underlying stream chunk by chunk, yielding parsed
787
+ partial outputs each time new content arrives. Each yielded value is a
788
+ Partial[FormattableT] with optional fields that may be None until fully received.
721
789
 
722
- This method yields Partial[FormatT] objects as the response content is streamed,
723
- allowing you to access partial structured data before the response is fully complete.
724
- Each yielded object represents the current state of the parsed structure with all
725
- fields optional.
790
+ Example:
791
+ >>> response = await recommend_book.stream("fantasy")
792
+ >>> async for partial in response.structured_stream():
793
+ >>> print(f"Title so far: {partial.title}")
794
+ >>> book = response.parse() # Get final complete result
726
795
 
727
796
  Fully iterating through this iterator will fully consume the underlying stream,
728
797
  updating the Response with all collected content.
798
+
799
+ Yields:
800
+ Partial[FormattableT]: Partial objects with fields populated as they arrive.
801
+ Fields not yet received will be None.
802
+
803
+ Raises:
804
+ ValueError: If format parameter not set.
805
+ NotImplementedError: If format uses OutputParser (not supported).
729
806
  """
730
- raise NotImplementedError()
807
+ if self.format is None:
808
+ raise ValueError("structured_stream() requires format parameter")
809
+
810
+ if is_output_parser(self.format.formattable):
811
+ raise NotImplementedError(
812
+ "structured_stream() not supported for OutputParser. "
813
+ "Use BaseModel or primitive types."
814
+ )
815
+
816
+ async for chunk in self.chunk_stream():
817
+ if chunk.type == "text_chunk":
818
+ partial = self.parse(partial=True)
819
+ if partial:
820
+ yield partial
@@ -24,7 +24,8 @@ from .finish_reason import FinishReason
24
24
  from .usage import Usage
25
25
 
26
26
  if TYPE_CHECKING:
27
- from ..providers import ModelId, Params, ProviderId
27
+ from ..models import Params
28
+ from ..providers import ModelId, ProviderId
28
29
 
29
30
 
30
31
  class Response(BaseResponse[Toolkit, FormattableT]):
@@ -3,10 +3,18 @@
3
3
  from abc import ABC
4
4
  from collections.abc import Sequence
5
5
  from types import NoneType
6
- from typing import TYPE_CHECKING, Any, Generic, Literal, overload
6
+ from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, overload
7
7
 
8
8
  from ..content import AssistantContentPart, Text, Thought, ToolCall
9
- from ..formatting import Format, FormattableT, Partial
9
+ from ..exceptions import ParseError
10
+ from ..formatting import (
11
+ Format,
12
+ FormattableT,
13
+ Partial,
14
+ create_wrapper_model,
15
+ is_output_parser,
16
+ is_primitive_type,
17
+ )
10
18
  from ..messages import Message
11
19
  from ..tools import ToolkitT
12
20
  from . import _utils
@@ -14,8 +22,10 @@ from .finish_reason import FinishReason
14
22
  from .usage import Usage
15
23
 
16
24
  if TYPE_CHECKING:
17
- from ..models import Model
18
- from ..providers import ModelId, Params, ProviderId
25
+ from ..models import Model, Params
26
+ from ..providers import ModelId, ProviderId
27
+
28
+ AnyResponse: TypeAlias = "RootResponse[Any, Any]"
19
29
 
20
30
 
21
31
  class RootResponse(Generic[ToolkitT, FormattableT], ABC):
@@ -107,31 +117,96 @@ class RootResponse(Generic[ToolkitT, FormattableT], ABC):
107
117
  ) -> FormattableT | Partial[FormattableT] | None:
108
118
  """Format the response according to the response format parser.
109
119
 
120
+ Args:
121
+ partial: If True, parse incomplete JSON as Partial model. Only works with
122
+ streaming responses that have accumulated JSON. Returns None if JSON
123
+ is not yet available or cannot be parsed.
124
+
125
+ Supports:
126
+ - Pydantic BaseModel types (JSON schema validation)
127
+ - Primitive types (automatically unwrapped from wrapper model)
128
+ - Custom OutputParsers (custom parsing logic)
129
+ - Partial parsing during streaming (when partial=True)
130
+
110
131
  Returns:
111
- The formatted response object of type FormatT.
132
+ The formatted response object of type FormatT. For BaseModel types, returns
133
+ the model instance. For primitive types, returns the unwrapped value (e.g.,
134
+ a string, list, dict, etc.). For OutputParsers, returns whatever the parser
135
+ returns. When partial=True, returns None if JSON is incomplete or unparsable.
112
136
 
113
137
  Raises:
114
- json.JSONDecodeError: If the response's textual content can't be parsed as
115
- JSON.
116
- pydantic.ValidationError: If the response's content fails validation for the
117
- format type.
138
+ NotImplementedError: If partial=True with OutputParser.
139
+ ParseError: If parsing fails. The `original_exception` attribute contains the
140
+ underlying error (ValueError for JSON extraction, json.JSONDecodeError
141
+ for invalid JSON, pydantic.ValidationError for schema validation, or
142
+ any exception from a custom OutputParser).
118
143
  """
119
144
  if self.format is None:
120
145
  return None
121
146
 
122
147
  formattable = self.format.formattable
148
+
149
+ if is_output_parser(formattable):
150
+ if partial:
151
+ raise NotImplementedError(
152
+ "parse(partial=True) not supported with OutputParser. "
153
+ "Use BaseModel or primitive types."
154
+ )
155
+ try:
156
+ return formattable(self)
157
+ except Exception as e:
158
+ raise ParseError(
159
+ f"OutputParser failed: {e}",
160
+ original_exception=e,
161
+ ) from e
162
+
123
163
  if formattable is None or formattable is NoneType: # pyright: ignore[reportUnnecessaryComparison]
124
164
  # note: pyright claims the None comparison is unnecessary, but removing it
125
165
  # introduces type errors.
126
166
  return None # pragma: no cover
127
167
 
128
- if partial:
129
- raise NotImplementedError
168
+ text = self.text("")
130
169
 
131
- text = "".join(text.text for text in self.texts)
132
- json_text = _utils.extract_serialized_json(text)
170
+ if partial:
171
+ return _utils.parse_partial_json(text, formattable)
172
+ else:
173
+ try:
174
+ json_text = _utils.extract_serialized_json(text)
175
+ if is_primitive_type(formattable):
176
+ wrapper_model = create_wrapper_model(formattable)
177
+ wrapper_instance = wrapper_model.model_validate_json(json_text)
178
+ return wrapper_instance.output
179
+
180
+ return formattable.model_validate_json(json_text)
181
+ except Exception as e:
182
+ raise ParseError(
183
+ f"Failed to parse response: {e}",
184
+ original_exception=e,
185
+ ) from e
186
+
187
+ def text(self, sep: str = "\n") -> str:
188
+ """Return all text content from this response as a single string.
189
+
190
+ Joins the text from all `Text` parts in the response content using the
191
+ specified separator.
192
+
193
+ Args:
194
+ sep: The separator to use when joining multiple text parts.
195
+ Defaults to newline ("\\n").
133
196
 
134
- return formattable.model_validate_json(json_text)
197
+ Returns:
198
+ A string containing all text content joined by the separator.
199
+ Returns an empty string if the response contains no text parts.
200
+
201
+ Example:
202
+ >>> response.text() # Join with newlines (default)
203
+ 'Hello\\nWorld'
204
+ >>> response.text(sep=" ") # Join with spaces
205
+ 'Hello World'
206
+ >>> response.text(sep="") # Concatenate directly
207
+ 'HelloWorld'
208
+ """
209
+ return sep.join(text.text for text in self.texts)
135
210
 
136
211
  def pretty(self) -> str:
137
212
  """Return a string representation of all response content.
@@ -147,9 +222,6 @@ class RootResponse(Generic[ToolkitT, FormattableT], ABC):
147
222
 
148
223
  I am going to use the calculator and answer your question for you!
149
224
  """
150
- if not self.content:
151
- return "**[No Content]**"
152
-
153
225
  pretty_parts: list[str] = []
154
226
  for part in self.content:
155
227
  if isinstance(part, Text):
@@ -27,7 +27,8 @@ from .base_stream_response import (
27
27
  )
28
28
 
29
29
  if TYPE_CHECKING:
30
- from ..providers import ModelId, Params, ProviderId
30
+ from ..models import Params
31
+ from ..providers import ModelId, ProviderId
31
32
 
32
33
 
33
34
  class StreamResponse(BaseSyncStreamResponse[Toolkit, FormattableT]):
@@ -85,9 +86,8 @@ class StreamResponse(BaseSyncStreamResponse[Toolkit, FormattableT]):
85
86
 
86
87
  stream_response = answer_question.stream("What is the capital of France?")
87
88
 
88
- for chunk in stream_response.pretty_stream():
89
+ for chunk in stream_response.text_stream():
89
90
  print(chunk, end="", flush=True)
90
- print()
91
91
  ```
92
92
  """
93
93
 
@@ -212,9 +212,8 @@ class AsyncStreamResponse(BaseAsyncStreamResponse[AsyncToolkit, FormattableT]):
212
212
 
213
213
  stream_response = await answer_question.stream("What is the capital of France?")
214
214
 
215
- async for chunk in stream_response.pretty_stream():
215
+ async for chunk in stream_response.text_stream():
216
216
  print(chunk, end="", flush=True)
217
- print()
218
217
  ```
219
218
  """
220
219
 
@@ -348,9 +347,8 @@ class ContextStreamResponse(
348
347
  ctx = llm.Context()
349
348
  stream_response = answer_question.stream(ctx, "What is the capital of France?")
350
349
 
351
- for chunk in stream_response.pretty_stream():
350
+ for chunk in stream_response.text_stream():
352
351
  print(chunk, end="", flush=True)
353
- print()
354
352
  ```
355
353
  """
356
354
 
@@ -492,9 +490,8 @@ class AsyncContextStreamResponse(
492
490
  ctx = llm.Context()
493
491
  stream_response = await answer_question.stream(ctx, "What is the capital of France?")
494
492
 
495
- async for chunk in stream_response.pretty_stream():
493
+ async for chunk in stream_response.text_stream():
496
494
  print(chunk, end="", flush=True)
497
- print()
498
495
  ```
499
496
  """
500
497
 
@@ -14,8 +14,12 @@ from .tools import AsyncContextTool, AsyncTool, ContextTool, Tool
14
14
  class ToolDecorator:
15
15
  """Protocol for the tool decorator."""
16
16
 
17
- strict: bool
18
- """Whether to use strict tool calling, if supported by the provider."""
17
+ strict: bool | None = None
18
+ """Whether to use strict tool calling, if supported by the provider.
19
+
20
+ If set to None, then it will use the provider's default setting (usually the
21
+ strictest possible).
22
+ """
19
23
 
20
24
  @overload
21
25
  def __call__( # pyright:ignore[reportOverlappingOverload]
@@ -106,7 +110,7 @@ def tool(__fn: ToolFn[P, JsonableCovariantT]) -> Tool[P, JsonableCovariantT]:
106
110
 
107
111
 
108
112
  @overload
109
- def tool(*, strict: bool = False) -> ToolDecorator:
113
+ def tool(*, strict: bool | None = None) -> ToolDecorator:
110
114
  """Overload for setting non-default arguments."""
111
115
  ...
112
116
 
@@ -118,7 +122,7 @@ def tool(
118
122
  | AsyncToolFn[P, JsonableCovariantT]
119
123
  | None = None,
120
124
  *,
121
- strict: bool = False,
125
+ strict: bool | None = None,
122
126
  ) -> (
123
127
  ContextTool[DepsT, JsonableCovariantT, P]
124
128
  | AsyncContextTool[DepsT, JsonableCovariantT, P]
@@ -137,6 +141,7 @@ def tool(
137
141
 
138
142
  Args:
139
143
  strict: Whether the tool should use strict mode when supported by the model.
144
+ If None, uses provider's default (usually as strict as possible).
140
145
 
141
146
  Returns:
142
147
  A decorator function that converts the function into a Tool or ContextTool.
@@ -22,6 +22,7 @@ from docstring_parser import parse
22
22
  from pydantic import BaseModel, Field, create_model
23
23
  from pydantic.fields import FieldInfo
24
24
 
25
+ from ..._utils import copy_function_metadata
25
26
  from ..content import ToolCall
26
27
  from ..types import Jsonable
27
28
  from .protocols import AsyncContextToolFn, AsyncToolFn, ContextToolFn, ToolFn
@@ -157,8 +158,15 @@ class ToolSchema(Generic[ToolFnT]):
157
158
  it should **not be modified** after the ToolSchema is created.
158
159
  """
159
160
 
160
- strict: bool
161
- """Whether the tool should use strict mode when supported by the model."""
161
+ strict: bool | None
162
+ """Whether the tool should use strict mode when supported by the model.
163
+
164
+ If set to None, will use the provider's default setting (usually as strict as
165
+ possible).
166
+ """
167
+
168
+ __name__: str
169
+ """The name of the underlying function (preserved for decorator stacking)."""
162
170
 
163
171
  def __hash__(self) -> int:
164
172
  if not hasattr(self, "_hash"):
@@ -179,7 +187,7 @@ class ToolSchema(Generic[ToolFnT]):
179
187
  description: str,
180
188
  parameters: ToolParameterSchema,
181
189
  *,
182
- strict: bool = False,
190
+ strict: bool | None = None,
183
191
  ) -> None:
184
192
  """Create a `ToolSchema` with the provided values.
185
193
 
@@ -188,20 +196,22 @@ class ToolSchema(Generic[ToolFnT]):
188
196
  name: The name of the tool
189
197
  description: Description of what the tool does
190
198
  parameters: JSON Schema describing the parameters accepted by the tool
191
- strict: Whether the tool should use strict mode when supported
199
+ strict: Whether the tool should use strict mode when supported.
200
+ If None, uses provider's default (usually as strict as possible).
192
201
  """
193
202
  self.fn = fn
194
203
  self.name = name
195
204
  self.description = description
196
205
  self.parameters = parameters
197
206
  self.strict = strict
207
+ copy_function_metadata(self, fn)
198
208
 
199
209
  @classmethod
200
210
  def from_function(
201
211
  cls,
202
212
  fn: AnyToolFn,
203
213
  *,
204
- strict: bool = False,
214
+ strict: bool | None = None,
205
215
  is_context_tool: bool = False,
206
216
  ) -> ToolSchema[AnyToolFn]:
207
217
  """Create a `ToolSchema` by inspecting a function and its docstring.
@@ -212,7 +222,8 @@ class ToolSchema(Generic[ToolFnT]):
212
222
 
213
223
  Args:
214
224
  fn: The function to extract schema from
215
- strict: Whether the tool should use strict mode when supported
225
+ strict: Whether the tool should use strict mode when supported.
226
+ If None, uses provider's default (usually as strict as possible).
216
227
  is_context_tool: Whether this is a context tool (skips the context parameter)
217
228
 
218
229
  Returns:
@@ -60,7 +60,7 @@ class BaseToolkit(Generic[ToolSchemaT]):
60
60
  """
61
61
  tool = self.tools_dict.get(tool_call.name, None)
62
62
  if not tool:
63
- raise ToolNotFoundError(f"Tool not found in toolkit: {tool_call.name}")
63
+ raise ToolNotFoundError(tool_call.name)
64
64
  return tool
65
65
 
66
66
 
@@ -75,12 +75,14 @@ class Toolkit(BaseToolkit[Tool]):
75
75
 
76
76
  Returns:
77
77
  The output from executing the `Tool`.
78
-
79
- Raises:
80
- ToolNotFoundError: If the requested tool is not found.
81
78
  """
82
- tool = self.get(tool_call)
83
- return tool.execute(tool_call)
79
+ try:
80
+ tool = self.get(tool_call)
81
+ return tool.execute(tool_call)
82
+ except ToolNotFoundError as e:
83
+ return ToolOutput(
84
+ id=tool_call.id, result=str(e), error=e, name=tool_call.name
85
+ )
84
86
 
85
87
 
86
88
  class AsyncToolkit(BaseToolkit[AsyncTool]):
@@ -94,12 +96,14 @@ class AsyncToolkit(BaseToolkit[AsyncTool]):
94
96
 
95
97
  Returns:
96
98
  The output from executing the `AsyncTool`.
97
-
98
- Raises:
99
- ToolNotFoundError: If the requested tool is not found.
100
99
  """
101
- tool = self.get(tool_call)
102
- return await tool.execute(tool_call)
100
+ try:
101
+ tool = self.get(tool_call)
102
+ return await tool.execute(tool_call)
103
+ except ToolNotFoundError as e:
104
+ return ToolOutput(
105
+ id=tool_call.id, result=str(e), error=e, name=tool_call.name
106
+ )
103
107
 
104
108
 
105
109
  class ContextToolkit(BaseToolkit[Tool | ContextTool[DepsT]], Generic[DepsT]):
@@ -114,15 +118,17 @@ class ContextToolkit(BaseToolkit[Tool | ContextTool[DepsT]], Generic[DepsT]):
114
118
 
115
119
  Returns:
116
120
  The output from executing the `ContextTool`.
117
-
118
- Raises:
119
- ToolNotFoundError: If the requested tool is not found.
120
121
  """
121
- tool = self.get(tool_call)
122
- if isinstance(tool, ContextTool):
123
- return tool.execute(ctx, tool_call)
124
- else:
125
- return tool.execute(tool_call)
122
+ try:
123
+ tool = self.get(tool_call)
124
+ if isinstance(tool, ContextTool):
125
+ return tool.execute(ctx, tool_call)
126
+ else:
127
+ return tool.execute(tool_call)
128
+ except ToolNotFoundError as e:
129
+ return ToolOutput(
130
+ id=tool_call.id, result=str(e), error=e, name=tool_call.name
131
+ )
126
132
 
127
133
 
128
134
  class AsyncContextToolkit(
@@ -141,12 +147,14 @@ class AsyncContextToolkit(
141
147
 
142
148
  Returns:
143
149
  The output from executing the `AsyncContextTool`.
144
-
145
- Raises:
146
- ToolNotFoundError: If the requested tool is not found.
147
150
  """
148
- tool = self.get(tool_call)
149
- if isinstance(tool, AsyncContextTool):
150
- return await tool.execute(ctx, tool_call)
151
- else:
152
- return await tool.execute(tool_call)
151
+ try:
152
+ tool = self.get(tool_call)
153
+ if isinstance(tool, AsyncContextTool):
154
+ return await tool.execute(ctx, tool_call)
155
+ else:
156
+ return await tool.execute(tool_call)
157
+ except ToolNotFoundError as e:
158
+ return ToolOutput(
159
+ id=tool_call.id, result=str(e), error=e, name=tool_call.name
160
+ )