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.
- mirascope/_utils.py +34 -0
- mirascope/api/_generated/__init__.py +186 -5
- mirascope/api/_generated/annotations/client.py +38 -6
- mirascope/api/_generated/annotations/raw_client.py +366 -47
- mirascope/api/_generated/annotations/types/annotations_create_response.py +19 -6
- mirascope/api/_generated/annotations/types/annotations_get_response.py +19 -6
- mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item.py +22 -7
- mirascope/api/_generated/annotations/types/annotations_update_response.py +19 -6
- mirascope/api/_generated/api_keys/__init__.py +12 -2
- mirascope/api/_generated/api_keys/client.py +107 -6
- mirascope/api/_generated/api_keys/raw_client.py +486 -38
- mirascope/api/_generated/api_keys/types/__init__.py +7 -1
- mirascope/api/_generated/api_keys/types/api_keys_list_all_for_org_response_item.py +40 -0
- mirascope/api/_generated/client.py +36 -0
- mirascope/api/_generated/docs/raw_client.py +71 -9
- mirascope/api/_generated/environment.py +3 -3
- mirascope/api/_generated/environments/__init__.py +6 -0
- mirascope/api/_generated/environments/client.py +158 -9
- mirascope/api/_generated/environments/raw_client.py +620 -52
- mirascope/api/_generated/environments/types/__init__.py +10 -0
- mirascope/api/_generated/environments/types/environments_get_analytics_response.py +60 -0
- mirascope/api/_generated/environments/types/environments_get_analytics_response_top_functions_item.py +24 -0
- mirascope/api/_generated/{organizations/types/organizations_credits_response.py → environments/types/environments_get_analytics_response_top_models_item.py} +6 -3
- mirascope/api/_generated/errors/__init__.py +6 -0
- mirascope/api/_generated/errors/bad_request_error.py +5 -2
- mirascope/api/_generated/errors/conflict_error.py +5 -2
- mirascope/api/_generated/errors/payment_required_error.py +15 -0
- mirascope/api/_generated/errors/service_unavailable_error.py +14 -0
- mirascope/api/_generated/errors/too_many_requests_error.py +15 -0
- mirascope/api/_generated/functions/__init__.py +10 -0
- mirascope/api/_generated/functions/client.py +222 -8
- mirascope/api/_generated/functions/raw_client.py +975 -134
- mirascope/api/_generated/functions/types/__init__.py +28 -4
- mirascope/api/_generated/functions/types/functions_get_by_env_response.py +53 -0
- mirascope/api/_generated/functions/types/functions_get_by_env_response_dependencies_value.py +22 -0
- mirascope/api/_generated/functions/types/functions_list_by_env_response.py +25 -0
- mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +56 -0
- mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item_dependencies_value.py +22 -0
- mirascope/api/_generated/health/raw_client.py +74 -10
- mirascope/api/_generated/organization_invitations/__init__.py +33 -0
- mirascope/api/_generated/organization_invitations/client.py +546 -0
- mirascope/api/_generated/organization_invitations/raw_client.py +1519 -0
- mirascope/api/_generated/organization_invitations/types/__init__.py +53 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response.py +34 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_create_request_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response.py +48 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_status.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response.py +48 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_status.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item.py +48 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_status.py +7 -0
- mirascope/api/_generated/organization_memberships/__init__.py +19 -0
- mirascope/api/_generated/organization_memberships/client.py +302 -0
- mirascope/api/_generated/organization_memberships/raw_client.py +736 -0
- mirascope/api/_generated/organization_memberships/types/__init__.py +27 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item.py +33 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item_role.py +7 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_update_request_role.py +7 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response.py +31 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response_role.py +7 -0
- mirascope/api/_generated/organizations/__init__.py +26 -2
- mirascope/api/_generated/organizations/client.py +442 -20
- mirascope/api/_generated/organizations/raw_client.py +1763 -164
- mirascope/api/_generated/organizations/types/__init__.py +48 -2
- mirascope/api/_generated/organizations/types/organizations_create_payment_intent_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_request_target_plan.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response.py +47 -0
- mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item.py +33 -0
- mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item_resource.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_router_balance_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response.py +53 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response_current_plan.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response_payment_method.py +26 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change.py +34 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change_target_plan.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_update_subscription_request_target_plan.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_update_subscription_response.py +35 -0
- mirascope/api/_generated/project_memberships/__init__.py +25 -0
- mirascope/api/_generated/project_memberships/client.py +437 -0
- mirascope/api/_generated/project_memberships/raw_client.py +1039 -0
- mirascope/api/_generated/project_memberships/types/__init__.py +29 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_create_request_role.py +7 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_create_response.py +35 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_create_response_role.py +7 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item.py +33 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item_role.py +7 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_update_request_role.py +7 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_update_response.py +35 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_update_response_role.py +7 -0
- mirascope/api/_generated/projects/raw_client.py +415 -58
- mirascope/api/_generated/reference.md +2767 -397
- mirascope/api/_generated/tags/__init__.py +19 -0
- mirascope/api/_generated/tags/client.py +504 -0
- mirascope/api/_generated/tags/raw_client.py +1288 -0
- mirascope/api/_generated/tags/types/__init__.py +17 -0
- mirascope/api/_generated/tags/types/tags_create_response.py +41 -0
- mirascope/api/_generated/tags/types/tags_get_response.py +41 -0
- mirascope/api/_generated/tags/types/tags_list_response.py +23 -0
- mirascope/api/_generated/tags/types/tags_list_response_tags_item.py +41 -0
- mirascope/api/_generated/tags/types/tags_update_response.py +41 -0
- mirascope/api/_generated/token_cost/__init__.py +7 -0
- mirascope/api/_generated/token_cost/client.py +160 -0
- mirascope/api/_generated/token_cost/raw_client.py +264 -0
- mirascope/api/_generated/token_cost/types/__init__.py +8 -0
- mirascope/api/_generated/token_cost/types/token_cost_calculate_request_usage.py +54 -0
- mirascope/api/_generated/token_cost/types/token_cost_calculate_response.py +52 -0
- mirascope/api/_generated/traces/__init__.py +20 -0
- mirascope/api/_generated/traces/client.py +543 -0
- mirascope/api/_generated/traces/raw_client.py +1366 -96
- mirascope/api/_generated/traces/types/__init__.py +28 -0
- mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py +6 -0
- mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response.py +33 -0
- mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response_spans_item.py +88 -0
- mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py +0 -2
- mirascope/api/_generated/traces/types/traces_list_by_function_hash_response.py +25 -0
- mirascope/api/_generated/traces/types/traces_list_by_function_hash_response_traces_item.py +44 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item.py +26 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item_operator.py +7 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_by.py +7 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_order.py +7 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_response.py +26 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_response_spans_item.py +50 -0
- mirascope/api/_generated/traces/types/traces_search_response_spans_item.py +10 -1
- mirascope/api/_generated/types/__init__.py +32 -2
- mirascope/api/_generated/types/bad_request_error_body.py +50 -0
- mirascope/api/_generated/types/date.py +3 -0
- mirascope/api/_generated/types/immutable_resource_error.py +22 -0
- mirascope/api/_generated/types/internal_server_error_body.py +3 -3
- mirascope/api/_generated/types/plan_limit_exceeded_error.py +32 -0
- mirascope/api/_generated/types/plan_limit_exceeded_error_tag.py +7 -0
- mirascope/api/_generated/types/pricing_unavailable_error.py +23 -0
- mirascope/api/_generated/types/rate_limit_error.py +31 -0
- mirascope/api/_generated/types/rate_limit_error_tag.py +5 -0
- mirascope/api/_generated/types/service_unavailable_error_body.py +24 -0
- mirascope/api/_generated/types/service_unavailable_error_tag.py +7 -0
- mirascope/api/_generated/types/subscription_past_due_error.py +31 -0
- mirascope/api/_generated/types/subscription_past_due_error_tag.py +7 -0
- mirascope/api/settings.py +19 -1
- mirascope/llm/__init__.py +53 -10
- mirascope/llm/calls/__init__.py +2 -1
- mirascope/llm/calls/calls.py +29 -20
- mirascope/llm/calls/decorator.py +21 -7
- mirascope/llm/content/tool_output.py +22 -5
- mirascope/llm/exceptions.py +284 -71
- mirascope/llm/formatting/__init__.py +17 -0
- mirascope/llm/formatting/format.py +112 -35
- mirascope/llm/formatting/output_parser.py +178 -0
- mirascope/llm/formatting/partial.py +80 -7
- mirascope/llm/formatting/primitives.py +192 -0
- mirascope/llm/formatting/types.py +20 -8
- mirascope/llm/messages/__init__.py +3 -0
- mirascope/llm/messages/_utils.py +34 -0
- mirascope/llm/models/__init__.py +5 -0
- mirascope/llm/models/models.py +137 -69
- mirascope/llm/{providers/base → models}/params.py +7 -57
- mirascope/llm/models/thinking_config.py +61 -0
- mirascope/llm/prompts/_utils.py +0 -32
- mirascope/llm/prompts/decorator.py +16 -5
- mirascope/llm/prompts/prompts.py +160 -92
- mirascope/llm/providers/__init__.py +1 -4
- mirascope/llm/providers/anthropic/_utils/__init__.py +2 -0
- mirascope/llm/providers/anthropic/_utils/beta_decode.py +18 -9
- mirascope/llm/providers/anthropic/_utils/beta_encode.py +62 -13
- mirascope/llm/providers/anthropic/_utils/decode.py +18 -9
- mirascope/llm/providers/anthropic/_utils/encode.py +26 -7
- mirascope/llm/providers/anthropic/_utils/errors.py +2 -2
- mirascope/llm/providers/anthropic/beta_provider.py +64 -18
- mirascope/llm/providers/anthropic/provider.py +91 -33
- mirascope/llm/providers/base/__init__.py +0 -4
- mirascope/llm/providers/base/_utils.py +55 -6
- mirascope/llm/providers/base/base_provider.py +116 -37
- mirascope/llm/providers/google/_utils/__init__.py +2 -0
- mirascope/llm/providers/google/_utils/decode.py +20 -7
- mirascope/llm/providers/google/_utils/encode.py +26 -7
- mirascope/llm/providers/google/_utils/errors.py +3 -2
- mirascope/llm/providers/google/provider.py +64 -18
- mirascope/llm/providers/mirascope/_utils.py +13 -17
- mirascope/llm/providers/mirascope/provider.py +49 -18
- mirascope/llm/providers/mlx/_utils.py +7 -2
- mirascope/llm/providers/mlx/encoding/base.py +5 -2
- mirascope/llm/providers/mlx/encoding/transformers.py +5 -2
- mirascope/llm/providers/mlx/mlx.py +23 -6
- mirascope/llm/providers/mlx/provider.py +42 -13
- mirascope/llm/providers/openai/_utils/errors.py +2 -2
- mirascope/llm/providers/openai/completions/_utils/encode.py +20 -16
- mirascope/llm/providers/openai/completions/base_provider.py +40 -11
- mirascope/llm/providers/openai/provider.py +40 -10
- mirascope/llm/providers/openai/responses/_utils/__init__.py +2 -0
- mirascope/llm/providers/openai/responses/_utils/decode.py +19 -6
- mirascope/llm/providers/openai/responses/_utils/encode.py +22 -10
- mirascope/llm/providers/openai/responses/provider.py +56 -18
- mirascope/llm/providers/provider_registry.py +93 -19
- mirascope/llm/responses/__init__.py +6 -1
- mirascope/llm/responses/_utils.py +102 -12
- mirascope/llm/responses/base_response.py +5 -2
- mirascope/llm/responses/base_stream_response.py +115 -25
- mirascope/llm/responses/response.py +2 -1
- mirascope/llm/responses/root_response.py +89 -17
- mirascope/llm/responses/stream_response.py +6 -9
- mirascope/llm/tools/decorator.py +9 -4
- mirascope/llm/tools/tool_schema.py +17 -6
- mirascope/llm/tools/toolkit.py +35 -27
- mirascope/llm/tools/tools.py +45 -20
- mirascope/ops/__init__.py +4 -0
- mirascope/ops/_internal/closure.py +4 -1
- mirascope/ops/_internal/configuration.py +82 -31
- mirascope/ops/_internal/exporters/exporters.py +55 -35
- mirascope/ops/_internal/exporters/utils.py +37 -0
- mirascope/ops/_internal/instrumentation/llm/common.py +530 -0
- mirascope/ops/_internal/instrumentation/llm/cost.py +190 -0
- mirascope/ops/_internal/instrumentation/llm/encode.py +1 -1
- mirascope/ops/_internal/instrumentation/llm/llm.py +116 -1242
- mirascope/ops/_internal/instrumentation/llm/model.py +1798 -0
- mirascope/ops/_internal/instrumentation/llm/response.py +521 -0
- mirascope/ops/_internal/instrumentation/llm/serialize.py +300 -0
- mirascope/ops/_internal/protocols.py +83 -1
- mirascope/ops/_internal/traced_calls.py +18 -0
- mirascope/ops/_internal/traced_functions.py +125 -10
- mirascope/ops/_internal/tracing.py +78 -1
- mirascope/ops/_internal/utils.py +60 -4
- mirascope/ops/_internal/versioned_functions.py +1 -1
- {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/METADATA +12 -11
- mirascope-2.0.2.dist-info/RECORD +424 -0
- {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/licenses/LICENSE +1 -1
- mirascope-2.0.0a6.dist-info/RECORD +0 -316
- {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""Shared utilities and types for OpenTelemetry GenAI instrumentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING, Protocol, TypeAlias, runtime_checkable
|
|
10
|
+
from typing_extensions import TypeIs
|
|
11
|
+
|
|
12
|
+
from opentelemetry import trace as otel_trace
|
|
13
|
+
from opentelemetry.semconv._incubating.attributes import (
|
|
14
|
+
gen_ai_attributes as GenAIAttributes,
|
|
15
|
+
)
|
|
16
|
+
from opentelemetry.semconv.attributes import error_attributes as ErrorAttributes
|
|
17
|
+
from opentelemetry.trace import SpanKind, Status, StatusCode
|
|
18
|
+
|
|
19
|
+
from .....llm import (
|
|
20
|
+
AnyToolFn,
|
|
21
|
+
AnyToolSchema,
|
|
22
|
+
BaseToolkit,
|
|
23
|
+
Format,
|
|
24
|
+
FormattableT,
|
|
25
|
+
Jsonable,
|
|
26
|
+
Message,
|
|
27
|
+
Model,
|
|
28
|
+
ModelId,
|
|
29
|
+
Params,
|
|
30
|
+
ProviderId,
|
|
31
|
+
RootResponse,
|
|
32
|
+
ToolkitT,
|
|
33
|
+
ToolSchema,
|
|
34
|
+
)
|
|
35
|
+
from ...configuration import get_tracer
|
|
36
|
+
from ...utils import json_dumps
|
|
37
|
+
from .cost import calculate_cost_async, calculate_cost_sync
|
|
38
|
+
from .encode import (
|
|
39
|
+
map_finish_reason,
|
|
40
|
+
snapshot_from_root_response,
|
|
41
|
+
split_request_messages,
|
|
42
|
+
)
|
|
43
|
+
from .serialize import (
|
|
44
|
+
serialize_mirascope_content,
|
|
45
|
+
serialize_mirascope_cost,
|
|
46
|
+
serialize_mirascope_messages,
|
|
47
|
+
serialize_mirascope_usage,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
# Re-export for backwards compatibility
|
|
53
|
+
_calculate_cost_sync = calculate_cost_sync
|
|
54
|
+
_calculate_cost_async = calculate_cost_async
|
|
55
|
+
|
|
56
|
+
if TYPE_CHECKING:
|
|
57
|
+
from opentelemetry.trace import Span, Tracer
|
|
58
|
+
from opentelemetry.util.types import AttributeValue
|
|
59
|
+
|
|
60
|
+
from . import gen_ai_types
|
|
61
|
+
else:
|
|
62
|
+
AttributeValue = None
|
|
63
|
+
Span = None
|
|
64
|
+
Tracer = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
ToolsParam: TypeAlias = (
|
|
68
|
+
Sequence[ToolSchema[AnyToolFn]] | BaseToolkit[AnyToolSchema] | None
|
|
69
|
+
)
|
|
70
|
+
FormatParam: TypeAlias = Format[FormattableT] | None
|
|
71
|
+
ParamsDict: TypeAlias = Mapping[str, str | int | float | bool | Sequence[str] | None]
|
|
72
|
+
SpanAttributes: TypeAlias = Mapping[str, AttributeValue]
|
|
73
|
+
AttributeSetter: TypeAlias = Callable[[str, AttributeValue], None]
|
|
74
|
+
ParamsValue = str | int | float | bool | Sequence[str] | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(slots=True)
|
|
78
|
+
class SpanContext:
|
|
79
|
+
"""Container for a GenAI span and its associated dropped parameters."""
|
|
80
|
+
|
|
81
|
+
span: Span | None
|
|
82
|
+
"""The active span, if any."""
|
|
83
|
+
|
|
84
|
+
dropped_params: dict[str, Jsonable]
|
|
85
|
+
"""Parameters that could not be recorded as span attributes."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@runtime_checkable
|
|
89
|
+
class Identifiable(Protocol):
|
|
90
|
+
"""Protocol for objects with an optional ID attribute."""
|
|
91
|
+
|
|
92
|
+
id: str | None
|
|
93
|
+
"""Optional ID attribute."""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_PARAM_ATTRIBUTE_MAP: Mapping[str, str] = {
|
|
97
|
+
"temperature": GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE,
|
|
98
|
+
"max_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
|
|
99
|
+
"max_output_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
|
|
100
|
+
"max_completion_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
|
|
101
|
+
"top_p": GenAIAttributes.GEN_AI_REQUEST_TOP_P,
|
|
102
|
+
"top_k": GenAIAttributes.GEN_AI_REQUEST_TOP_K,
|
|
103
|
+
"frequency_penalty": GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY,
|
|
104
|
+
"presence_penalty": GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY,
|
|
105
|
+
"seed": GenAIAttributes.GEN_AI_REQUEST_SEED,
|
|
106
|
+
"stop_sequences": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES,
|
|
107
|
+
"stop": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES,
|
|
108
|
+
"n": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT,
|
|
109
|
+
"choice_count": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def record_exception(span: Span, exc: Exception) -> None:
|
|
114
|
+
"""Record exception details on span following OpenTelemetry semantic conventions."""
|
|
115
|
+
span.record_exception(exc)
|
|
116
|
+
span.set_attribute(ErrorAttributes.ERROR_TYPE, exc.__class__.__name__)
|
|
117
|
+
error_message = str(exc)
|
|
118
|
+
if error_message:
|
|
119
|
+
span.set_attribute("error.message", error_message)
|
|
120
|
+
span.set_status(Status(StatusCode.ERROR, error_message))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _infer_output_type(format_obj: FormatParam) -> str:
|
|
124
|
+
"""Infer the GenAI output type from the format parameter."""
|
|
125
|
+
if format_obj is None:
|
|
126
|
+
return GenAIAttributes.GenAiOutputTypeValues.TEXT.value
|
|
127
|
+
return GenAIAttributes.GenAiOutputTypeValues.JSON.value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _apply_param_attributes(
|
|
131
|
+
attrs: dict[str, AttributeValue], params: ParamsDict
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Apply model parameters as span attributes."""
|
|
134
|
+
if not params:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
for key, attr_key in _PARAM_ATTRIBUTE_MAP.items():
|
|
138
|
+
if key not in params:
|
|
139
|
+
continue
|
|
140
|
+
value = params[key]
|
|
141
|
+
if value is None:
|
|
142
|
+
continue
|
|
143
|
+
if key in {"stop", "stop_sequences"} and isinstance(value, str):
|
|
144
|
+
value = [value]
|
|
145
|
+
attrs[attr_key] = value
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _set_json_attribute(
|
|
149
|
+
setter: AttributeSetter,
|
|
150
|
+
*,
|
|
151
|
+
key: str,
|
|
152
|
+
payload: (
|
|
153
|
+
gen_ai_types.SystemInstructions
|
|
154
|
+
| gen_ai_types.InputMessages
|
|
155
|
+
| gen_ai_types.OutputMessages
|
|
156
|
+
),
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Assign a JSON-serialized attribute to a span."""
|
|
159
|
+
if not payload:
|
|
160
|
+
return
|
|
161
|
+
setter(key, json_dumps(payload))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _assign_request_message_attributes(
|
|
165
|
+
setter: AttributeSetter,
|
|
166
|
+
*,
|
|
167
|
+
messages: Sequence[Message],
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Assign request message attributes to a span."""
|
|
170
|
+
system_payload, input_payload = split_request_messages(messages)
|
|
171
|
+
_set_json_attribute(
|
|
172
|
+
setter,
|
|
173
|
+
key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
|
|
174
|
+
payload=system_payload,
|
|
175
|
+
)
|
|
176
|
+
_set_json_attribute(
|
|
177
|
+
setter,
|
|
178
|
+
key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
|
|
179
|
+
payload=input_payload,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _collect_tool_schemas(
|
|
184
|
+
tools: Sequence[ToolSchema[AnyToolFn]] | BaseToolkit[AnyToolSchema],
|
|
185
|
+
) -> list[ToolSchema[AnyToolFn]]:
|
|
186
|
+
"""Collect ToolSchema instances from a tools parameter."""
|
|
187
|
+
iterable = list(tools.tools) if isinstance(tools, BaseToolkit) else list(tools)
|
|
188
|
+
schemas: list[ToolSchema[AnyToolFn]] = []
|
|
189
|
+
for tool in iterable:
|
|
190
|
+
if isinstance(tool, ToolSchema):
|
|
191
|
+
schemas.append(tool)
|
|
192
|
+
return schemas
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _serialize_tool_definitions(
|
|
196
|
+
tools: ToolsParam,
|
|
197
|
+
format: FormatParam = None,
|
|
198
|
+
) -> str | None:
|
|
199
|
+
"""Serialize tool definitions to JSON for span attributes."""
|
|
200
|
+
if tools is None:
|
|
201
|
+
tool_schemas: list[ToolSchema[AnyToolFn]] = []
|
|
202
|
+
else:
|
|
203
|
+
tool_schemas = _collect_tool_schemas(tools)
|
|
204
|
+
|
|
205
|
+
if isinstance(format, Format) and format.mode == "tool":
|
|
206
|
+
tool_schemas.append(format.create_tool_schema())
|
|
207
|
+
|
|
208
|
+
if not tool_schemas:
|
|
209
|
+
return None
|
|
210
|
+
definitions: list[dict[str, str | int | bool | dict[str, str | int | bool]]] = []
|
|
211
|
+
for tool in tool_schemas:
|
|
212
|
+
tool_def: dict[str, str | int | bool | dict[str, str | int | bool]] = {
|
|
213
|
+
"name": tool.name,
|
|
214
|
+
"description": tool.description,
|
|
215
|
+
"parameters": tool.parameters.model_dump(by_alias=True, mode="json"),
|
|
216
|
+
}
|
|
217
|
+
if tool.strict is not None:
|
|
218
|
+
tool_def["strict"] = tool.strict
|
|
219
|
+
definitions.append(tool_def)
|
|
220
|
+
return json_dumps(definitions)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _build_request_attributes(
|
|
224
|
+
*,
|
|
225
|
+
operation: str,
|
|
226
|
+
provider: ProviderId,
|
|
227
|
+
model_id: ModelId,
|
|
228
|
+
messages: Sequence[Message],
|
|
229
|
+
tools: ToolsParam,
|
|
230
|
+
format: FormatParam,
|
|
231
|
+
params: ParamsDict,
|
|
232
|
+
) -> dict[str, AttributeValue]:
|
|
233
|
+
"""Build GenAI request attributes for a span."""
|
|
234
|
+
attrs: dict[str, AttributeValue] = {
|
|
235
|
+
GenAIAttributes.GEN_AI_OPERATION_NAME: operation,
|
|
236
|
+
GenAIAttributes.GEN_AI_PROVIDER_NAME: provider,
|
|
237
|
+
GenAIAttributes.GEN_AI_REQUEST_MODEL: model_id,
|
|
238
|
+
GenAIAttributes.GEN_AI_OUTPUT_TYPE: _infer_output_type(format),
|
|
239
|
+
}
|
|
240
|
+
_apply_param_attributes(attrs, params)
|
|
241
|
+
|
|
242
|
+
_assign_request_message_attributes(
|
|
243
|
+
attrs.__setitem__,
|
|
244
|
+
messages=messages,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
tool_payload = _serialize_tool_definitions(tools, format=format)
|
|
248
|
+
if tool_payload:
|
|
249
|
+
# The incubating semconv module does not yet expose a constant for this key.
|
|
250
|
+
attrs["gen_ai.tool.definitions"] = tool_payload
|
|
251
|
+
|
|
252
|
+
return attrs
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _extract_response_id(
|
|
256
|
+
raw: dict[str, str | int] | str | Identifiable | None,
|
|
257
|
+
) -> str | None:
|
|
258
|
+
"""Extract response ID from raw response data."""
|
|
259
|
+
if isinstance(raw, dict):
|
|
260
|
+
for key in ("id", "response_id", "responseId"):
|
|
261
|
+
value = raw.get(key)
|
|
262
|
+
if isinstance(value, str):
|
|
263
|
+
return value
|
|
264
|
+
elif isinstance(raw, Identifiable):
|
|
265
|
+
return raw.id
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def attach_response(
|
|
270
|
+
span: Span,
|
|
271
|
+
response: RootResponse[ToolkitT, FormattableT | None],
|
|
272
|
+
*,
|
|
273
|
+
request_messages: Sequence[Message],
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Attach response attributes to a GenAI span."""
|
|
276
|
+
span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.model_id)
|
|
277
|
+
span.set_attribute(
|
|
278
|
+
GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS,
|
|
279
|
+
[map_finish_reason(response.finish_reason)],
|
|
280
|
+
)
|
|
281
|
+
response_id = _extract_response_id(getattr(response, "raw", None))
|
|
282
|
+
if response_id:
|
|
283
|
+
span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_ID, response_id)
|
|
284
|
+
|
|
285
|
+
snapshot = snapshot_from_root_response(
|
|
286
|
+
response,
|
|
287
|
+
request_messages=request_messages,
|
|
288
|
+
)
|
|
289
|
+
_set_json_attribute(
|
|
290
|
+
span.set_attribute,
|
|
291
|
+
key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
|
|
292
|
+
payload=snapshot.system_instructions,
|
|
293
|
+
)
|
|
294
|
+
_set_json_attribute(
|
|
295
|
+
span.set_attribute,
|
|
296
|
+
key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
|
|
297
|
+
payload=snapshot.inputs,
|
|
298
|
+
)
|
|
299
|
+
_set_json_attribute(
|
|
300
|
+
span.set_attribute,
|
|
301
|
+
key=GenAIAttributes.GEN_AI_OUTPUT_MESSAGES,
|
|
302
|
+
payload=snapshot.outputs,
|
|
303
|
+
)
|
|
304
|
+
if response.usage is not None:
|
|
305
|
+
span.set_attribute(
|
|
306
|
+
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, response.usage.input_tokens
|
|
307
|
+
)
|
|
308
|
+
span.set_attribute(
|
|
309
|
+
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, response.usage.output_tokens
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Mirascope-specific attributes
|
|
313
|
+
span.set_attribute(
|
|
314
|
+
"mirascope.response.messages", serialize_mirascope_messages(request_messages)
|
|
315
|
+
)
|
|
316
|
+
span.set_attribute(
|
|
317
|
+
"mirascope.response.content", serialize_mirascope_content(response.content)
|
|
318
|
+
)
|
|
319
|
+
if (usage_json := serialize_mirascope_usage(response.usage)) is not None:
|
|
320
|
+
span.set_attribute("mirascope.response.usage", usage_json)
|
|
321
|
+
|
|
322
|
+
# Calculate and attach cost if usage is available
|
|
323
|
+
if response.usage is not None:
|
|
324
|
+
cost = _calculate_cost_sync(
|
|
325
|
+
response.provider_id, response.model_id, response.usage
|
|
326
|
+
)
|
|
327
|
+
if cost is not None:
|
|
328
|
+
span.set_attribute(
|
|
329
|
+
"mirascope.response.cost",
|
|
330
|
+
serialize_mirascope_cost(
|
|
331
|
+
input_cost=cost.input_cost_centicents,
|
|
332
|
+
output_cost=cost.output_cost_centicents,
|
|
333
|
+
total_cost=cost.total_cost_centicents,
|
|
334
|
+
cache_read_cost=cost.cache_read_cost_centicents,
|
|
335
|
+
cache_write_cost=cost.cache_write_cost_centicents,
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
async def attach_response_async(
|
|
341
|
+
span: Span,
|
|
342
|
+
response: RootResponse[ToolkitT, FormattableT | None],
|
|
343
|
+
*,
|
|
344
|
+
request_messages: Sequence[Message],
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Attach response attributes to a GenAI span (async version for cost calculation)."""
|
|
347
|
+
span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.model_id)
|
|
348
|
+
span.set_attribute(
|
|
349
|
+
GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS,
|
|
350
|
+
[map_finish_reason(response.finish_reason)],
|
|
351
|
+
)
|
|
352
|
+
response_id = _extract_response_id(getattr(response, "raw", None))
|
|
353
|
+
if response_id:
|
|
354
|
+
span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_ID, response_id)
|
|
355
|
+
|
|
356
|
+
snapshot = snapshot_from_root_response(
|
|
357
|
+
response,
|
|
358
|
+
request_messages=request_messages,
|
|
359
|
+
)
|
|
360
|
+
_set_json_attribute(
|
|
361
|
+
span.set_attribute,
|
|
362
|
+
key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
|
|
363
|
+
payload=snapshot.system_instructions,
|
|
364
|
+
)
|
|
365
|
+
_set_json_attribute(
|
|
366
|
+
span.set_attribute,
|
|
367
|
+
key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
|
|
368
|
+
payload=snapshot.inputs,
|
|
369
|
+
)
|
|
370
|
+
_set_json_attribute(
|
|
371
|
+
span.set_attribute,
|
|
372
|
+
key=GenAIAttributes.GEN_AI_OUTPUT_MESSAGES,
|
|
373
|
+
payload=snapshot.outputs,
|
|
374
|
+
)
|
|
375
|
+
if response.usage is not None:
|
|
376
|
+
span.set_attribute(
|
|
377
|
+
GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, response.usage.input_tokens
|
|
378
|
+
)
|
|
379
|
+
span.set_attribute(
|
|
380
|
+
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, response.usage.output_tokens
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Mirascope-specific attributes
|
|
384
|
+
span.set_attribute(
|
|
385
|
+
"mirascope.response.messages", serialize_mirascope_messages(request_messages)
|
|
386
|
+
)
|
|
387
|
+
span.set_attribute(
|
|
388
|
+
"mirascope.response.content", serialize_mirascope_content(response.content)
|
|
389
|
+
)
|
|
390
|
+
if (usage_json := serialize_mirascope_usage(response.usage)) is not None:
|
|
391
|
+
span.set_attribute("mirascope.response.usage", usage_json)
|
|
392
|
+
|
|
393
|
+
# Calculate and attach cost if usage is available (async)
|
|
394
|
+
if response.usage is not None:
|
|
395
|
+
cost = await _calculate_cost_async(
|
|
396
|
+
response.provider_id, response.model_id, response.usage
|
|
397
|
+
)
|
|
398
|
+
if cost is not None:
|
|
399
|
+
span.set_attribute(
|
|
400
|
+
"mirascope.response.cost",
|
|
401
|
+
serialize_mirascope_cost(
|
|
402
|
+
input_cost=cost.input_cost_centicents,
|
|
403
|
+
output_cost=cost.output_cost_centicents,
|
|
404
|
+
total_cost=cost.total_cost_centicents,
|
|
405
|
+
cache_read_cost=cost.cache_read_cost_centicents,
|
|
406
|
+
cache_write_cost=cost.cache_write_cost_centicents,
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _is_supported_param_value(value: object) -> TypeIs[ParamsValue]:
|
|
412
|
+
"""Returns True if the value can be exported as an OTEL attribute."""
|
|
413
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
414
|
+
return True
|
|
415
|
+
if isinstance(value, Sequence) and not isinstance(value, str | bytes):
|
|
416
|
+
return all(isinstance(item, str) for item in value)
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _normalize_dropped_value(value: object) -> Jsonable:
|
|
421
|
+
"""Returns a JSON-safe representation for unsupported param values."""
|
|
422
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
423
|
+
return value
|
|
424
|
+
if isinstance(value, Mapping):
|
|
425
|
+
normalized: dict[str, Jsonable] = {}
|
|
426
|
+
for key, item in value.items():
|
|
427
|
+
normalized[str(key)] = _normalize_dropped_value(item)
|
|
428
|
+
return normalized
|
|
429
|
+
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
|
|
430
|
+
return [_normalize_dropped_value(item) for item in value]
|
|
431
|
+
try:
|
|
432
|
+
return str(value)
|
|
433
|
+
except Exception: # pragma: no cover
|
|
434
|
+
return f"<{type(value).__name__}>"
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _params_as_mapping(params: Params) -> tuple[ParamsDict, dict[str, Jsonable]]:
|
|
438
|
+
"""Returns supported params and a mapping of dropped params."""
|
|
439
|
+
filtered: dict[str, ParamsValue] = {}
|
|
440
|
+
dropped: dict[str, Jsonable] = {}
|
|
441
|
+
for key, value in params.items():
|
|
442
|
+
if _is_supported_param_value(value):
|
|
443
|
+
filtered[key] = value
|
|
444
|
+
else:
|
|
445
|
+
dropped[key] = _normalize_dropped_value(value)
|
|
446
|
+
return filtered, dropped
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def record_dropped_params(
|
|
450
|
+
span: Span,
|
|
451
|
+
dropped_params: Mapping[str, Jsonable],
|
|
452
|
+
) -> None:
|
|
453
|
+
"""Emit an event with JSON-encoded params that cannot become attributes.
|
|
454
|
+
|
|
455
|
+
See https://opentelemetry.io/docs/specs/otel/common/ for the attribute type limits,
|
|
456
|
+
https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/ for the GenAI
|
|
457
|
+
guidance on recording richer payloads via events, and
|
|
458
|
+
https://opentelemetry.io/blog/2025/complex-attribute-types/ for the recommendation
|
|
459
|
+
to serialize unsupported complex types instead of dropping them outright.
|
|
460
|
+
"""
|
|
461
|
+
if not dropped_params:
|
|
462
|
+
return None
|
|
463
|
+
payload = json_dumps(dropped_params)
|
|
464
|
+
span.add_event(
|
|
465
|
+
"gen_ai.request.params.untracked",
|
|
466
|
+
attributes={
|
|
467
|
+
"gen_ai.untracked_params.count": len(dropped_params),
|
|
468
|
+
"gen_ai.untracked_params.keys": list(dropped_params.keys()),
|
|
469
|
+
"gen_ai.untracked_params.json": payload,
|
|
470
|
+
},
|
|
471
|
+
)
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@contextmanager
|
|
476
|
+
def start_model_span(
|
|
477
|
+
model: Model,
|
|
478
|
+
*,
|
|
479
|
+
messages: Sequence[Message],
|
|
480
|
+
tools: ToolsParam,
|
|
481
|
+
format: FormatParam,
|
|
482
|
+
activate: bool = True,
|
|
483
|
+
) -> Iterator[SpanContext]:
|
|
484
|
+
"""Context manager that yields a SpanContext for a model call."""
|
|
485
|
+
params, dropped_params = _params_as_mapping(model.params)
|
|
486
|
+
tracer = get_tracer()
|
|
487
|
+
|
|
488
|
+
if tracer is None or otel_trace is None:
|
|
489
|
+
yield SpanContext(None, dropped_params)
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
operation = GenAIAttributes.GenAiOperationNameValues.CHAT.value
|
|
493
|
+
attrs = _build_request_attributes(
|
|
494
|
+
operation=operation,
|
|
495
|
+
provider=model.provider_id,
|
|
496
|
+
model_id=model.model_id,
|
|
497
|
+
messages=messages,
|
|
498
|
+
tools=tools,
|
|
499
|
+
format=format,
|
|
500
|
+
params=params,
|
|
501
|
+
)
|
|
502
|
+
span_name = f"{operation} {model.model_id}"
|
|
503
|
+
|
|
504
|
+
if activate:
|
|
505
|
+
with tracer.start_as_current_span(
|
|
506
|
+
name=span_name,
|
|
507
|
+
kind=SpanKind.CLIENT,
|
|
508
|
+
) as active_span:
|
|
509
|
+
for key, value in attrs.items():
|
|
510
|
+
active_span.set_attribute(key, value)
|
|
511
|
+
try:
|
|
512
|
+
yield SpanContext(active_span, dropped_params)
|
|
513
|
+
except Exception as exc:
|
|
514
|
+
record_exception(active_span, exc)
|
|
515
|
+
raise
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
span = tracer.start_span(
|
|
519
|
+
name=span_name,
|
|
520
|
+
kind=SpanKind.CLIENT,
|
|
521
|
+
)
|
|
522
|
+
for key, value in attrs.items():
|
|
523
|
+
span.set_attribute(key, value)
|
|
524
|
+
try:
|
|
525
|
+
yield SpanContext(span, dropped_params)
|
|
526
|
+
except Exception as exc:
|
|
527
|
+
record_exception(span, exc)
|
|
528
|
+
raise
|
|
529
|
+
finally:
|
|
530
|
+
span.end()
|