mirascope 1.19.0__py3-none-any.whl → 1.20.1__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 (90) hide show
  1. mirascope/__init__.py +4 -0
  2. mirascope/beta/openai/realtime/realtime.py +7 -8
  3. mirascope/beta/openai/realtime/tool.py +2 -2
  4. mirascope/core/__init__.py +10 -1
  5. mirascope/core/anthropic/_utils/__init__.py +0 -2
  6. mirascope/core/anthropic/_utils/_convert_message_params.py +1 -7
  7. mirascope/core/anthropic/_utils/_message_param_converter.py +48 -31
  8. mirascope/core/anthropic/call_response.py +7 -9
  9. mirascope/core/anthropic/call_response_chunk.py +10 -0
  10. mirascope/core/anthropic/stream.py +6 -8
  11. mirascope/core/azure/_utils/__init__.py +0 -2
  12. mirascope/core/azure/call_response.py +7 -10
  13. mirascope/core/azure/call_response_chunk.py +6 -1
  14. mirascope/core/azure/stream.py +6 -8
  15. mirascope/core/base/__init__.py +10 -1
  16. mirascope/core/base/_utils/__init__.py +2 -0
  17. mirascope/core/base/_utils/_get_image_dimensions.py +39 -0
  18. mirascope/core/base/call_response.py +36 -6
  19. mirascope/core/base/call_response_chunk.py +15 -1
  20. mirascope/core/base/stream.py +25 -3
  21. mirascope/core/base/types.py +276 -2
  22. mirascope/core/bedrock/_utils/__init__.py +0 -2
  23. mirascope/core/bedrock/call_response.py +7 -10
  24. mirascope/core/bedrock/call_response_chunk.py +6 -0
  25. mirascope/core/bedrock/stream.py +6 -10
  26. mirascope/core/cohere/_utils/__init__.py +0 -2
  27. mirascope/core/cohere/call_response.py +7 -10
  28. mirascope/core/cohere/call_response_chunk.py +6 -0
  29. mirascope/core/cohere/stream.py +5 -8
  30. mirascope/core/costs/__init__.py +5 -0
  31. mirascope/core/{anthropic/_utils/_calculate_cost.py → costs/_anthropic_calculate_cost.py} +45 -14
  32. mirascope/core/{azure/_utils/_calculate_cost.py → costs/_azure_calculate_cost.py} +3 -3
  33. mirascope/core/{bedrock/_utils/_calculate_cost.py → costs/_bedrock_calculate_cost.py} +3 -3
  34. mirascope/core/{cohere/_utils/_calculate_cost.py → costs/_cohere_calculate_cost.py} +12 -8
  35. mirascope/core/{gemini/_utils/_calculate_cost.py → costs/_gemini_calculate_cost.py} +7 -7
  36. mirascope/core/costs/_google_calculate_cost.py +427 -0
  37. mirascope/core/costs/_groq_calculate_cost.py +156 -0
  38. mirascope/core/costs/_litellm_calculate_cost.py +11 -0
  39. mirascope/core/costs/_mistral_calculate_cost.py +64 -0
  40. mirascope/core/costs/_openai_calculate_cost.py +416 -0
  41. mirascope/core/{vertex/_utils/_calculate_cost.py → costs/_vertex_calculate_cost.py} +8 -7
  42. mirascope/core/{xai/_utils/_calculate_cost.py → costs/_xai_calculate_cost.py} +9 -9
  43. mirascope/core/costs/calculate_cost.py +86 -0
  44. mirascope/core/gemini/_utils/__init__.py +0 -2
  45. mirascope/core/gemini/call_response.py +7 -10
  46. mirascope/core/gemini/call_response_chunk.py +6 -1
  47. mirascope/core/gemini/stream.py +5 -8
  48. mirascope/core/google/_utils/__init__.py +0 -2
  49. mirascope/core/google/_utils/_setup_call.py +21 -2
  50. mirascope/core/google/call_response.py +9 -10
  51. mirascope/core/google/call_response_chunk.py +6 -1
  52. mirascope/core/google/stream.py +5 -8
  53. mirascope/core/groq/_utils/__init__.py +0 -2
  54. mirascope/core/groq/call_response.py +22 -10
  55. mirascope/core/groq/call_response_chunk.py +6 -0
  56. mirascope/core/groq/stream.py +5 -8
  57. mirascope/core/litellm/call_response.py +3 -4
  58. mirascope/core/litellm/stream.py +30 -22
  59. mirascope/core/mistral/_utils/__init__.py +0 -2
  60. mirascope/core/mistral/call_response.py +7 -10
  61. mirascope/core/mistral/call_response_chunk.py +6 -0
  62. mirascope/core/mistral/stream.py +5 -8
  63. mirascope/core/openai/_utils/__init__.py +0 -2
  64. mirascope/core/openai/_utils/_convert_message_params.py +4 -4
  65. mirascope/core/openai/call_response.py +30 -10
  66. mirascope/core/openai/call_response_chunk.py +6 -0
  67. mirascope/core/openai/stream.py +5 -8
  68. mirascope/core/vertex/_utils/__init__.py +0 -2
  69. mirascope/core/vertex/call_response.py +5 -10
  70. mirascope/core/vertex/call_response_chunk.py +6 -0
  71. mirascope/core/vertex/stream.py +5 -8
  72. mirascope/core/xai/_utils/__init__.py +1 -2
  73. mirascope/core/xai/call_response.py +0 -11
  74. mirascope/llm/__init__.py +10 -2
  75. mirascope/llm/_protocols.py +8 -28
  76. mirascope/llm/call_response.py +6 -6
  77. mirascope/llm/call_response_chunk.py +12 -3
  78. mirascope/llm/llm_call.py +21 -23
  79. mirascope/llm/llm_override.py +56 -27
  80. mirascope/llm/stream.py +7 -7
  81. mirascope/llm/tool.py +1 -1
  82. mirascope/retries/fallback.py +1 -1
  83. {mirascope-1.19.0.dist-info → mirascope-1.20.1.dist-info}/METADATA +1 -1
  84. {mirascope-1.19.0.dist-info → mirascope-1.20.1.dist-info}/RECORD +86 -82
  85. mirascope/core/google/_utils/_calculate_cost.py +0 -215
  86. mirascope/core/groq/_utils/_calculate_cost.py +0 -69
  87. mirascope/core/mistral/_utils/_calculate_cost.py +0 -48
  88. mirascope/core/openai/_utils/_calculate_cost.py +0 -246
  89. {mirascope-1.19.0.dist-info → mirascope-1.20.1.dist-info}/WHEEL +0 -0
  90. {mirascope-1.19.0.dist-info → mirascope-1.20.1.dist-info}/licenses/LICENSE +0 -0
