mirascope 2.0.0a3__py3-none-any.whl → 2.0.0a5__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 (118) hide show
  1. mirascope/api/_generated/__init__.py +78 -6
  2. mirascope/api/_generated/api_keys/__init__.py +7 -0
  3. mirascope/api/_generated/api_keys/client.py +453 -0
  4. mirascope/api/_generated/api_keys/raw_client.py +853 -0
  5. mirascope/api/_generated/api_keys/types/__init__.py +9 -0
  6. mirascope/api/_generated/api_keys/types/api_keys_create_response.py +36 -0
  7. mirascope/api/_generated/api_keys/types/api_keys_get_response.py +35 -0
  8. mirascope/api/_generated/api_keys/types/api_keys_list_response_item.py +35 -0
  9. mirascope/api/_generated/client.py +14 -0
  10. mirascope/api/_generated/environments/__init__.py +17 -0
  11. mirascope/api/_generated/environments/client.py +532 -0
  12. mirascope/api/_generated/environments/raw_client.py +1088 -0
  13. mirascope/api/_generated/environments/types/__init__.py +15 -0
  14. mirascope/api/_generated/environments/types/environments_create_response.py +26 -0
  15. mirascope/api/_generated/environments/types/environments_get_response.py +26 -0
  16. mirascope/api/_generated/environments/types/environments_list_response_item.py +26 -0
  17. mirascope/api/_generated/environments/types/environments_update_response.py +26 -0
  18. mirascope/api/_generated/errors/__init__.py +11 -1
  19. mirascope/api/_generated/errors/conflict_error.py +15 -0
  20. mirascope/api/_generated/errors/forbidden_error.py +15 -0
  21. mirascope/api/_generated/errors/internal_server_error.py +15 -0
  22. mirascope/api/_generated/errors/not_found_error.py +15 -0
  23. mirascope/api/_generated/organizations/__init__.py +25 -0
  24. mirascope/api/_generated/organizations/client.py +404 -0
  25. mirascope/api/_generated/organizations/raw_client.py +902 -0
  26. mirascope/api/_generated/organizations/types/__init__.py +23 -0
  27. mirascope/api/_generated/organizations/types/organizations_create_response.py +25 -0
  28. mirascope/api/_generated/organizations/types/organizations_create_response_role.py +7 -0
  29. mirascope/api/_generated/organizations/types/organizations_get_response.py +25 -0
  30. mirascope/api/_generated/organizations/types/organizations_get_response_role.py +7 -0
  31. mirascope/api/_generated/organizations/types/organizations_list_response_item.py +25 -0
  32. mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +7 -0
  33. mirascope/api/_generated/organizations/types/organizations_update_response.py +25 -0
  34. mirascope/api/_generated/organizations/types/organizations_update_response_role.py +7 -0
  35. mirascope/api/_generated/projects/__init__.py +17 -0
  36. mirascope/api/_generated/projects/client.py +482 -0
  37. mirascope/api/_generated/projects/raw_client.py +1058 -0
  38. mirascope/api/_generated/projects/types/__init__.py +15 -0
  39. mirascope/api/_generated/projects/types/projects_create_response.py +31 -0
  40. mirascope/api/_generated/projects/types/projects_get_response.py +31 -0
  41. mirascope/api/_generated/projects/types/projects_list_response_item.py +31 -0
  42. mirascope/api/_generated/projects/types/projects_update_response.py +31 -0
  43. mirascope/api/_generated/reference.md +1311 -0
  44. mirascope/api/_generated/types/__init__.py +20 -4
  45. mirascope/api/_generated/types/already_exists_error.py +24 -0
  46. mirascope/api/_generated/types/already_exists_error_tag.py +5 -0
  47. mirascope/api/_generated/types/database_error.py +24 -0
  48. mirascope/api/_generated/types/database_error_tag.py +5 -0
  49. mirascope/api/_generated/types/http_api_decode_error.py +1 -3
  50. mirascope/api/_generated/types/issue.py +1 -5
  51. mirascope/api/_generated/types/not_found_error_body.py +24 -0
  52. mirascope/api/_generated/types/not_found_error_tag.py +5 -0
  53. mirascope/api/_generated/types/permission_denied_error.py +24 -0
  54. mirascope/api/_generated/types/permission_denied_error_tag.py +7 -0
  55. mirascope/api/_generated/types/property_key.py +2 -2
  56. mirascope/api/_generated/types/{property_key_tag.py → property_key_key.py} +3 -5
  57. mirascope/api/_generated/types/{property_key_tag_tag.py → property_key_key_tag.py} +1 -1
  58. mirascope/llm/__init__.py +6 -2
  59. mirascope/llm/exceptions.py +28 -0
  60. mirascope/llm/providers/__init__.py +12 -4
  61. mirascope/llm/providers/anthropic/__init__.py +6 -1
  62. mirascope/llm/providers/anthropic/_utils/__init__.py +17 -5
  63. mirascope/llm/providers/anthropic/_utils/beta_decode.py +271 -0
  64. mirascope/llm/providers/anthropic/_utils/beta_encode.py +216 -0
  65. mirascope/llm/providers/anthropic/_utils/decode.py +39 -7
  66. mirascope/llm/providers/anthropic/_utils/encode.py +156 -64
  67. mirascope/llm/providers/anthropic/_utils/errors.py +46 -0
  68. mirascope/llm/providers/anthropic/beta_provider.py +328 -0
  69. mirascope/llm/providers/anthropic/model_id.py +10 -27
  70. mirascope/llm/providers/anthropic/model_info.py +87 -0
  71. mirascope/llm/providers/anthropic/provider.py +132 -145
  72. mirascope/llm/providers/base/__init__.py +2 -1
  73. mirascope/llm/providers/base/_utils.py +15 -1
  74. mirascope/llm/providers/base/base_provider.py +173 -58
  75. mirascope/llm/providers/google/_utils/__init__.py +2 -0
  76. mirascope/llm/providers/google/_utils/decode.py +55 -3
  77. mirascope/llm/providers/google/_utils/encode.py +14 -6
  78. mirascope/llm/providers/google/_utils/errors.py +49 -0
  79. mirascope/llm/providers/google/model_id.py +7 -13
  80. mirascope/llm/providers/google/model_info.py +62 -0
  81. mirascope/llm/providers/google/provider.py +13 -8
  82. mirascope/llm/providers/mlx/_utils.py +31 -2
  83. mirascope/llm/providers/mlx/encoding/transformers.py +17 -1
  84. mirascope/llm/providers/mlx/provider.py +12 -0
  85. mirascope/llm/providers/ollama/__init__.py +19 -0
  86. mirascope/llm/providers/ollama/provider.py +71 -0
  87. mirascope/llm/providers/openai/__init__.py +10 -1
  88. mirascope/llm/providers/openai/_utils/__init__.py +5 -0
  89. mirascope/llm/providers/openai/_utils/errors.py +46 -0
  90. mirascope/llm/providers/openai/completions/__init__.py +6 -1
  91. mirascope/llm/providers/openai/completions/_utils/decode.py +57 -5
  92. mirascope/llm/providers/openai/completions/_utils/encode.py +9 -8
  93. mirascope/llm/providers/openai/completions/base_provider.py +513 -0
  94. mirascope/llm/providers/openai/completions/provider.py +13 -447
  95. mirascope/llm/providers/openai/model_info.py +57 -0
  96. mirascope/llm/providers/openai/provider.py +30 -5
  97. mirascope/llm/providers/openai/responses/_utils/decode.py +55 -4
  98. mirascope/llm/providers/openai/responses/_utils/encode.py +9 -9
  99. mirascope/llm/providers/openai/responses/provider.py +33 -28
  100. mirascope/llm/providers/provider_id.py +11 -1
  101. mirascope/llm/providers/provider_registry.py +59 -4
  102. mirascope/llm/providers/together/__init__.py +19 -0
  103. mirascope/llm/providers/together/provider.py +40 -0
  104. mirascope/llm/responses/__init__.py +3 -0
  105. mirascope/llm/responses/base_response.py +4 -0
  106. mirascope/llm/responses/base_stream_response.py +25 -1
  107. mirascope/llm/responses/finish_reason.py +1 -0
  108. mirascope/llm/responses/response.py +9 -0
  109. mirascope/llm/responses/root_response.py +5 -1
  110. mirascope/llm/responses/usage.py +95 -0
  111. mirascope/ops/_internal/closure.py +62 -11
  112. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/METADATA +3 -3
  113. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/RECORD +115 -56
  114. mirascope/llm/providers/load_provider.py +0 -48
  115. mirascope/llm/providers/openai/shared/__init__.py +0 -7
  116. mirascope/llm/providers/openai/shared/_utils.py +0 -59
  117. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/WHEEL +0 -0
  118. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/licenses/LICENSE +0 -0
@@ -3,18 +3,22 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
- from collections.abc import Sequence
7
- from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeAlias, overload
6
+ from collections.abc import Callable, Generator, Mapping, Sequence
7
+ from contextlib import contextmanager
8
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeAlias, cast, overload
8
9
  from typing_extensions import TypeVar, Unpack
9
10
 
10
11
  from ...context import Context, DepsT
12
+ from ...exceptions import APIError, MirascopeLLMError
11
13
  from ...formatting import Format, FormattableT
12
14
  from ...messages import Message, UserContent, user
13
15
  from ...responses import (
16
+ AsyncChunkIterator,
14
17
  AsyncContextResponse,
15
18
  AsyncContextStreamResponse,
16
19
  AsyncResponse,
17
20
  AsyncStreamResponse,
21
+ ChunkIterator,
18
22
  ContextResponse,
19
23
  ContextStreamResponse,
20
24
  Response,
@@ -33,6 +37,7 @@ from ...tools import (
33
37
  from .params import Params
34
38
 
35
39
  if TYPE_CHECKING:
40
+ from ...exceptions import MirascopeLLMError
36
41
  from ..provider_id import ProviderId
37
42
 
38
43
  ProviderClientT = TypeVar("ProviderClientT")
@@ -40,6 +45,18 @@ ProviderClientT = TypeVar("ProviderClientT")
40
45
  Provider: TypeAlias = "BaseProvider[Any]"
41
46
  """Type alias for `BaseProvider` with any client type."""
42
47
 
48
+ ProviderErrorMap: TypeAlias = Mapping[
49
+ type[Exception],
50
+ "type[MirascopeLLMError] | Callable[[Exception], type[MirascopeLLMError]]",
51
+ ]
52
+ """Mapping from provider SDK exceptions to Mirascope error types.
53
+
54
+ Keys are provider SDK exception types (e.g., OpenAIError, AnthropicError).
55
+ Values can be:
56
+ - Error type: Simple 1:1 mapping (e.g., RateLimitError)
57
+ - Callable: Transform function returning error type based on exception details
58
+ """
59
+
43
60
 
44
61
  class BaseProvider(Generic[ProviderClientT], ABC):
45
62
  """Base abstract provider for LLM interactions.
@@ -59,8 +76,67 @@ class BaseProvider(Generic[ProviderClientT], ABC):
59
76
  - ["anthropic/", "openai/"] - Multiple scopes (e.g., for AWS Bedrock)
60
77
  """
61
78
 
79
+ error_map: ClassVar[ProviderErrorMap]
80
+ """Mapping from provider SDK exceptions to Mirascope error types.
81
+
82
+ Values can be:
83
+ - Error type: Simple 1:1 mapping (e.g., AnthropicRateLimitError -> RateLimitError)
84
+ - Callable: Transform function returning error type based on exception details
85
+ (e.g., lambda e: NotFoundError if e.code == "model_not_found" else BadRequestError)
86
+
87
+ The mapping is walked via the exception's MRO, allowing both specific error handling
88
+ and fallback to base SDK error types (e.g., AnthropicError -> APIError).
89
+ """
90
+
62
91
  client: ProviderClientT
63
92
 
93
+ @contextmanager
94
+ def _wrap_errors(self) -> Generator[None, None, None]:
95
+ """Wrap provider API calls and convert errors to Mirascope exceptions.
96
+
97
+ Walks the exception's MRO to find the first matching error type in the
98
+ provider's error_map, allowing both specific error handling and fallback
99
+ to base SDK error types (e.g., AnthropicError -> APIError).
100
+ """
101
+ try:
102
+ yield
103
+ except Exception as e:
104
+ # Walk MRO to find first matching error type in provider's error_map
105
+ for error_class in type(e).__mro__:
106
+ if error_class in self.error_map:
107
+ error_type_or_fn = self.error_map[error_class]
108
+
109
+ if isinstance(error_type_or_fn, type):
110
+ error_type = cast(type[MirascopeLLMError], error_type_or_fn)
111
+ else:
112
+ error_type = error_type_or_fn(e)
113
+
114
+ # Construct Mirascope error with metadata
115
+ error: MirascopeLLMError = error_type(str(e))
116
+ if isinstance(error, APIError):
117
+ error.status_code = self.get_error_status(e)
118
+ error.provider = self.id
119
+ error.original_exception = e
120
+ raise error from e
121
+
122
+ # Not in error_map - not a provider error, re-raise as-is
123
+ raise
124
+
125
+ def _wrap_iterator_errors(self, iterator: ChunkIterator) -> ChunkIterator:
126
+ """Wrap sync chunk iterator to handle errors during iteration."""
127
+ # TODO: Consider moving this logic into BaseSyncStreamResponse if appropriate.
128
+ with self._wrap_errors():
129
+ yield from iterator
130
+
131
+ async def _wrap_async_iterator_errors(
132
+ self, iterator: AsyncChunkIterator
133
+ ) -> AsyncChunkIterator:
134
+ """Wrap async chunk iterator to handle errors during iteration."""
135
+ # TODO: Consider moving this logic into BaseAsyncStreamResponse if appropriate.
136
+ with self._wrap_errors():
137
+ async for chunk in iterator:
138
+ yield chunk
139
+
64
140
  @overload
65
141
  def call(
66
142
  self,
@@ -121,13 +197,14 @@ class BaseProvider(Generic[ProviderClientT], ABC):
121
197
  Returns:
122
198
  An `llm.Response` object containing the LLM-generated content.
123
199
  """
124
- return self._call(
125
- model_id=model_id,
126
- messages=messages,
127
- tools=tools,
128
- format=format,
129
- **params,
130
- )
200
+ with self._wrap_errors():
201
+ return self._call(
202
+ model_id=model_id,
203
+ messages=messages,
204
+ tools=tools,
205
+ format=format,
206
+ **params,
207
+ )
131
208
 
132
209
  @abstractmethod
133
210
  def _call(
@@ -215,14 +292,15 @@ class BaseProvider(Generic[ProviderClientT], ABC):
215
292
  Returns:
216
293
  An `llm.ContextResponse` object containing the LLM-generated content.
217
294
  """
218
- return self._context_call(
219
- ctx=ctx,
220
- model_id=model_id,
221
- messages=messages,
222
- tools=tools,
223
- format=format,
224
- **params,
225
- )
295
+ with self._wrap_errors():
296
+ return self._context_call(
297
+ ctx=ctx,
298
+ model_id=model_id,
299
+ messages=messages,
300
+ tools=tools,
301
+ format=format,
302
+ **params,
303
+ )
226
304
 
227
305
  @abstractmethod
228
306
  def _context_call(
@@ -300,13 +378,14 @@ class BaseProvider(Generic[ProviderClientT], ABC):
300
378
  Returns:
301
379
  An `llm.AsyncResponse` object containing the LLM-generated content.
302
380
  """
303
- return await self._call_async(
304
- model_id=model_id,
305
- messages=messages,
306
- tools=tools,
307
- format=format,
308
- **params,
309
- )
381
+ with self._wrap_errors():
382
+ return await self._call_async(
383
+ model_id=model_id,
384
+ messages=messages,
385
+ tools=tools,
386
+ format=format,
387
+ **params,
388
+ )
310
389
 
311
390
  @abstractmethod
312
391
  async def _call_async(
@@ -394,14 +473,15 @@ class BaseProvider(Generic[ProviderClientT], ABC):
394
473
  Returns:
395
474
  An `llm.AsyncContextResponse` object containing the LLM-generated content.
396
475
  """
397
- return await self._context_call_async(
398
- ctx=ctx,
399
- model_id=model_id,
400
- messages=messages,
401
- tools=tools,
402
- format=format,
403
- **params,
404
- )
476
+ with self._wrap_errors():
477
+ return await self._context_call_async(
478
+ ctx=ctx,
479
+ model_id=model_id,
480
+ messages=messages,
481
+ tools=tools,
482
+ format=format,
483
+ **params,
484
+ )
405
485
 
406
486
  @abstractmethod
407
487
  async def _context_call_async(
@@ -479,13 +559,18 @@ class BaseProvider(Generic[ProviderClientT], ABC):
479
559
  Returns:
480
560
  An `llm.StreamResponse` object for iterating over the LLM-generated content.
481
561
  """
482
- return self._stream(
483
- model_id=model_id,
484
- messages=messages,
485
- tools=tools,
486
- format=format,
487
- **params,
562
+ with self._wrap_errors():
563
+ stream_response = self._stream(
564
+ model_id=model_id,
565
+ messages=messages,
566
+ tools=tools,
567
+ format=format,
568
+ **params,
569
+ )
570
+ stream_response._chunk_iterator = self._wrap_iterator_errors( # pyright: ignore[reportPrivateUsage]
571
+ stream_response._chunk_iterator # pyright: ignore[reportPrivateUsage]
488
572
  )
573
+ return stream_response
489
574
 
490
575
  @abstractmethod
491
576
  def _stream(
@@ -577,14 +662,19 @@ class BaseProvider(Generic[ProviderClientT], ABC):
577
662
  Returns:
578
663
  An `llm.ContextStreamResponse` object for iterating over the LLM-generated content.
579
664
  """
580
- return self._context_stream(
581
- ctx=ctx,
582
- model_id=model_id,
583
- messages=messages,
584
- tools=tools,
585
- format=format,
586
- **params,
665
+ with self._wrap_errors():
666
+ stream_response = self._context_stream(
667
+ ctx=ctx,
668
+ model_id=model_id,
669
+ messages=messages,
670
+ tools=tools,
671
+ format=format,
672
+ **params,
673
+ )
674
+ stream_response._chunk_iterator = self._wrap_iterator_errors( # pyright: ignore[reportPrivateUsage]
675
+ stream_response._chunk_iterator # pyright: ignore[reportPrivateUsage]
587
676
  )
677
+ return stream_response
588
678
 
589
679
  @abstractmethod
590
680
  def _context_stream(
@@ -664,13 +754,18 @@ class BaseProvider(Generic[ProviderClientT], ABC):
664
754
  Returns:
665
755
  An `llm.AsyncStreamResponse` object for asynchronously iterating over the LLM-generated content.
666
756
  """
667
- return await self._stream_async(
668
- model_id=model_id,
669
- messages=messages,
670
- tools=tools,
671
- format=format,
672
- **params,
757
+ with self._wrap_errors():
758
+ stream_response = await self._stream_async(
759
+ model_id=model_id,
760
+ messages=messages,
761
+ tools=tools,
762
+ format=format,
763
+ **params,
764
+ )
765
+ stream_response._chunk_iterator = self._wrap_async_iterator_errors( # pyright: ignore[reportPrivateUsage]
766
+ stream_response._chunk_iterator # pyright: ignore[reportPrivateUsage]
673
767
  )
768
+ return stream_response
674
769
 
675
770
  @abstractmethod
676
771
  async def _stream_async(
@@ -764,14 +859,19 @@ class BaseProvider(Generic[ProviderClientT], ABC):
764
859
  Returns:
765
860
  An `llm.AsyncContextStreamResponse` object for asynchronously iterating over the LLM-generated content.
766
861
  """
767
- return await self._context_stream_async(
768
- ctx=ctx,
769
- model_id=model_id,
770
- messages=messages,
771
- tools=tools,
772
- format=format,
773
- **params,
862
+ with self._wrap_errors():
863
+ stream_response = await self._context_stream_async(
864
+ ctx=ctx,
865
+ model_id=model_id,
866
+ messages=messages,
867
+ tools=tools,
868
+ format=format,
869
+ **params,
870
+ )
871
+ stream_response._chunk_iterator = self._wrap_async_iterator_errors( # pyright: ignore[reportPrivateUsage]
872
+ stream_response._chunk_iterator # pyright: ignore[reportPrivateUsage]
774
873
  )
874
+ return stream_response
775
875
 
776
876
  @abstractmethod
777
877
  async def _context_stream_async(
@@ -1383,3 +1483,18 @@ class BaseProvider(Generic[ProviderClientT], ABC):
1383
1483
  format=response.format,
1384
1484
  **params,
1385
1485
  )
1486
+
1487
+ @abstractmethod
1488
+ def get_error_status(self, e: Exception) -> int | None:
1489
+ """Extract HTTP status code from provider-specific exception.
1490
+
1491
+ Different SDKs store status codes differently (e.g., .status_code vs .code).
1492
+ Each provider implements this to handle their SDK's convention.
1493
+
1494
+ Args:
1495
+ e: The exception to extract status code from.
1496
+
1497
+ Returns:
1498
+ The HTTP status code if available, None otherwise.
1499
+ """
1500
+ ...
@@ -4,8 +4,10 @@ from .decode import (
4
4
  decode_stream,
5
5
  )
6
6
  from .encode import encode_request
7
+ from .errors import GOOGLE_ERROR_MAP
7
8
 
8
9
  __all__ = [
10
+ "GOOGLE_ERROR_MAP",
9
11
  "decode_async_stream",
10
12
  "decode_response",
11
13
  "decode_stream",
@@ -29,6 +29,8 @@ from ....responses import (
29
29
  FinishReasonChunk,
30
30
  RawMessageChunk,
31
31
  RawStreamEventChunk,
32
+ Usage,
33
+ UsageDeltaChunk,
32
34
  )
33
35
  from ..model_id import GoogleModelId, model_name
34
36
  from .encode import UNKNOWN_TOOL_ID
@@ -43,6 +45,30 @@ GOOGLE_FINISH_REASON_MAP = {
43
45
  }
44
46
 
45
47
 
48
+ def _decode_usage(
49
+ usage: genai_types.GenerateContentResponseUsageMetadata | None,
50
+ ) -> Usage | None:
51
+ """Convert Google UsageMetadata to Mirascope Usage."""
52
+ if (
53
+ usage is None
54
+ or usage.prompt_token_count is None
55
+ or usage.candidates_token_count is None
56
+ ): # pragma: no cover
57
+ return None
58
+
59
+ reasoning_tokens = usage.thoughts_token_count or 0
60
+ output_tokens = usage.candidates_token_count + reasoning_tokens
61
+
62
+ return Usage(
63
+ input_tokens=usage.prompt_token_count,
64
+ output_tokens=output_tokens,
65
+ cache_read_tokens=usage.cached_content_token_count or 0,
66
+ cache_write_tokens=0,
67
+ reasoning_tokens=usage.thoughts_token_count or 0,
68
+ raw=usage,
69
+ )
70
+
71
+
46
72
  def _decode_content_part(part: genai_types.Part) -> AssistantContentPart | None:
47
73
  """Returns an `AssistantContentPart` (or `None`) decoded from a google `Part`"""
48
74
  if part.thought and part.text:
@@ -100,8 +126,8 @@ def _decode_candidate_content(
100
126
  def decode_response(
101
127
  response: genai_types.GenerateContentResponse,
102
128
  model_id: GoogleModelId,
103
- ) -> tuple[AssistantMessage, FinishReason | None]:
104
- """Returns an `AssistantMessage` and `FinishReason` extracted from a `GenerateContentResponse`"""
129
+ ) -> tuple[AssistantMessage, FinishReason | None, Usage | None]:
130
+ """Returns an `AssistantMessage`, `FinishReason`, and `Usage` extracted from a `GenerateContentResponse`"""
105
131
  content: Sequence[AssistantContentPart] = []
106
132
  candidate_content: genai_types.Content | None = None
107
133
  finish_reason: FinishReason | None = None
@@ -122,7 +148,8 @@ def decode_response(
122
148
  raw_message=candidate_content.model_dump(),
123
149
  )
124
150
 
125
- return assistant_message, finish_reason
151
+ usage = _decode_usage(response.usage_metadata)
152
+ return assistant_message, finish_reason, usage
126
153
 
127
154
 
128
155
  class _GoogleChunkProcessor:
@@ -132,6 +159,8 @@ class _GoogleChunkProcessor:
132
159
  self.current_content_type: Literal["text", "tool_call", "thought"] | None = None
133
160
  self.accumulated_parts: list[genai_types.Part] = []
134
161
  self.reconstructed_content = genai_types.Content(parts=[])
162
+ # Track previous cumulative usage to compute deltas
163
+ self.prev_usage = Usage()
135
164
 
136
165
  def process_chunk(
137
166
  self, chunk: genai_types.GenerateContentResponse
@@ -207,6 +236,29 @@ class _GoogleChunkProcessor:
207
236
  if finish_reason is not None:
208
237
  yield FinishReasonChunk(finish_reason=finish_reason)
209
238
 
239
+ # Emit usage delta if usage metadata is present
240
+ if chunk.usage_metadata:
241
+ usage_metadata = chunk.usage_metadata
242
+ current_input = usage_metadata.prompt_token_count or 0
243
+ current_output = usage_metadata.candidates_token_count or 0
244
+ current_cache_read = usage_metadata.cached_content_token_count or 0
245
+ current_reasoning = usage_metadata.thoughts_token_count or 0
246
+
247
+ yield UsageDeltaChunk(
248
+ input_tokens=current_input - self.prev_usage.input_tokens,
249
+ output_tokens=current_output - self.prev_usage.output_tokens,
250
+ cache_read_tokens=current_cache_read
251
+ - self.prev_usage.cache_read_tokens,
252
+ cache_write_tokens=0,
253
+ reasoning_tokens=current_reasoning - self.prev_usage.reasoning_tokens,
254
+ )
255
+
256
+ # Update previous usage
257
+ self.prev_usage.input_tokens = current_input
258
+ self.prev_usage.output_tokens = current_output
259
+ self.prev_usage.cache_read_tokens = current_cache_read
260
+ self.prev_usage.reasoning_tokens = current_reasoning
261
+
210
262
  def raw_message_chunk(self) -> RawMessageChunk:
211
263
  content = genai_types.Content(role="model", parts=self.accumulated_parts)
212
264
  return RawMessageChunk(raw_message=content.model_dump())
@@ -21,6 +21,7 @@ from ....messages import AssistantMessage, Message, UserMessage
21
21
  from ....tools import FORMAT_TOOL_NAME, AnyToolSchema, BaseToolkit
22
22
  from ...base import Params, _utils as _base_utils
23
23
  from ..model_id import GoogleModelId, model_name
24
+ from ..model_info import MODELS_WITHOUT_STRUCTURED_OUTPUT_AND_TOOLS_SUPPORT
24
25
 
25
26
  UNKNOWN_TOOL_ID = "google_unknown_tool_id"
26
27
 
@@ -187,6 +188,7 @@ def encode_request(
187
188
  genai_types.GenerateContentConfigDict()
188
189
  )
189
190
  encode_thoughts = False
191
+ google_model_name = model_name(model_id)
190
192
 
191
193
  with _base_utils.ensure_all_params_accessed(
192
194
  params=params, provider_id="google"
@@ -219,17 +221,23 @@ def encode_request(
219
221
  tools = tools.tools if isinstance(tools, BaseToolkit) else tools or []
220
222
  google_tools: list[genai_types.ToolDict] = []
221
223
 
222
- format = resolve_format(
223
- format,
224
- # Google does not support strict outputs when tools are present
225
- # (Gemini 2.5 will error, 2.0 and below will ignore tools)
226
- default_mode="strict" if not tools else "tool",
224
+ allows_strict_mode_with_tools = (
225
+ google_model_name not in MODELS_WITHOUT_STRUCTURED_OUTPUT_AND_TOOLS_SUPPORT
227
226
  )
227
+ # Older google models do not allow strict mode when using tools; if so, we use tool
228
+ # mode when tools are present by default for compatibility. Otherwise, prefer strict mode.
229
+ default_mode = "tool" if tools and not allows_strict_mode_with_tools else "strict"
230
+ format = resolve_format(format, default_mode=default_mode)
228
231
  if format is not None:
229
- if format.mode in ("strict", "json") and tools:
232
+ if (
233
+ format.mode in ("strict", "json")
234
+ and tools
235
+ and not allows_strict_mode_with_tools
236
+ ):
230
237
  raise FeatureNotSupportedError(
231
238
  feature=f"formatting_mode:{format.mode} with tools",
232
239
  provider_id="google",
240
+ model_id=model_id,
233
241
  )
234
242
 
235
243
  if format.mode == "strict":
@@ -0,0 +1,49 @@
1
+ """Google error handling utilities."""
2
+
3
+ from google.genai.errors import (
4
+ ClientError as GoogleClientError,
5
+ ServerError as GoogleServerError,
6
+ )
7
+
8
+ from ....exceptions import (
9
+ APIError,
10
+ AuthenticationError,
11
+ BadRequestError,
12
+ NotFoundError,
13
+ PermissionError,
14
+ RateLimitError,
15
+ ServerError,
16
+ )
17
+ from ...base import ProviderErrorMap
18
+
19
+
20
+ def map_google_error(e: Exception) -> type[APIError]:
21
+ """Map Google error to appropriate Mirascope error type.
22
+
23
+ Google only provides ClientError (4xx) and ServerError (5xx) with status codes,
24
+ so we map based on status code and message patterns.
25
+ """
26
+ if not isinstance(e, GoogleClientError | GoogleServerError):
27
+ return APIError
28
+
29
+ # Authentication errors (401) or 400 with "API key not valid"
30
+ if e.code == 401 or (e.code == 400 and "API key not valid" in str(e)):
31
+ return AuthenticationError
32
+ if e.code == 403:
33
+ return PermissionError
34
+ if e.code == 404:
35
+ return NotFoundError
36
+ if e.code == 429:
37
+ return RateLimitError
38
+ if e.code in (400, 422):
39
+ return BadRequestError
40
+ if isinstance(e, GoogleServerError) and e.code >= 500:
41
+ return ServerError
42
+ return APIError
43
+
44
+
45
+ # Shared error mapping for Google provider
46
+ GOOGLE_ERROR_MAP: ProviderErrorMap = {
47
+ GoogleClientError: map_google_error,
48
+ GoogleServerError: map_google_error,
49
+ }
@@ -1,20 +1,14 @@
1
1
  """Google registered LLM models."""
2
2
 
3
- from typing import Literal, TypeAlias
4
-
5
- GoogleModelId: TypeAlias = (
6
- Literal[
7
- "google/gemini-3-pro-preview",
8
- "google/gemini-2.5-pro",
9
- "google/gemini-2.5-flash",
10
- "google/gemini-2.5-flash-lite",
11
- "google/gemini-2.0-flash",
12
- "google/gemini-2.0-flash-lite",
13
- ]
14
- | str
15
- )
3
+ from typing import TypeAlias, get_args
4
+
5
+ from .model_info import GoogleKnownModels
6
+
7
+ GoogleModelId: TypeAlias = GoogleKnownModels | str
16
8
  """The Google model ids registered with Mirascope."""
17
9
 
10
+ GOOGLE_KNOWN_MODELS: set[str] = set(get_args(GoogleKnownModels))
11
+
18
12
 
19
13
  def model_name(model_id: GoogleModelId) -> str:
20
14
  """Extract the google model name from a full model ID.
@@ -0,0 +1,62 @@
1
+ """Google model information.
2
+
3
+ This file is auto-generated by scripts/model_features/codegen_google.py
4
+ Do not edit manually - run the codegen script to update."""
5
+
6
+ from typing import Literal
7
+
8
+ GoogleKnownModels = Literal[
9
+ "google/gemini-2.0-flash",
10
+ "google/gemini-2.0-flash-001",
11
+ "google/gemini-2.0-flash-exp",
12
+ "google/gemini-2.0-flash-exp-image-generation",
13
+ "google/gemini-2.0-flash-lite",
14
+ "google/gemini-2.0-flash-lite-001",
15
+ "google/gemini-2.0-flash-lite-preview",
16
+ "google/gemini-2.0-flash-lite-preview-02-05",
17
+ "google/gemini-2.5-flash",
18
+ "google/gemini-2.5-flash-image",
19
+ "google/gemini-2.5-flash-image-preview",
20
+ "google/gemini-2.5-flash-lite",
21
+ "google/gemini-2.5-flash-lite-preview-09-2025",
22
+ "google/gemini-2.5-flash-preview-09-2025",
23
+ "google/gemini-2.5-pro",
24
+ "google/gemini-3-pro-image-preview",
25
+ "google/gemini-3-pro-preview",
26
+ "google/gemini-flash-latest",
27
+ "google/gemini-flash-lite-latest",
28
+ "google/gemini-pro-latest",
29
+ "google/gemini-robotics-er-1.5-preview",
30
+ "google/gemma-3-12b-it",
31
+ "google/gemma-3-1b-it",
32
+ "google/gemma-3-27b-it",
33
+ "google/gemma-3-4b-it",
34
+ "google/gemma-3n-e2b-it",
35
+ "google/gemma-3n-e4b-it",
36
+ "google/nano-banana-pro-preview",
37
+ ]
38
+ """Valid Google model IDs."""
39
+
40
+
41
+ MODELS_WITHOUT_STRUCTURED_OUTPUT_AND_TOOLS_SUPPORT: set[str] = {
42
+ "gemini-2.5-flash",
43
+ "gemini-2.5-flash-image",
44
+ "gemini-2.5-flash-image-preview",
45
+ "gemini-2.5-flash-lite",
46
+ "gemini-2.5-flash-lite-preview-09-2025",
47
+ "gemini-2.5-flash-preview-09-2025",
48
+ "gemini-2.5-pro",
49
+ "gemini-3-pro-image-preview",
50
+ "gemini-flash-latest",
51
+ "gemini-flash-lite-latest",
52
+ "gemini-pro-latest",
53
+ "gemini-robotics-er-1.5-preview",
54
+ "gemma-3-12b-it",
55
+ "gemma-3-1b-it",
56
+ "gemma-3-27b-it",
57
+ "gemma-3-4b-it",
58
+ "gemma-3n-e2b-it",
59
+ "gemma-3n-e4b-it",
60
+ "nano-banana-pro-preview",
61
+ }
62
+ """Models that do not support structured outputs when tools are present."""