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.
- mirascope/api/_generated/__init__.py +78 -6
- mirascope/api/_generated/api_keys/__init__.py +7 -0
- mirascope/api/_generated/api_keys/client.py +453 -0
- mirascope/api/_generated/api_keys/raw_client.py +853 -0
- mirascope/api/_generated/api_keys/types/__init__.py +9 -0
- mirascope/api/_generated/api_keys/types/api_keys_create_response.py +36 -0
- mirascope/api/_generated/api_keys/types/api_keys_get_response.py +35 -0
- mirascope/api/_generated/api_keys/types/api_keys_list_response_item.py +35 -0
- mirascope/api/_generated/client.py +14 -0
- mirascope/api/_generated/environments/__init__.py +17 -0
- mirascope/api/_generated/environments/client.py +532 -0
- mirascope/api/_generated/environments/raw_client.py +1088 -0
- mirascope/api/_generated/environments/types/__init__.py +15 -0
- mirascope/api/_generated/environments/types/environments_create_response.py +26 -0
- mirascope/api/_generated/environments/types/environments_get_response.py +26 -0
- mirascope/api/_generated/environments/types/environments_list_response_item.py +26 -0
- mirascope/api/_generated/environments/types/environments_update_response.py +26 -0
- mirascope/api/_generated/errors/__init__.py +11 -1
- mirascope/api/_generated/errors/conflict_error.py +15 -0
- mirascope/api/_generated/errors/forbidden_error.py +15 -0
- mirascope/api/_generated/errors/internal_server_error.py +15 -0
- mirascope/api/_generated/errors/not_found_error.py +15 -0
- mirascope/api/_generated/organizations/__init__.py +25 -0
- mirascope/api/_generated/organizations/client.py +404 -0
- mirascope/api/_generated/organizations/raw_client.py +902 -0
- mirascope/api/_generated/organizations/types/__init__.py +23 -0
- mirascope/api/_generated/organizations/types/organizations_create_response.py +25 -0
- mirascope/api/_generated/organizations/types/organizations_create_response_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_get_response.py +25 -0
- mirascope/api/_generated/organizations/types/organizations_get_response_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_list_response_item.py +25 -0
- mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_update_response.py +25 -0
- mirascope/api/_generated/organizations/types/organizations_update_response_role.py +7 -0
- mirascope/api/_generated/projects/__init__.py +17 -0
- mirascope/api/_generated/projects/client.py +482 -0
- mirascope/api/_generated/projects/raw_client.py +1058 -0
- mirascope/api/_generated/projects/types/__init__.py +15 -0
- mirascope/api/_generated/projects/types/projects_create_response.py +31 -0
- mirascope/api/_generated/projects/types/projects_get_response.py +31 -0
- mirascope/api/_generated/projects/types/projects_list_response_item.py +31 -0
- mirascope/api/_generated/projects/types/projects_update_response.py +31 -0
- mirascope/api/_generated/reference.md +1311 -0
- mirascope/api/_generated/types/__init__.py +20 -4
- mirascope/api/_generated/types/already_exists_error.py +24 -0
- mirascope/api/_generated/types/already_exists_error_tag.py +5 -0
- mirascope/api/_generated/types/database_error.py +24 -0
- mirascope/api/_generated/types/database_error_tag.py +5 -0
- mirascope/api/_generated/types/http_api_decode_error.py +1 -3
- mirascope/api/_generated/types/issue.py +1 -5
- mirascope/api/_generated/types/not_found_error_body.py +24 -0
- mirascope/api/_generated/types/not_found_error_tag.py +5 -0
- mirascope/api/_generated/types/permission_denied_error.py +24 -0
- mirascope/api/_generated/types/permission_denied_error_tag.py +7 -0
- mirascope/api/_generated/types/property_key.py +2 -2
- mirascope/api/_generated/types/{property_key_tag.py → property_key_key.py} +3 -5
- mirascope/api/_generated/types/{property_key_tag_tag.py → property_key_key_tag.py} +1 -1
- mirascope/llm/__init__.py +6 -2
- mirascope/llm/exceptions.py +28 -0
- mirascope/llm/providers/__init__.py +12 -4
- mirascope/llm/providers/anthropic/__init__.py +6 -1
- mirascope/llm/providers/anthropic/_utils/__init__.py +17 -5
- mirascope/llm/providers/anthropic/_utils/beta_decode.py +271 -0
- mirascope/llm/providers/anthropic/_utils/beta_encode.py +216 -0
- mirascope/llm/providers/anthropic/_utils/decode.py +39 -7
- mirascope/llm/providers/anthropic/_utils/encode.py +156 -64
- mirascope/llm/providers/anthropic/_utils/errors.py +46 -0
- mirascope/llm/providers/anthropic/beta_provider.py +328 -0
- mirascope/llm/providers/anthropic/model_id.py +10 -27
- mirascope/llm/providers/anthropic/model_info.py +87 -0
- mirascope/llm/providers/anthropic/provider.py +132 -145
- mirascope/llm/providers/base/__init__.py +2 -1
- mirascope/llm/providers/base/_utils.py +15 -1
- mirascope/llm/providers/base/base_provider.py +173 -58
- mirascope/llm/providers/google/_utils/__init__.py +2 -0
- mirascope/llm/providers/google/_utils/decode.py +55 -3
- mirascope/llm/providers/google/_utils/encode.py +14 -6
- mirascope/llm/providers/google/_utils/errors.py +49 -0
- mirascope/llm/providers/google/model_id.py +7 -13
- mirascope/llm/providers/google/model_info.py +62 -0
- mirascope/llm/providers/google/provider.py +13 -8
- mirascope/llm/providers/mlx/_utils.py +31 -2
- mirascope/llm/providers/mlx/encoding/transformers.py +17 -1
- mirascope/llm/providers/mlx/provider.py +12 -0
- mirascope/llm/providers/ollama/__init__.py +19 -0
- mirascope/llm/providers/ollama/provider.py +71 -0
- mirascope/llm/providers/openai/__init__.py +10 -1
- mirascope/llm/providers/openai/_utils/__init__.py +5 -0
- mirascope/llm/providers/openai/_utils/errors.py +46 -0
- mirascope/llm/providers/openai/completions/__init__.py +6 -1
- mirascope/llm/providers/openai/completions/_utils/decode.py +57 -5
- mirascope/llm/providers/openai/completions/_utils/encode.py +9 -8
- mirascope/llm/providers/openai/completions/base_provider.py +513 -0
- mirascope/llm/providers/openai/completions/provider.py +13 -447
- mirascope/llm/providers/openai/model_info.py +57 -0
- mirascope/llm/providers/openai/provider.py +30 -5
- mirascope/llm/providers/openai/responses/_utils/decode.py +55 -4
- mirascope/llm/providers/openai/responses/_utils/encode.py +9 -9
- mirascope/llm/providers/openai/responses/provider.py +33 -28
- mirascope/llm/providers/provider_id.py +11 -1
- mirascope/llm/providers/provider_registry.py +59 -4
- mirascope/llm/providers/together/__init__.py +19 -0
- mirascope/llm/providers/together/provider.py +40 -0
- mirascope/llm/responses/__init__.py +3 -0
- mirascope/llm/responses/base_response.py +4 -0
- mirascope/llm/responses/base_stream_response.py +25 -1
- mirascope/llm/responses/finish_reason.py +1 -0
- mirascope/llm/responses/response.py +9 -0
- mirascope/llm/responses/root_response.py +5 -1
- mirascope/llm/responses/usage.py +95 -0
- mirascope/ops/_internal/closure.py +62 -11
- {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/METADATA +3 -3
- {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/RECORD +115 -56
- mirascope/llm/providers/load_provider.py +0 -48
- mirascope/llm/providers/openai/shared/__init__.py +0 -7
- mirascope/llm/providers/openai/shared/_utils.py +0 -59
- {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
+
...
|
|
@@ -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 `
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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."""
|