mirascope/__init__.py CHANGED
@@ -17,7 +17,9 @@ from .core import (
17
17
  DocumentPart,
18
18
  ImagePart,
19
19
  ImageURLPart,
20
+ LocalProvider,
20
21
  Messages,
22
+ Provider,
21
23
  TextPart,
22
24
  ToolCallPart,
23
25
  ToolResultPart,
@@ -43,7 +45,9 @@ __all__ = [
43
45
  "DocumentPart",
44
46
  "ImagePart",
45
47
  "ImageURLPart",
48
+ "LocalProvider",
46
49
  "Messages",
50
+ "Provider",
47
51
  "TextPart",
48
52
  "ToolCallPart",
49
53
  "ToolResultPart",
@@ -5,6 +5,7 @@ import base64
5
5
  import inspect
6
6
  import json
7
7
  import os
8
+ from collections import defaultdict
8
9
  from collections.abc import Callable
9
10
  from functools import lru_cache
10
11
  from io import BytesIO
@@ -17,23 +18,21 @@ from typing import (
17
18
  )
18
19
 
19
20
  import websockets
20
- from black.trans import defaultdict
21
21
  from pydantic import BaseModel
22
22
  from pydub import AudioSegment
23
23
  from typing_extensions import NotRequired, overload
24
24
  from websockets.asyncio.client import ClientConnection, connect
25
25
 
26
- from mirascope.beta.openai.realtime._utils._audio import (
26
+ from ....core import BaseTool
27
+ from ....core.base._utils import (
28
+ convert_base_model_to_base_tool,
29
+ convert_function_to_base_tool,
30
+ )
31
+ from ._utils._audio import (
27
32
  async_audio_input_audio_buffer_append_event,
28
33
  async_audio_to_item_create_event,
29
34
  audio_chunk_to_audio_segment,
30
35
  )
31
- from mirascope.core import BaseTool
32
- from mirascope.core.base._utils import (
33
- convert_base_model_to_base_tool,
34
- convert_function_to_base_tool,
35
- )
36
-
37
36
  from ._utils._protocols import FunctionCallHandlerFunc, ReceiverFunc, SenderFunc
38
37
  from .tool import FunctionCallArguments, OpenAIRealtimeTool, RealtimeToolParam
39
38
 
@@ -6,8 +6,8 @@ import jiter
6
6
  from pydantic.json_schema import SkipJsonSchema
7
7
  from typing_extensions import NotRequired, TypedDict
8
8
 
9
- from mirascope.core import BaseTool
10
- from mirascope.core.base import GenerateJsonSchemaNoTitles, ToolConfig
9
+ from ....core import BaseTool
10
+ from ....core.base import GenerateJsonSchemaNoTitles, ToolConfig
11
11
 
12
12
 
13
13
  class OpenAIRealtimeToolConfig(ToolConfig, total=False):
@@ -2,7 +2,7 @@
2
2
 
3
3
  from contextlib import suppress
4
4
 
5
- from . import base
5
+ from . import base, costs
6
6
  from .base import (
7
7
  AudioPart,
8
8
  AudioURLPart,
@@ -14,11 +14,14 @@ from .base import (
14
14
  BaseTool,
15
15
  BaseToolKit,
16
16
  CacheControlPart,
17
+ CostMetadata,
17
18
  DocumentPart,
18
19
  FromCallArgs,
19
20
  ImagePart,
20
21
  ImageURLPart,
22
+ LocalProvider,
21
23
  Messages,
24
+ Provider,
22
25
  ResponseModelConfigDict,
23
26
  TextPart,
24
27
  ToolCallPart,
@@ -28,6 +31,7 @@ from .base import (
28
31
  prompt_template,
29
32
  toolkit_tool,
30
33
  )
34
+ from .costs import calculate_cost
31
35
 
32
36
  with suppress(ImportError):
33
37
  from . import anthropic as anthropic
@@ -71,11 +75,14 @@ __all__ = [
71
75
  "BaseTool",
72
76
  "BaseToolKit",
73
77
  "CacheControlPart",
78
+ "CostMetadata",
74
79
  "DocumentPart",
75
80
  "FromCallArgs",
76
81
  "ImagePart",
77
82
  "ImageURLPart",
83
+ "LocalProvider",
78
84
  "Messages",
85
+ "Provider",
79
86
  "ResponseModelConfigDict",
80
87
  "TextPart",
81
88
  "ToolCallPart",
@@ -83,7 +90,9 @@ __all__ = [
83
90
  "anthropic",
84
91
  "azure",
85
92
  "base",
93
+ "calculate_cost",
86
94
  "cohere",
95
+ "costs",
87
96
  "gemini",
88
97
  "google",
89
98
  "groq",
@@ -1,6 +1,5 @@
1
1
  """Anthropic utilities for decorator factories."""
2
2
 
3
- from ._calculate_cost import calculate_cost
4
3
  from ._convert_common_call_params import convert_common_call_params
5
4
  from ._convert_message_params import convert_message_params
6
5
  from ._get_json_output import get_json_output
@@ -8,7 +7,6 @@ from ._handle_stream import handle_stream, handle_stream_async
8
7
  from ._setup_call import setup_call
9
8
 
10
9
  __all__ = [
11
- "calculate_cost",
12
10
  "convert_common_call_params",
13
11
  "convert_message_params",
14
12
  "get_json_output",
@@ -5,7 +5,6 @@ import base64
5
5
  from anthropic.types import MessageParam
6
6
 
7
7
  from ...base import BaseMessageParam
8
- from ...base._utils._parse_content_template import _load_media, get_image_type
9
8
 
10
9
 
11
10
  def convert_message_params(
@@ -47,15 +46,10 @@ def convert_message_params(
47
46
  }
48
47
  )
49
48
  elif part.type == "image_url":
50
- image = _load_media(part.url)
51
49
  converted_content.append(
52
50
  {
53
51
  "type": "image",
54
- "source": {
55
- "data": base64.b64encode(image).decode("utf-8"),
56
- "media_type": f"image/{get_image_type(image)}",
57
- "type": "base64",
58
- },
52
+ "source": {"url": part.url, "type": "url"},
59
53
  }
60
54
  )
61
55
  elif part.type == "document":
@@ -6,7 +6,13 @@ from anthropic.types import MessageParam
6
6
 
7
7
  from mirascope.core import BaseMessageParam
8
8
  from mirascope.core.anthropic._utils import convert_message_params
9
- from mirascope.core.base import ImagePart, TextPart, ToolCallPart, ToolResultPart
9
+ from mirascope.core.base import (
10
+ ImagePart,
11
+ ImageURLPart,
12
+ TextPart,
13
+ ToolCallPart,
14
+ ToolResultPart,
15
+ )
10
16
  from mirascope.core.base._utils._base_message_param_converter import (
11
17
  BaseMessageParamConverter,
12
18
  )
@@ -53,41 +59,52 @@ class AnthropicMessageParamConverter(BaseMessageParamConverter):
53
59
 
54
60
  elif block["type"] == "image":
55
61
  source = block.get("source")
56
- if not source or source.get("type") != "base64":
57
- raise ValueError(
58
- "ImageBlockParam must have a 'source' with type='base64'."
59
- )
60
- image_data = source.get("data")
61
- media_type = source.get("media_type")
62
- if not image_data or not media_type:
62
+ source_type = source.get("type") if source else None
63
+ if not source or source_type not in ["base64", "url"]:
63
64
  raise ValueError(
64
- "ImageBlockParam source must have 'data' and 'media_type'."
65
+ "ImageBlockParam must have a 'source' with type='base64' or type='url'."
65
66
  )
66
- if media_type not in [
67
- "image/jpeg",
68
- "image/png",
69
- "image/gif",
70
- "image/webp",
71
- ]:
72
- raise ValueError(
73
- f"Unsupported image media type: {media_type}. "
74
- "BaseMessageParam currently only supports JPEG, PNG, GIF, and WebP images."
67
+ if source_type == "url":
68
+ url = source.get("url")
69
+ if not url:
70
+ raise ValueError(
71
+ "ImageBlockParam source with type='url' must have a 'url'."
72
+ )
73
+ converted_content.append(
74
+ ImageURLPart(type="image_url", url=url, detail=None)
75
75
  )
76
- if isinstance(image_data, str):
77
- decoded_image_data = base64.b64decode(image_data)
78
- elif isinstance(image_data, PathLike):
79
- with open(image_data, "rb") as image_data:
80
- decoded_image_data = image_data.read()
81
76
  else:
82
- decoded_image_data = image_data.read()
83
- converted_content.append(
84
- ImagePart(
85
- type="image",
86
- media_type=media_type,
87
- image=decoded_image_data,
88
- detail=None,
77
+ image_data = source.get("data")
78
+ media_type = source.get("media_type")
79
+ if not image_data or not media_type:
80
+ raise ValueError(
81
+ "ImageBlockParam source with type='base64' must have 'data' and 'media_type'."
82
+ )
83
+ if media_type not in [
84
+ "image/jpeg",
85
+ "image/png",
86
+ "image/gif",
87
+ "image/webp",
88
+ ]:
89
+ raise ValueError(
90
+ f"Unsupported image media type: {media_type}. "
91
+ "BaseMessageParam currently only supports JPEG, PNG, GIF, and WebP images."
92
+ )
93
+ if isinstance(image_data, str):
94
+ decoded_image_data = base64.b64decode(image_data)
95
+ elif isinstance(image_data, PathLike):
96
+ with open(image_data, "rb") as image_data:
97
+ decoded_image_data = image_data.read()
98
+ else:
99
+ decoded_image_data = image_data.read()
100
+ converted_content.append(
101
+ ImagePart(
102
+ type="image",
103
+ media_type=media_type,
104
+ image=decoded_image_data,
105
+ detail=None,
106
+ )
89
107
  )
90
- )
91
108
 
92
109
  elif block["type"] == "tool_use":
93
110
  if converted_content:
@@ -16,7 +16,7 @@ from pydantic import SerializeAsAny, computed_field
16
16
 
17
17
  from .. import BaseMessageParam
18
18
  from ..base import BaseCallResponse, transform_tool_outputs, types
19
- from ._utils import calculate_cost
19
+ from ..base.types import CostMetadata
20
20
  from ._utils._convert_finish_reason_to_common_finish_reasons import (
21
21
  _convert_finish_reasons_to_common_finish_reasons,
22
22
  )
@@ -112,14 +112,6 @@ class AnthropicCallResponse(
112
112
  """Returns the number of output tokens."""
113
113
  return self.usage.output_tokens
114
114
 
115
- @computed_field
116
- @property
117
- def cost(self) -> float | None:
118
- """Returns the cost of the call."""
119
- return calculate_cost(
120
- self.input_tokens, self.cached_tokens, self.output_tokens, self.model
121
- )
122
-
123
115
  @computed_field
124
116
  @cached_property
125
117
  def message_param(self) -> SerializeAsAny[MessageParam]:
@@ -202,3 +194,9 @@ class AnthropicCallResponse(
202
194
  return AnthropicMessageParamConverter.from_provider(
203
195
  [(self.user_message_param)]
204
196
  )[0]
197
+
198
+ @computed_field
199
+ @property
200
+ def cost_metadata(self) -> CostMetadata:
201
+ """Get metadata required for cost calculation."""
202
+ return super().cost_metadata
@@ -16,6 +16,7 @@ from anthropic.types import (
16
16
  )
17
17
 
18
18
  from ..base import BaseCallResponseChunk, types
19
+ from ..base.types import CostMetadata
19
20
  from ._utils._convert_finish_reason_to_common_finish_reasons import (
20
21
  _convert_finish_reasons_to_common_finish_reasons,
21
22
  )
@@ -114,6 +115,15 @@ class AnthropicCallResponseChunk(
114
115
  return self.usage.output_tokens
115
116
  return None
116
117
 
118
+ @property
119
+ def cost_metadata(self) -> CostMetadata:
120
+ """Returns the cost metadata."""
121
+ return CostMetadata(
122
+ input_tokens=self.input_tokens,
123
+ output_tokens=self.output_tokens,
124
+ cached_tokens=self.cached_tokens,
125
+ )
126
+
117
127
  @property
118
128
  def common_finish_reasons(self) -> list[types.FinishReason] | None:
119
129
  """Provider-agnostic finish reasons."""
@@ -17,7 +17,7 @@ from anthropic.types.tool_use_block_param import ToolUseBlockParam
17
17
  from pydantic import BaseModel
18
18
 
19
19
  from ..base.stream import BaseStream
20
- from ._utils import calculate_cost
20
+ from ..base.types import CostMetadata
21
21
  from .call_params import AnthropicCallParams
22
22
  from .call_response import AnthropicCallResponse
23
23
  from .call_response_chunk import AnthropicCallResponseChunk
@@ -64,13 +64,6 @@ class AnthropicStream(
64
64
 
65
65
  _provider = "anthropic"
66
66
 
67
- @property
68
- def cost(self) -> float | None:
69
- """Returns the cost of the call."""
70
- return calculate_cost(
71
- self.input_tokens, self.cached_tokens, self.output_tokens, self.model
72
- )
73
-
74
67
  def _construct_message_param(
75
68
  self, tool_calls: list[ToolUseBlock] | None = None, content: str | None = None
76
69
  ) -> MessageParam:
@@ -147,3 +140,8 @@ class AnthropicStream(
147
140
  start_time=self.start_time,
148
141
  end_time=self.end_time,
149
142
  )
143
+
144
+ @property
145
+ def cost_metadata(self) -> CostMetadata:
146
+ """Get metadata required for cost calculation."""
147
+ return super().cost_metadata
@@ -1,13 +1,11 @@
1
1
  """Azure utilities for decorator factories."""
2
2
 
3
- from ._calculate_cost import calculate_cost
4
3
  from ._convert_message_params import convert_message_params
5
4
  from ._get_json_output import get_json_output
6
5
  from ._handle_stream import handle_stream, handle_stream_async
7
6
  from ._setup_call import setup_call
8
7
 
9
8
  __all__ = [
10
- "calculate_cost",
11
9
  "convert_message_params",
12
10
  "get_json_output",
13
11
  "handle_stream",
@@ -19,8 +19,7 @@ from pydantic import SerializeAsAny, SkipValidation, computed_field
19
19
 
20
20
  from .. import BaseMessageParam
21
21
  from ..base import BaseCallResponse, transform_tool_outputs
22
- from ..base.types import FinishReason
23
- from ._utils import calculate_cost
22
+ from ..base.types import CostMetadata, FinishReason
24
23
  from ._utils._convert_finish_reason_to_common_finish_reasons import (
25
24
  _convert_finish_reasons_to_common_finish_reasons,
26
25
  )
@@ -116,14 +115,6 @@ class AzureCallResponse(
116
115
  """Returns the number of output tokens."""
117
116
  return self.usage.completion_tokens if self.usage else None
118
117
 
119
- @computed_field
120
- @property
121
- def cost(self) -> float | None:
122
- """Returns the cost of the call."""
123
- return calculate_cost(
124
- self.input_tokens, self.cached_tokens, self.output_tokens, self.model
125
- )
126
-
127
118
  @computed_field
128
119
  @cached_property
129
120
  def message_param(self) -> SerializeAsAny[AssistantMessage]:
@@ -213,3 +204,9 @@ class AzureCallResponse(
213
204
  if not self.user_message_param:
214
205
  return None
215
206
  return AzureMessageParamConverter.from_provider([self.user_message_param])[0]
207
+
208
+ @computed_field
209
+ @property
210
+ def cost_metadata(self) -> CostMetadata:
211
+ """Get metadata required for cost calculation."""
212
+ return super().cost_metadata
@@ -13,7 +13,7 @@ from azure.ai.inference.models import (
13
13
  from pydantic import SkipValidation
14
14
 
15
15
  from ..base import BaseCallResponseChunk
16
- from ..base.types import FinishReason
16
+ from ..base.types import CostMetadata, FinishReason
17
17
 
18
18
 
19
19
  class AzureCallResponseChunk(
@@ -94,6 +94,11 @@ class AzureCallResponseChunk(
94
94
  """Returns the number of output tokens."""
95
95
  return self.usage.completion_tokens
96
96
 
97
+ @property
98
+ def cost_metadata(self) -> CostMetadata:
99
+ """Returns the cost metadata."""
100
+ return super().cost_metadata
101
+
97
102
  @property
98
103
  def common_finish_reasons(self) -> list[FinishReason] | None:
99
104
  """Provider-agnostic finish reasons."""
@@ -21,7 +21,7 @@ from azure.ai.inference.models import (
21
21
  )
22
22
 
23
23
  from ..base.stream import BaseStream
24
- from ._utils import calculate_cost
24
+ from ..base.types import CostMetadata
25
25
  from .call_params import AzureCallParams
26
26
  from .call_response import AzureCallResponse
27
27
  from .call_response_chunk import AzureCallResponseChunk
@@ -66,13 +66,6 @@ class AzureStream(
66
66
 
67
67
  _provider = "azure"
68
68
 
69
- @property
70
- def cost(self) -> float | None:
71
- """Returns the cost of the call."""
72
- return calculate_cost(
73
- self.input_tokens, self.cached_tokens, self.output_tokens, self.model
74
- )
75
-
76
69
  def _construct_message_param(
77
70
  self,
78
71
  tool_calls: list[ChatCompletionsToolCall] | None = None,
@@ -147,3 +140,8 @@ class AzureStream(
147
140
  start_time=self.start_time,
148
141
  end_time=self.end_time,
149
142
  )
143
+
144
+ @property
145
+ def cost_metadata(self) -> CostMetadata:
146
+ """Get metadata required for cost calculation."""
147
+ return super().cost_metadata
@@ -30,7 +30,14 @@ from .stream import BaseStream
30
30
  from .structured_stream import BaseStructuredStream
31
31
  from .tool import BaseTool, GenerateJsonSchemaNoTitles, ToolConfig
32
32
  from .toolkit import BaseToolKit, toolkit_tool
33
- from .types import AudioSegment, JsonableType, Usage
33
+ from .types import (
34
+ AudioSegment,
35
+ CostMetadata,
36
+ JsonableType,
37
+ LocalProvider,
38
+ Provider,
39
+ Usage,
40
+ )
34
41
 
35
42
  __all__ = [
36
43
  "AudioPart",
@@ -50,12 +57,14 @@ __all__ = [
50
57
  "BaseType",
51
58
  "CacheControlPart",
52
59
  "CommonCallParams",
60
+ "CostMetadata",
53
61
  "DocumentPart",
54
62
  "FromCallArgs",
55
63
  "GenerateJsonSchemaNoTitles",
56
64
  "ImagePart",
57
65
  "ImageURLPart",
58
66
  "JsonableType",
67
+ "LocalProvider",
59
68
  "Messages",
60
69
  "Metadata",
61
70
  "ResponseModelConfigDict",
@@ -14,6 +14,7 @@ from ._get_create_fn_or_async_create_fn import get_async_create_fn, get_create_f
14
14
  from ._get_document_type import get_document_type
15
15
  from ._get_dynamic_configuration import get_dynamic_configuration
16
16
  from ._get_fn_args import get_fn_args
17
+ from ._get_image_dimensions import get_image_dimensions
17
18
  from ._get_image_type import get_image_type
18
19
  from ._get_metadata import get_metadata
19
20
  from ._get_possible_user_message_param import get_possible_user_message_param
@@ -68,6 +69,7 @@ __all__ = [
68
69
  "get_document_type",
69
70
  "get_dynamic_configuration",
70
71
  "get_fn_args",
72
+ "get_image_dimensions",
71
73
  "get_image_type",
72
74
  "get_metadata",
73
75
  "get_possible_user_message_param",
@@ -0,0 +1,39 @@
1
+ import base64
2
+ import io
3
+ import re
4
+
5
+ from ...base.types import Image, ImageMetadata, has_pil_module
6
+ from ._parse_content_template import _load_media
7
+
8
+
9
+ def get_image_dimensions(data_url: str) -> ImageMetadata | None:
10
+ """
11
+ Extract width and height from a base64 encoded image.
12
+
13
+ Args:
14
+ data_url: The data URL containing base64 encoded image data or External URL
15
+
16
+ Returns:
17
+ Dictionary with width and height, or None if extraction failed
18
+ """
19
+ try:
20
+ if data_url.startswith("http"):
21
+ binary_data = _load_media(data_url)
22
+ else:
23
+ # Extract the base64 data part from the URL
24
+ # Format is: data:[<media type>][;base64],<data>
25
+ pattern = r"data:image/[^;]+;base64,"
26
+ base64_data = re.sub(pattern, "", data_url)
27
+
28
+ # Decode the base64 data
29
+ binary_data = base64.b64decode(base64_data)
30
+
31
+ # Use PIL to open the image and get dimensions
32
+ if not has_pil_module: # pragma: no cover
33
+ raise ImportError("PIL module is not available")
34
+ with Image.open(io.BytesIO(binary_data)) as image:
35
+ width, height = image.size
36
+
37
+ return ImageMetadata(width=width, height=height)
38
+ except Exception:
39
+ return None
@@ -7,7 +7,7 @@ import json
7
7
  from abc import ABC, abstractmethod
8
8
  from collections.abc import Callable
9
9
  from functools import cached_property, wraps
10
- from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar
10
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, cast
11
11
 
12
12
  from pydantic import (
13
13
  BaseModel,
@@ -18,13 +18,14 @@ from pydantic import (
18
18
  field_serializer,
19
19
  )
20
20
 
21
+ from ..costs import calculate_cost
21
22
  from ._utils import BaseType, get_common_usage
22
23
  from .call_kwargs import BaseCallKwargs
23
24
  from .call_params import BaseCallParams
24
25
  from .dynamic_config import BaseDynamicConfig
25
26
  from .metadata import Metadata
26
27
  from .tool import BaseTool
27
- from .types import FinishReason, JsonableType, Usage
28
+ from .types import CostMetadata, FinishReason, JsonableType, Provider, Usage
28
29
 
29
30
  if TYPE_CHECKING:
30
31
  from ...llm.tool import Tool
@@ -225,12 +226,41 @@ class BaseCallResponse(
225
226
  @computed_field
226
227
  @property
227
228
  @abstractmethod
228
- def cost(self) -> float | None:
229
- """Should return the cost of the response in dollars.
229
+ def cost_metadata(self) -> CostMetadata:
230
+ """Get metadata required for cost calculation.
230
231
 
231
- If there is no cost, this method must return None.
232
+ Returns:
233
+ Metadata relevant to cost calculation
232
234
  """
233
- ...
235
+
236
+ return CostMetadata(
237
+ input_tokens=self.input_tokens,
238
+ output_tokens=self.output_tokens,
239
+ cached_tokens=self.cached_tokens,
240
+ )
241
+
242
+ @computed_field
243
+ @property
244
+ def cost(self) -> float | None:
245
+ """Calculate the cost of this API call using the unified calculate_cost function."""
246
+
247
+ model = self.model
248
+ if not model:
249
+ return None
250
+
251
+ if self.input_tokens is None or self.output_tokens is None:
252
+ return None
253
+
254
+ return calculate_cost(
255
+ provider=self.provider,
256
+ model=model,
257
+ metadata=self.cost_metadata,
258
+ )
259
+
260
+ @property
261
+ def provider(self) -> Provider:
262
+ """Get the provider used for this API call."""
263
+ return cast(Provider, self._provider)
234
264
 
235
265
  @computed_field
236
266
  @cached_property
@@ -11,7 +11,7 @@ from typing import Any, Generic, TypeVar
11
11
  from pydantic import BaseModel, ConfigDict
12
12
 
13
13
  from mirascope.core.base._utils import get_common_usage
14
- from mirascope.core.base.types import FinishReason, Usage
14
+ from mirascope.core.base.types import CostMetadata, FinishReason, Usage
15
15
 
16
16
  _ChunkT = TypeVar("_ChunkT", bound=Any)
17
17
  _FinishReasonT = TypeVar("_FinishReasonT", bound=Any)
@@ -102,6 +102,20 @@ class BaseCallResponseChunk(BaseModel, Generic[_ChunkT, _FinishReasonT], ABC):
102
102
  """
103
103
  ...
104
104
 
105
+ @property
106
+ @abstractmethod
107
+ def cost_metadata(self) -> CostMetadata:
108
+ """Get metadata required for cost calculation.
109
+
110
+ Returns:
111
+ A CostMetadata object with information relevant to cost calculation
112
+ """
113
+ return CostMetadata(
114
+ input_tokens=self.input_tokens,
115
+ output_tokens=self.output_tokens,
116
+ cached_tokens=self.cached_tokens,
117
+ )
118
+
105
119
  @property
106
120
  @abstractmethod
107
121
  def common_finish_reasons(self) -> list[FinishReason] | None: