mirascope 2.0.0a2__py3-none-any.whl → 2.0.0a4__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/__init__.py +2 -2
- mirascope/api/__init__.py +6 -0
- mirascope/api/_generated/README.md +207 -0
- mirascope/api/_generated/__init__.py +141 -0
- mirascope/api/_generated/client.py +163 -0
- mirascope/api/_generated/core/__init__.py +52 -0
- mirascope/api/_generated/core/api_error.py +23 -0
- mirascope/api/_generated/core/client_wrapper.py +58 -0
- mirascope/api/_generated/core/datetime_utils.py +30 -0
- mirascope/api/_generated/core/file.py +70 -0
- mirascope/api/_generated/core/force_multipart.py +16 -0
- mirascope/api/_generated/core/http_client.py +619 -0
- mirascope/api/_generated/core/http_response.py +55 -0
- mirascope/api/_generated/core/jsonable_encoder.py +102 -0
- mirascope/api/_generated/core/pydantic_utilities.py +310 -0
- mirascope/api/_generated/core/query_encoder.py +60 -0
- mirascope/api/_generated/core/remove_none_from_dict.py +11 -0
- mirascope/api/_generated/core/request_options.py +35 -0
- mirascope/api/_generated/core/serialization.py +282 -0
- mirascope/api/_generated/docs/__init__.py +4 -0
- mirascope/api/_generated/docs/client.py +95 -0
- mirascope/api/_generated/docs/raw_client.py +132 -0
- mirascope/api/_generated/environment.py +9 -0
- mirascope/api/_generated/errors/__init__.py +17 -0
- mirascope/api/_generated/errors/bad_request_error.py +15 -0
- 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/health/__init__.py +7 -0
- mirascope/api/_generated/health/client.py +96 -0
- mirascope/api/_generated/health/raw_client.py +129 -0
- mirascope/api/_generated/health/types/__init__.py +8 -0
- mirascope/api/_generated/health/types/health_check_response.py +24 -0
- mirascope/api/_generated/health/types/health_check_response_status.py +5 -0
- mirascope/api/_generated/organizations/__init__.py +25 -0
- mirascope/api/_generated/organizations/client.py +380 -0
- mirascope/api/_generated/organizations/raw_client.py +876 -0
- mirascope/api/_generated/organizations/types/__init__.py +23 -0
- mirascope/api/_generated/organizations/types/organizations_create_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_create_response_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_get_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_get_response_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_list_response_item.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_update_response.py +24 -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 +458 -0
- mirascope/api/_generated/projects/raw_client.py +1016 -0
- mirascope/api/_generated/projects/types/__init__.py +15 -0
- mirascope/api/_generated/projects/types/projects_create_response.py +30 -0
- mirascope/api/_generated/projects/types/projects_get_response.py +30 -0
- mirascope/api/_generated/projects/types/projects_list_response_item.py +30 -0
- mirascope/api/_generated/projects/types/projects_update_response.py +30 -0
- mirascope/api/_generated/reference.md +753 -0
- mirascope/api/_generated/traces/__init__.py +55 -0
- mirascope/api/_generated/traces/client.py +162 -0
- mirascope/api/_generated/traces/raw_client.py +168 -0
- mirascope/api/_generated/traces/types/__init__.py +95 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item.py +36 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource.py +31 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item.py +25 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value.py +54 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_array_value.py +23 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value.py +28 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value_values_item.py +24 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item.py +35 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope.py +35 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item.py +27 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value.py +54 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_array_value.py +23 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value.py +28 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value_values_item.py +24 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item.py +60 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item.py +29 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value.py +54 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_array_value.py +23 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value.py +28 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value_values_item.py +24 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_status.py +24 -0
- mirascope/api/_generated/traces/types/traces_create_response.py +27 -0
- mirascope/api/_generated/traces/types/traces_create_response_partial_success.py +28 -0
- mirascope/api/_generated/types/__init__.py +37 -0
- 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 +29 -0
- mirascope/api/_generated/types/http_api_decode_error_tag.py +5 -0
- mirascope/api/_generated/types/issue.py +40 -0
- mirascope/api/_generated/types/issue_tag.py +17 -0
- 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 +7 -0
- mirascope/api/_generated/types/property_key_key.py +27 -0
- mirascope/api/_generated/types/property_key_key_tag.py +5 -0
- mirascope/api/client.py +255 -0
- mirascope/api/settings.py +81 -0
- mirascope/llm/__init__.py +45 -11
- mirascope/llm/calls/calls.py +81 -57
- mirascope/llm/calls/decorator.py +121 -115
- mirascope/llm/content/__init__.py +3 -2
- mirascope/llm/context/_utils.py +19 -6
- mirascope/llm/exceptions.py +30 -16
- mirascope/llm/formatting/_utils.py +9 -5
- mirascope/llm/formatting/format.py +2 -2
- mirascope/llm/formatting/from_call_args.py +2 -2
- mirascope/llm/messages/message.py +13 -5
- mirascope/llm/models/__init__.py +2 -2
- mirascope/llm/models/models.py +189 -81
- mirascope/llm/prompts/__init__.py +13 -12
- mirascope/llm/prompts/_utils.py +27 -24
- mirascope/llm/prompts/decorator.py +133 -204
- mirascope/llm/prompts/prompts.py +424 -0
- mirascope/llm/prompts/protocols.py +25 -59
- mirascope/llm/providers/__init__.py +44 -0
- mirascope/llm/{clients → providers}/_missing_import_stubs.py +8 -6
- mirascope/llm/providers/anthropic/__init__.py +29 -0
- mirascope/llm/providers/anthropic/_utils/__init__.py +23 -0
- mirascope/llm/providers/anthropic/_utils/beta_decode.py +271 -0
- mirascope/llm/providers/anthropic/_utils/beta_encode.py +216 -0
- mirascope/llm/{clients → providers}/anthropic/_utils/decode.py +44 -11
- mirascope/llm/providers/anthropic/_utils/encode.py +356 -0
- mirascope/llm/providers/anthropic/beta_provider.py +322 -0
- mirascope/llm/providers/anthropic/model_id.py +23 -0
- mirascope/llm/providers/anthropic/model_info.py +87 -0
- mirascope/llm/providers/anthropic/provider.py +416 -0
- mirascope/llm/{clients → providers}/base/__init__.py +3 -3
- mirascope/llm/{clients → providers}/base/_utils.py +25 -8
- mirascope/llm/{clients/base/client.py → providers/base/base_provider.py} +255 -126
- mirascope/llm/providers/google/__init__.py +21 -0
- mirascope/llm/{clients → providers}/google/_utils/decode.py +61 -7
- mirascope/llm/{clients → providers}/google/_utils/encode.py +44 -30
- mirascope/llm/providers/google/model_id.py +22 -0
- mirascope/llm/providers/google/model_info.py +62 -0
- mirascope/llm/providers/google/provider.py +442 -0
- mirascope/llm/providers/load_provider.py +54 -0
- mirascope/llm/providers/mlx/__init__.py +24 -0
- mirascope/llm/providers/mlx/_utils.py +129 -0
- mirascope/llm/providers/mlx/encoding/__init__.py +8 -0
- mirascope/llm/providers/mlx/encoding/base.py +69 -0
- mirascope/llm/providers/mlx/encoding/transformers.py +147 -0
- mirascope/llm/providers/mlx/mlx.py +237 -0
- mirascope/llm/providers/mlx/model_id.py +17 -0
- mirascope/llm/providers/mlx/provider.py +415 -0
- mirascope/llm/providers/model_id.py +16 -0
- mirascope/llm/providers/ollama/__init__.py +19 -0
- mirascope/llm/providers/ollama/provider.py +71 -0
- mirascope/llm/providers/openai/__init__.py +6 -0
- mirascope/llm/providers/openai/completions/__init__.py +25 -0
- mirascope/llm/{clients → providers}/openai/completions/_utils/__init__.py +2 -0
- mirascope/llm/{clients → providers}/openai/completions/_utils/decode.py +60 -6
- mirascope/llm/{clients → providers}/openai/completions/_utils/encode.py +37 -26
- mirascope/llm/providers/openai/completions/base_provider.py +513 -0
- mirascope/llm/providers/openai/completions/provider.py +22 -0
- mirascope/llm/providers/openai/model_id.py +31 -0
- mirascope/llm/providers/openai/model_info.py +303 -0
- mirascope/llm/providers/openai/provider.py +398 -0
- mirascope/llm/providers/openai/responses/__init__.py +21 -0
- mirascope/llm/{clients → providers}/openai/responses/_utils/decode.py +59 -6
- mirascope/llm/{clients → providers}/openai/responses/_utils/encode.py +34 -23
- mirascope/llm/providers/openai/responses/provider.py +469 -0
- mirascope/llm/providers/provider_id.py +23 -0
- mirascope/llm/providers/provider_registry.py +169 -0
- 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 +14 -5
- mirascope/llm/responses/base_stream_response.py +35 -6
- mirascope/llm/responses/finish_reason.py +1 -0
- mirascope/llm/responses/response.py +33 -13
- mirascope/llm/responses/root_response.py +12 -13
- mirascope/llm/responses/stream_response.py +35 -23
- mirascope/llm/responses/usage.py +95 -0
- mirascope/llm/tools/__init__.py +9 -2
- mirascope/llm/tools/_utils.py +12 -3
- mirascope/llm/tools/protocols.py +4 -4
- mirascope/llm/tools/tool_schema.py +44 -9
- mirascope/llm/tools/tools.py +10 -9
- mirascope/ops/__init__.py +156 -0
- mirascope/ops/_internal/__init__.py +5 -0
- mirascope/ops/_internal/closure.py +1118 -0
- mirascope/ops/_internal/configuration.py +126 -0
- mirascope/ops/_internal/context.py +76 -0
- mirascope/ops/_internal/exporters/__init__.py +26 -0
- mirascope/ops/_internal/exporters/exporters.py +342 -0
- mirascope/ops/_internal/exporters/processors.py +104 -0
- mirascope/ops/_internal/exporters/types.py +165 -0
- mirascope/ops/_internal/exporters/utils.py +29 -0
- mirascope/ops/_internal/instrumentation/__init__.py +8 -0
- mirascope/ops/_internal/instrumentation/llm/__init__.py +8 -0
- mirascope/ops/_internal/instrumentation/llm/encode.py +238 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/__init__.py +38 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_input_messages.py +31 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_output_messages.py +38 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_system_instructions.py +18 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/shared.py +100 -0
- mirascope/ops/_internal/instrumentation/llm/llm.py +1288 -0
- mirascope/ops/_internal/propagation.py +198 -0
- mirascope/ops/_internal/protocols.py +51 -0
- mirascope/ops/_internal/session.py +139 -0
- mirascope/ops/_internal/spans.py +232 -0
- mirascope/ops/_internal/traced_calls.py +371 -0
- mirascope/ops/_internal/traced_functions.py +394 -0
- mirascope/ops/_internal/tracing.py +276 -0
- mirascope/ops/_internal/types.py +13 -0
- mirascope/ops/_internal/utils.py +75 -0
- mirascope/ops/_internal/versioned_calls.py +512 -0
- mirascope/ops/_internal/versioned_functions.py +346 -0
- mirascope/ops/_internal/versioning.py +303 -0
- mirascope/ops/exceptions.py +21 -0
- {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a4.dist-info}/METADATA +78 -3
- mirascope-2.0.0a4.dist-info/RECORD +247 -0
- {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a4.dist-info}/WHEEL +1 -1
- mirascope/graphs/__init__.py +0 -22
- mirascope/graphs/finite_state_machine.py +0 -625
- mirascope/llm/agents/__init__.py +0 -15
- mirascope/llm/agents/agent.py +0 -97
- mirascope/llm/agents/agent_template.py +0 -45
- mirascope/llm/agents/decorator.py +0 -176
- mirascope/llm/calls/base_call.py +0 -33
- mirascope/llm/clients/__init__.py +0 -34
- mirascope/llm/clients/anthropic/__init__.py +0 -25
- mirascope/llm/clients/anthropic/_utils/encode.py +0 -243
- mirascope/llm/clients/anthropic/clients.py +0 -819
- mirascope/llm/clients/anthropic/model_ids.py +0 -8
- mirascope/llm/clients/google/__init__.py +0 -20
- mirascope/llm/clients/google/clients.py +0 -853
- mirascope/llm/clients/google/model_ids.py +0 -15
- mirascope/llm/clients/openai/__init__.py +0 -25
- mirascope/llm/clients/openai/completions/__init__.py +0 -28
- mirascope/llm/clients/openai/completions/_utils/model_features.py +0 -81
- mirascope/llm/clients/openai/completions/clients.py +0 -833
- mirascope/llm/clients/openai/completions/model_ids.py +0 -8
- mirascope/llm/clients/openai/responses/__init__.py +0 -26
- mirascope/llm/clients/openai/responses/_utils/__init__.py +0 -13
- mirascope/llm/clients/openai/responses/_utils/model_features.py +0 -87
- mirascope/llm/clients/openai/responses/clients.py +0 -832
- mirascope/llm/clients/openai/responses/model_ids.py +0 -8
- mirascope/llm/clients/openai/shared/__init__.py +0 -7
- mirascope/llm/clients/openai/shared/_utils.py +0 -55
- mirascope/llm/clients/providers.py +0 -175
- mirascope-2.0.0a2.dist-info/RECORD +0 -102
- /mirascope/llm/{clients → providers}/base/kwargs.py +0 -0
- /mirascope/llm/{clients → providers}/base/params.py +0 -0
- /mirascope/llm/{clients/anthropic → providers/google}/_utils/__init__.py +0 -0
- /mirascope/llm/{clients → providers}/google/message.py +0 -0
- /mirascope/llm/{clients/google → providers/openai/responses}/_utils/__init__.py +0 -0
- {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1288 @@
|
|
|
1
|
+
"""OpenTelemetry GenAI instrumentation for `mirascope.llm`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import weakref
|
|
7
|
+
from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence
|
|
8
|
+
from contextlib import AbstractContextManager, contextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from types import TracebackType
|
|
12
|
+
from typing import TYPE_CHECKING, Protocol, TypeAlias, overload, runtime_checkable
|
|
13
|
+
from typing_extensions import TypeIs
|
|
14
|
+
|
|
15
|
+
from opentelemetry.semconv._incubating.attributes import (
|
|
16
|
+
gen_ai_attributes as GenAIAttributes,
|
|
17
|
+
)
|
|
18
|
+
from opentelemetry.semconv.attributes import (
|
|
19
|
+
error_attributes as ErrorAttributes,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .....llm.context import Context, DepsT
|
|
23
|
+
from .....llm.formatting import Format, FormattableT
|
|
24
|
+
from .....llm.formatting._utils import create_tool_schema
|
|
25
|
+
from .....llm.messages import Message
|
|
26
|
+
from .....llm.models import Model
|
|
27
|
+
from .....llm.providers import Params, ProviderId
|
|
28
|
+
from .....llm.providers.model_id import ModelId
|
|
29
|
+
from .....llm.responses import (
|
|
30
|
+
AsyncContextResponse,
|
|
31
|
+
AsyncContextStreamResponse,
|
|
32
|
+
AsyncResponse,
|
|
33
|
+
ContextResponse,
|
|
34
|
+
ContextStreamResponse,
|
|
35
|
+
Response,
|
|
36
|
+
StreamResponse,
|
|
37
|
+
StreamResponseChunk,
|
|
38
|
+
)
|
|
39
|
+
from .....llm.responses.root_response import RootResponse
|
|
40
|
+
from .....llm.tools import (
|
|
41
|
+
AnyToolFn,
|
|
42
|
+
AnyToolSchema,
|
|
43
|
+
AsyncContextTool,
|
|
44
|
+
AsyncContextToolkit,
|
|
45
|
+
AsyncTool,
|
|
46
|
+
AsyncToolkit,
|
|
47
|
+
ContextTool,
|
|
48
|
+
ContextToolkit,
|
|
49
|
+
Tool,
|
|
50
|
+
Toolkit,
|
|
51
|
+
)
|
|
52
|
+
from .....llm.tools.tool_schema import ToolSchema
|
|
53
|
+
from .....llm.tools.toolkit import BaseToolkit, ToolkitT
|
|
54
|
+
from .....llm.types import Jsonable
|
|
55
|
+
from ...configuration import (
|
|
56
|
+
get_tracer,
|
|
57
|
+
)
|
|
58
|
+
from ...utils import json_dumps
|
|
59
|
+
from .encode import (
|
|
60
|
+
map_finish_reason,
|
|
61
|
+
snapshot_from_root_response,
|
|
62
|
+
split_request_messages,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# TODO: refactor alongside all other import error handling to provide nice error messages
|
|
66
|
+
try:
|
|
67
|
+
from opentelemetry import trace as otel_trace
|
|
68
|
+
from opentelemetry.trace import SpanKind, Status, StatusCode
|
|
69
|
+
except ImportError: # pragma: no cover
|
|
70
|
+
if not TYPE_CHECKING:
|
|
71
|
+
otel_trace = None
|
|
72
|
+
SpanKind = None
|
|
73
|
+
StatusCode = None
|
|
74
|
+
Status = None
|
|
75
|
+
|
|
76
|
+
if TYPE_CHECKING:
|
|
77
|
+
from opentelemetry import trace as otel_trace
|
|
78
|
+
from opentelemetry.trace import (
|
|
79
|
+
Span,
|
|
80
|
+
SpanKind,
|
|
81
|
+
Status,
|
|
82
|
+
StatusCode,
|
|
83
|
+
Tracer,
|
|
84
|
+
)
|
|
85
|
+
from opentelemetry.util.types import AttributeValue
|
|
86
|
+
|
|
87
|
+
from . import gen_ai_types
|
|
88
|
+
else:
|
|
89
|
+
AttributeValue = None
|
|
90
|
+
Span = None
|
|
91
|
+
Tracer = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
ToolsParam: TypeAlias = (
|
|
95
|
+
Sequence[ToolSchema[AnyToolFn]] | BaseToolkit[AnyToolSchema] | None
|
|
96
|
+
)
|
|
97
|
+
FormatParam: TypeAlias = Format[FormattableT] | None
|
|
98
|
+
ParamsDict: TypeAlias = Mapping[str, str | int | float | bool | Sequence[str] | None]
|
|
99
|
+
SpanAttributes: TypeAlias = Mapping[str, AttributeValue]
|
|
100
|
+
AttributeSetter: TypeAlias = Callable[[str, AttributeValue], None]
|
|
101
|
+
ParamsValue = str | int | float | bool | Sequence[str] | None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class SpanContext:
|
|
106
|
+
"""Container for a GenAI span and its associated dropped parameters."""
|
|
107
|
+
|
|
108
|
+
span: Span | None
|
|
109
|
+
"""The active span, if any."""
|
|
110
|
+
|
|
111
|
+
dropped_params: dict[str, Jsonable]
|
|
112
|
+
"""Parameters that could not be recorded as span attributes."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@runtime_checkable
|
|
116
|
+
class Identifiable(Protocol):
|
|
117
|
+
"""Protocol for objects with an optional ID attribute."""
|
|
118
|
+
|
|
119
|
+
id: str | None
|
|
120
|
+
"""Optional ID attribute."""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
_PARAM_ATTRIBUTE_MAP: Mapping[str, str] = {
|
|
124
|
+
"temperature": GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE,
|
|
125
|
+
"max_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
|
|
126
|
+
"max_output_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
|
|
127
|
+
"max_completion_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
|
|
128
|
+
"top_p": GenAIAttributes.GEN_AI_REQUEST_TOP_P,
|
|
129
|
+
"top_k": GenAIAttributes.GEN_AI_REQUEST_TOP_K,
|
|
130
|
+
"frequency_penalty": GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY,
|
|
131
|
+
"presence_penalty": GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY,
|
|
132
|
+
"seed": GenAIAttributes.GEN_AI_REQUEST_SEED,
|
|
133
|
+
"stop_sequences": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES,
|
|
134
|
+
"stop": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES,
|
|
135
|
+
"n": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT,
|
|
136
|
+
"choice_count": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _record_exception(span: Span, exc: Exception) -> None:
|
|
141
|
+
"""Record exception details on span following OpenTelemetry semantic conventions."""
|
|
142
|
+
span.record_exception(exc)
|
|
143
|
+
span.set_attribute(ErrorAttributes.ERROR_TYPE, exc.__class__.__name__)
|
|
144
|
+
error_message = str(exc)
|
|
145
|
+
if error_message:
|
|
146
|
+
span.set_attribute("error.message", error_message)
|
|
147
|
+
span.set_status(Status(StatusCode.ERROR, error_message))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _infer_output_type(format_obj: FormatParam) -> str:
|
|
151
|
+
"""Infer the GenAI output type from the format parameter."""
|
|
152
|
+
if format_obj is None:
|
|
153
|
+
return GenAIAttributes.GenAiOutputTypeValues.TEXT.value
|
|
154
|
+
return GenAIAttributes.GenAiOutputTypeValues.JSON.value
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _apply_param_attributes(
|
|
158
|
+
attrs: dict[str, AttributeValue], params: ParamsDict
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Apply model parameters as span attributes."""
|
|
161
|
+
if not params:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
for key, attr_key in _PARAM_ATTRIBUTE_MAP.items():
|
|
165
|
+
if key not in params:
|
|
166
|
+
continue
|
|
167
|
+
value = params[key]
|
|
168
|
+
if value is None:
|
|
169
|
+
continue
|
|
170
|
+
if key in {"stop", "stop_sequences"} and isinstance(value, str):
|
|
171
|
+
value = [value]
|
|
172
|
+
attrs[attr_key] = value
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _set_json_attribute(
|
|
176
|
+
setter: AttributeSetter,
|
|
177
|
+
*,
|
|
178
|
+
key: str,
|
|
179
|
+
payload: (
|
|
180
|
+
gen_ai_types.SystemInstructions
|
|
181
|
+
| gen_ai_types.InputMessages
|
|
182
|
+
| gen_ai_types.OutputMessages
|
|
183
|
+
),
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Assign a JSON-serialized attribute to a span."""
|
|
186
|
+
if not payload:
|
|
187
|
+
return
|
|
188
|
+
setter(key, json_dumps(payload))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _assign_request_message_attributes(
|
|
192
|
+
setter: AttributeSetter,
|
|
193
|
+
*,
|
|
194
|
+
messages: Sequence[Message],
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Assign request message attributes to a span."""
|
|
197
|
+
system_payload, input_payload = split_request_messages(messages)
|
|
198
|
+
_set_json_attribute(
|
|
199
|
+
setter,
|
|
200
|
+
key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
|
|
201
|
+
payload=system_payload,
|
|
202
|
+
)
|
|
203
|
+
_set_json_attribute(
|
|
204
|
+
setter,
|
|
205
|
+
key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
|
|
206
|
+
payload=input_payload,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _collect_tool_schemas(
|
|
211
|
+
tools: Sequence[ToolSchema[AnyToolFn]] | BaseToolkit[AnyToolSchema],
|
|
212
|
+
) -> list[ToolSchema[AnyToolFn]]:
|
|
213
|
+
"""Collect ToolSchema instances from a tools parameter."""
|
|
214
|
+
iterable = list(tools.tools) if isinstance(tools, BaseToolkit) else list(tools)
|
|
215
|
+
schemas: list[ToolSchema[AnyToolFn]] = []
|
|
216
|
+
for tool in iterable:
|
|
217
|
+
if isinstance(tool, ToolSchema):
|
|
218
|
+
schemas.append(tool)
|
|
219
|
+
return schemas
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _serialize_tool_definitions(
|
|
223
|
+
tools: ToolsParam,
|
|
224
|
+
format: FormatParam = None,
|
|
225
|
+
) -> str | None:
|
|
226
|
+
"""Serialize tool definitions to JSON for span attributes."""
|
|
227
|
+
if tools is None:
|
|
228
|
+
tool_schemas: list[ToolSchema[AnyToolFn]] = []
|
|
229
|
+
else:
|
|
230
|
+
tool_schemas = _collect_tool_schemas(tools)
|
|
231
|
+
|
|
232
|
+
if isinstance(format, Format) and format.mode == "tool":
|
|
233
|
+
tool_schemas.append(create_tool_schema(format))
|
|
234
|
+
|
|
235
|
+
if not tool_schemas:
|
|
236
|
+
return None
|
|
237
|
+
definitions: list[dict[str, str | int | bool | dict[str, str | int | bool]]] = []
|
|
238
|
+
for tool in tool_schemas:
|
|
239
|
+
definitions.append(
|
|
240
|
+
{
|
|
241
|
+
"name": tool.name,
|
|
242
|
+
"description": tool.description,
|
|
243
|
+
"strict": tool.strict,
|
|
244
|
+
"parameters": tool.parameters.model_dump(by_alias=True, mode="json"),
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
return json_dumps(definitions)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _build_request_attributes(
|
|
251
|
+
*,
|
|
252
|
+
operation: str,
|
|
253
|
+
provider: ProviderId,
|
|
254
|
+
model_id: ModelId,
|
|
255
|
+
messages: Sequence[Message],
|
|
256
|
+
tools: ToolsParam,
|
|
257
|
+
format: FormatParam,
|
|
258
|
+
params: ParamsDict,
|
|
259
|
+
) -> dict[str, AttributeValue]:
|
|
260
|
+
"""Build GenAI request attributes for a span."""
|
|
261
|
+
attrs: dict[str, AttributeValue] = {
|
|
262
|
+
GenAIAttributes.GEN_AI_OPERATION_NAME: operation,
|
|
263
|
+
GenAIAttributes.GEN_AI_PROVIDER_NAME: provider,
|
|
264
|
+
GenAIAttributes.GEN_AI_REQUEST_MODEL: model_id,
|
|
265
|
+
GenAIAttributes.GEN_AI_OUTPUT_TYPE: _infer_output_type(format),
|
|
266
|
+
}
|
|
267
|
+
_apply_param_attributes(attrs, params)
|
|
268
|
+
|
|
269
|
+
_assign_request_message_attributes(
|
|
270
|
+
attrs.__setitem__,
|
|
271
|
+
messages=messages,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
tool_payload = _serialize_tool_definitions(tools, format=format)
|
|
275
|
+
if tool_payload:
|
|
276
|
+
# The incubating semconv module does not yet expose a constant for this key.
|
|
277
|
+
attrs["gen_ai.tool.definitions"] = tool_payload
|
|
278
|
+
|
|
279
|
+
return attrs
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _extract_response_id(
|
|
283
|
+
raw: dict[str, str | int] | str | Identifiable | None,
|
|
284
|
+
) -> str | None:
|
|
285
|
+
"""Extract response ID from raw response data."""
|
|
286
|
+
if isinstance(raw, dict):
|
|
287
|
+
for key in ("id", "response_id", "responseId"):
|
|
288
|
+
value = raw.get(key)
|
|
289
|
+
if isinstance(value, str):
|
|
290
|
+
return value
|
|
291
|
+
elif isinstance(raw, Identifiable):
|
|
292
|
+
return raw.id
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _attach_response(
|
|
297
|
+
span: Span,
|
|
298
|
+
response: RootResponse[ToolkitT, FormattableT | None],
|
|
299
|
+
*,
|
|
300
|
+
request_messages: Sequence[Message],
|
|
301
|
+
) -> None:
|
|
302
|
+
"""Attach response attributes to a GenAI span."""
|
|
303
|
+
span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.model_id)
|
|
304
|
+
span.set_attribute(
|
|
305
|
+
GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS,
|
|
306
|
+
[map_finish_reason(response.finish_reason)],
|
|
307
|
+
)
|
|
308
|
+
response_id = _extract_response_id(getattr(response, "raw", None))
|
|
309
|
+
if response_id:
|
|
310
|
+
span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_ID, response_id)
|
|
311
|
+
|
|
312
|
+
snapshot = snapshot_from_root_response(
|
|
313
|
+
response,
|
|
314
|
+
request_messages=request_messages,
|
|
315
|
+
)
|
|
316
|
+
_set_json_attribute(
|
|
317
|
+
span.set_attribute,
|
|
318
|
+
key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
|
|
319
|
+
payload=snapshot.system_instructions,
|
|
320
|
+
)
|
|
321
|
+
_set_json_attribute(
|
|
322
|
+
span.set_attribute,
|
|
323
|
+
key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
|
|
324
|
+
payload=snapshot.inputs,
|
|
325
|
+
)
|
|
326
|
+
_set_json_attribute(
|
|
327
|
+
span.set_attribute,
|
|
328
|
+
key=GenAIAttributes.GEN_AI_OUTPUT_MESSAGES,
|
|
329
|
+
payload=snapshot.outputs,
|
|
330
|
+
)
|
|
331
|
+
# TODO: Emit gen_ai.usage metrics once Response exposes provider-agnostic usage fields.
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
_ORIGINAL_MODEL_CALL = Model.call
|
|
335
|
+
_MODEL_CALL_WRAPPED = False
|
|
336
|
+
_ORIGINAL_MODEL_CALL_ASYNC = Model.call_async
|
|
337
|
+
_MODEL_CALL_ASYNC_WRAPPED = False
|
|
338
|
+
_ORIGINAL_MODEL_CONTEXT_CALL = Model.context_call
|
|
339
|
+
_MODEL_CONTEXT_CALL_WRAPPED = False
|
|
340
|
+
_ORIGINAL_MODEL_CONTEXT_CALL_ASYNC = Model.context_call_async
|
|
341
|
+
_MODEL_CONTEXT_CALL_ASYNC_WRAPPED = False
|
|
342
|
+
_ORIGINAL_MODEL_STREAM = Model.stream
|
|
343
|
+
_MODEL_STREAM_WRAPPED = False
|
|
344
|
+
_ORIGINAL_MODEL_CONTEXT_STREAM = Model.context_stream
|
|
345
|
+
_MODEL_CONTEXT_STREAM_WRAPPED = False
|
|
346
|
+
_ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC = Model.context_stream_async
|
|
347
|
+
_MODEL_CONTEXT_STREAM_ASYNC_WRAPPED = False
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _is_supported_param_value(value: object) -> TypeIs[ParamsValue]:
|
|
351
|
+
"""Returns True if the value can be exported as an OTEL attribute."""
|
|
352
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
353
|
+
return True
|
|
354
|
+
if isinstance(value, Sequence) and not isinstance(value, str | bytes):
|
|
355
|
+
return all(isinstance(item, str) for item in value)
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _normalize_dropped_value(value: object) -> Jsonable:
|
|
360
|
+
"""Returns a JSON-safe representation for unsupported param values."""
|
|
361
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
362
|
+
return value
|
|
363
|
+
if isinstance(value, Mapping):
|
|
364
|
+
normalized: dict[str, Jsonable] = {}
|
|
365
|
+
for key, item in value.items():
|
|
366
|
+
normalized[str(key)] = _normalize_dropped_value(item)
|
|
367
|
+
return normalized
|
|
368
|
+
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
|
|
369
|
+
return [_normalize_dropped_value(item) for item in value]
|
|
370
|
+
try:
|
|
371
|
+
return str(value)
|
|
372
|
+
except Exception: # pragma: no cover
|
|
373
|
+
return f"<{type(value).__name__}>"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _params_as_mapping(params: Params) -> tuple[ParamsDict, dict[str, Jsonable]]:
|
|
377
|
+
"""Returns supported params and a mapping of dropped params."""
|
|
378
|
+
filtered: dict[str, ParamsValue] = {}
|
|
379
|
+
dropped: dict[str, Jsonable] = {}
|
|
380
|
+
for key, value in params.items():
|
|
381
|
+
if _is_supported_param_value(value):
|
|
382
|
+
filtered[key] = value
|
|
383
|
+
else:
|
|
384
|
+
dropped[key] = _normalize_dropped_value(value)
|
|
385
|
+
return filtered, dropped
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _record_dropped_params(
|
|
389
|
+
span: Span,
|
|
390
|
+
dropped_params: Mapping[str, Jsonable],
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Emit an event with JSON-encoded params that cannot become attributes.
|
|
393
|
+
|
|
394
|
+
See https://opentelemetry.io/docs/specs/otel/common/ for the attribute type limits,
|
|
395
|
+
https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/ for the GenAI
|
|
396
|
+
guidance on recording richer payloads via events, and
|
|
397
|
+
https://opentelemetry.io/blog/2025/complex-attribute-types/ for the recommendation
|
|
398
|
+
to serialize unsupported complex types instead of dropping them outright.
|
|
399
|
+
"""
|
|
400
|
+
if not dropped_params:
|
|
401
|
+
return None
|
|
402
|
+
payload = json_dumps(dropped_params)
|
|
403
|
+
span.add_event(
|
|
404
|
+
"gen_ai.request.params.untracked",
|
|
405
|
+
attributes={
|
|
406
|
+
"gen_ai.untracked_params.count": len(dropped_params),
|
|
407
|
+
"gen_ai.untracked_params.keys": list(dropped_params.keys()),
|
|
408
|
+
"gen_ai.untracked_params.json": payload,
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@contextmanager
|
|
415
|
+
def _start_model_span(
|
|
416
|
+
model: Model,
|
|
417
|
+
*,
|
|
418
|
+
messages: Sequence[Message],
|
|
419
|
+
tools: ToolsParam,
|
|
420
|
+
format: FormatParam,
|
|
421
|
+
activate: bool = True,
|
|
422
|
+
) -> Iterator[SpanContext]:
|
|
423
|
+
"""Context manager that yields a SpanContext for a model call."""
|
|
424
|
+
params, dropped_params = _params_as_mapping(model.params)
|
|
425
|
+
tracer = get_tracer()
|
|
426
|
+
|
|
427
|
+
if tracer is None or otel_trace is None:
|
|
428
|
+
yield SpanContext(None, dropped_params)
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
operation = GenAIAttributes.GenAiOperationNameValues.CHAT.value
|
|
432
|
+
attrs = _build_request_attributes(
|
|
433
|
+
operation=operation,
|
|
434
|
+
provider=model.provider_id,
|
|
435
|
+
model_id=model.model_id,
|
|
436
|
+
messages=messages,
|
|
437
|
+
tools=tools,
|
|
438
|
+
format=format,
|
|
439
|
+
params=params,
|
|
440
|
+
)
|
|
441
|
+
span_name = f"{operation} {model.model_id}"
|
|
442
|
+
|
|
443
|
+
if activate:
|
|
444
|
+
with tracer.start_as_current_span(
|
|
445
|
+
name=span_name,
|
|
446
|
+
kind=SpanKind.CLIENT,
|
|
447
|
+
) as active_span:
|
|
448
|
+
for key, value in attrs.items():
|
|
449
|
+
active_span.set_attribute(key, value)
|
|
450
|
+
try:
|
|
451
|
+
yield SpanContext(active_span, dropped_params)
|
|
452
|
+
except Exception as exc:
|
|
453
|
+
_record_exception(active_span, exc)
|
|
454
|
+
raise
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
span = tracer.start_span(
|
|
458
|
+
name=span_name,
|
|
459
|
+
kind=SpanKind.CLIENT,
|
|
460
|
+
)
|
|
461
|
+
for key, value in attrs.items():
|
|
462
|
+
span.set_attribute(key, value)
|
|
463
|
+
try:
|
|
464
|
+
yield SpanContext(span, dropped_params)
|
|
465
|
+
except Exception as exc:
|
|
466
|
+
_record_exception(span, exc)
|
|
467
|
+
raise
|
|
468
|
+
finally:
|
|
469
|
+
span.end()
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@overload
|
|
473
|
+
def _instrumented_model_call(
|
|
474
|
+
self: Model,
|
|
475
|
+
*,
|
|
476
|
+
messages: Sequence[Message],
|
|
477
|
+
tools: Sequence[Tool] | Toolkit | None = None,
|
|
478
|
+
format: None = None,
|
|
479
|
+
) -> Response: ...
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@overload
|
|
483
|
+
def _instrumented_model_call(
|
|
484
|
+
self: Model,
|
|
485
|
+
*,
|
|
486
|
+
messages: Sequence[Message],
|
|
487
|
+
tools: Sequence[Tool] | Toolkit | None = None,
|
|
488
|
+
format: type[FormattableT] | Format[FormattableT],
|
|
489
|
+
) -> Response[FormattableT]: ...
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@overload
|
|
493
|
+
def _instrumented_model_call(
|
|
494
|
+
self: Model,
|
|
495
|
+
*,
|
|
496
|
+
messages: Sequence[Message],
|
|
497
|
+
tools: Sequence[Tool] | Toolkit | None = None,
|
|
498
|
+
format: type[FormattableT] | Format[FormattableT] | None = None,
|
|
499
|
+
) -> Response | Response[FormattableT]: ...
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@wraps(_ORIGINAL_MODEL_CALL)
|
|
503
|
+
def _instrumented_model_call(
|
|
504
|
+
self: Model,
|
|
505
|
+
*,
|
|
506
|
+
messages: Sequence[Message],
|
|
507
|
+
tools: Sequence[Tool] | Toolkit | None = None,
|
|
508
|
+
format: FormatParam = None,
|
|
509
|
+
) -> Response | Response[FormattableT]:
|
|
510
|
+
"""Returns a GenAI-instrumented result of `Model.call`."""
|
|
511
|
+
with _start_model_span(
|
|
512
|
+
self,
|
|
513
|
+
messages=messages,
|
|
514
|
+
tools=tools,
|
|
515
|
+
format=format,
|
|
516
|
+
) as span_ctx:
|
|
517
|
+
response = _ORIGINAL_MODEL_CALL(
|
|
518
|
+
self,
|
|
519
|
+
messages=messages,
|
|
520
|
+
tools=tools,
|
|
521
|
+
format=format,
|
|
522
|
+
)
|
|
523
|
+
if span_ctx.span is not None:
|
|
524
|
+
_attach_response(
|
|
525
|
+
span_ctx.span,
|
|
526
|
+
response,
|
|
527
|
+
request_messages=messages,
|
|
528
|
+
)
|
|
529
|
+
_record_dropped_params(span_ctx.span, span_ctx.dropped_params)
|
|
530
|
+
return response
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _wrap_model_call() -> None:
|
|
534
|
+
"""Returns None. Replaces `Model.call` with the instrumented wrapper."""
|
|
535
|
+
global _MODEL_CALL_WRAPPED
|
|
536
|
+
if _MODEL_CALL_WRAPPED:
|
|
537
|
+
return
|
|
538
|
+
Model.call = _instrumented_model_call
|
|
539
|
+
_MODEL_CALL_WRAPPED = True
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _unwrap_model_call() -> None:
|
|
543
|
+
"""Returns None. Restores the original `Model.call` implementation."""
|
|
544
|
+
global _MODEL_CALL_WRAPPED
|
|
545
|
+
if not _MODEL_CALL_WRAPPED:
|
|
546
|
+
return
|
|
547
|
+
Model.call = _ORIGINAL_MODEL_CALL
|
|
548
|
+
_MODEL_CALL_WRAPPED = False
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@overload
|
|
552
|
+
async def _instrumented_model_call_async(
|
|
553
|
+
self: Model,
|
|
554
|
+
*,
|
|
555
|
+
messages: Sequence[Message],
|
|
556
|
+
tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
|
|
557
|
+
format: None = None,
|
|
558
|
+
) -> AsyncResponse: ...
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@overload
|
|
562
|
+
async def _instrumented_model_call_async(
|
|
563
|
+
self: Model,
|
|
564
|
+
*,
|
|
565
|
+
messages: Sequence[Message],
|
|
566
|
+
tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
|
|
567
|
+
format: type[FormattableT] | Format[FormattableT],
|
|
568
|
+
) -> AsyncResponse[FormattableT]: ...
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@overload
|
|
572
|
+
async def _instrumented_model_call_async(
|
|
573
|
+
self: Model,
|
|
574
|
+
*,
|
|
575
|
+
messages: Sequence[Message],
|
|
576
|
+
tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
|
|
577
|
+
format: type[FormattableT] | Format[FormattableT] | None = None,
|
|
578
|
+
) -> AsyncResponse | AsyncResponse[FormattableT]: ...
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@wraps(_ORIGINAL_MODEL_CALL_ASYNC)
|
|
582
|
+
async def _instrumented_model_call_async(
|
|
583
|
+
self: Model,
|
|
584
|
+
*,
|
|
585
|
+
messages: Sequence[Message],
|
|
586
|
+
tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
|
|
587
|
+
format: FormatParam = None,
|
|
588
|
+
) -> AsyncResponse | AsyncResponse[FormattableT]:
|
|
589
|
+
"""Returns a GenAI-instrumented result of `Model.call_async`."""
|
|
590
|
+
with _start_model_span(
|
|
591
|
+
self,
|
|
592
|
+
messages=messages,
|
|
593
|
+
tools=tools,
|
|
594
|
+
format=format,
|
|
595
|
+
activate=True,
|
|
596
|
+
) as span_ctx:
|
|
597
|
+
response = await _ORIGINAL_MODEL_CALL_ASYNC(
|
|
598
|
+
self,
|
|
599
|
+
messages=messages,
|
|
600
|
+
tools=tools,
|
|
601
|
+
format=format,
|
|
602
|
+
)
|
|
603
|
+
if span_ctx.span is not None:
|
|
604
|
+
_attach_response(
|
|
605
|
+
span_ctx.span,
|
|
606
|
+
response,
|
|
607
|
+
request_messages=messages,
|
|
608
|
+
)
|
|
609
|
+
_record_dropped_params(span_ctx.span, span_ctx.dropped_params)
|
|
610
|
+
return response
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _wrap_model_call_async() -> None:
|
|
614
|
+
"""Returns None. Replaces `Model.call_async` with the instrumented wrapper."""
|
|
615
|
+
global _MODEL_CALL_ASYNC_WRAPPED
|
|
616
|
+
if _MODEL_CALL_ASYNC_WRAPPED:
|
|
617
|
+
return
|
|
618
|
+
Model.call_async = _instrumented_model_call_async
|
|
619
|
+
_MODEL_CALL_ASYNC_WRAPPED = True
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _unwrap_model_call_async() -> None:
|
|
623
|
+
"""Returns None. Restores the original `Model.call_async` implementation."""
|
|
624
|
+
global _MODEL_CALL_ASYNC_WRAPPED
|
|
625
|
+
if not _MODEL_CALL_ASYNC_WRAPPED:
|
|
626
|
+
return
|
|
627
|
+
Model.call_async = _ORIGINAL_MODEL_CALL_ASYNC
|
|
628
|
+
_MODEL_CALL_ASYNC_WRAPPED = False
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
@overload
|
|
632
|
+
def _instrumented_model_context_call(
|
|
633
|
+
self: Model,
|
|
634
|
+
*,
|
|
635
|
+
ctx: Context[DepsT],
|
|
636
|
+
messages: Sequence[Message],
|
|
637
|
+
tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
|
|
638
|
+
format: None = None,
|
|
639
|
+
) -> ContextResponse[DepsT, None]: ...
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@overload
|
|
643
|
+
def _instrumented_model_context_call(
|
|
644
|
+
self: Model,
|
|
645
|
+
*,
|
|
646
|
+
ctx: Context[DepsT],
|
|
647
|
+
messages: Sequence[Message],
|
|
648
|
+
tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
|
|
649
|
+
format: type[FormattableT] | Format[FormattableT],
|
|
650
|
+
) -> ContextResponse[DepsT, FormattableT]: ...
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@overload
|
|
654
|
+
def _instrumented_model_context_call(
|
|
655
|
+
self: Model,
|
|
656
|
+
*,
|
|
657
|
+
ctx: Context[DepsT],
|
|
658
|
+
messages: Sequence[Message],
|
|
659
|
+
tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
|
|
660
|
+
format: type[FormattableT] | Format[FormattableT] | None = None,
|
|
661
|
+
) -> ContextResponse[DepsT, None] | ContextResponse[DepsT, FormattableT]: ...
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@wraps(_ORIGINAL_MODEL_CONTEXT_CALL)
|
|
665
|
+
def _instrumented_model_context_call(
|
|
666
|
+
self: Model,
|
|
667
|
+
*,
|
|
668
|
+
ctx: Context[DepsT],
|
|
669
|
+
messages: Sequence[Message],
|
|
670
|
+
tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
|
|
671
|
+
format: FormatParam = None,
|
|
672
|
+
) -> ContextResponse[DepsT, None] | ContextResponse[DepsT, FormattableT]:
|
|
673
|
+
"""Returns a GenAI-instrumented result of `Model.context_call`."""
|
|
674
|
+
with _start_model_span(
|
|
675
|
+
self,
|
|
676
|
+
messages=messages,
|
|
677
|
+
tools=tools,
|
|
678
|
+
format=format,
|
|
679
|
+
activate=True,
|
|
680
|
+
) as span_ctx:
|
|
681
|
+
response = _ORIGINAL_MODEL_CONTEXT_CALL(
|
|
682
|
+
self,
|
|
683
|
+
ctx=ctx,
|
|
684
|
+
messages=messages,
|
|
685
|
+
tools=tools,
|
|
686
|
+
format=format,
|
|
687
|
+
)
|
|
688
|
+
if span_ctx.span is not None:
|
|
689
|
+
_attach_response(
|
|
690
|
+
span_ctx.span,
|
|
691
|
+
response,
|
|
692
|
+
request_messages=messages,
|
|
693
|
+
)
|
|
694
|
+
_record_dropped_params(span_ctx.span, span_ctx.dropped_params)
|
|
695
|
+
return response
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _wrap_model_context_call() -> None:
|
|
699
|
+
"""Returns None. Replaces `Model.context_call` with the instrumented wrapper."""
|
|
700
|
+
global _MODEL_CONTEXT_CALL_WRAPPED
|
|
701
|
+
if _MODEL_CONTEXT_CALL_WRAPPED:
|
|
702
|
+
return
|
|
703
|
+
Model.context_call = _instrumented_model_context_call
|
|
704
|
+
_MODEL_CONTEXT_CALL_WRAPPED = True
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _unwrap_model_context_call() -> None:
|
|
708
|
+
"""Returns None. Restores the original `Model.context_call` implementation."""
|
|
709
|
+
global _MODEL_CONTEXT_CALL_WRAPPED
|
|
710
|
+
if not _MODEL_CONTEXT_CALL_WRAPPED:
|
|
711
|
+
return
|
|
712
|
+
Model.context_call = _ORIGINAL_MODEL_CONTEXT_CALL
|
|
713
|
+
_MODEL_CONTEXT_CALL_WRAPPED = False
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@overload
|
|
717
|
+
async def _instrumented_model_context_call_async(
|
|
718
|
+
self: Model,
|
|
719
|
+
*,
|
|
720
|
+
ctx: Context[DepsT],
|
|
721
|
+
messages: Sequence[Message],
|
|
722
|
+
tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
|
|
723
|
+
| AsyncContextToolkit[DepsT]
|
|
724
|
+
| None = None,
|
|
725
|
+
format: None = None,
|
|
726
|
+
) -> AsyncContextResponse[DepsT, None]: ...
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@overload
|
|
730
|
+
async def _instrumented_model_context_call_async(
|
|
731
|
+
self: Model,
|
|
732
|
+
*,
|
|
733
|
+
ctx: Context[DepsT],
|
|
734
|
+
messages: Sequence[Message],
|
|
735
|
+
tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
|
|
736
|
+
| AsyncContextToolkit[DepsT]
|
|
737
|
+
| None = None,
|
|
738
|
+
format: type[FormattableT] | Format[FormattableT],
|
|
739
|
+
) -> AsyncContextResponse[DepsT, FormattableT]: ...
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
@overload
|
|
743
|
+
async def _instrumented_model_context_call_async(
|
|
744
|
+
self: Model,
|
|
745
|
+
*,
|
|
746
|
+
ctx: Context[DepsT],
|
|
747
|
+
messages: Sequence[Message],
|
|
748
|
+
tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
|
|
749
|
+
| AsyncContextToolkit[DepsT]
|
|
750
|
+
| None = None,
|
|
751
|
+
format: type[FormattableT] | Format[FormattableT] | None = None,
|
|
752
|
+
) -> AsyncContextResponse[DepsT, None] | AsyncContextResponse[DepsT, FormattableT]: ...
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@wraps(_ORIGINAL_MODEL_CONTEXT_CALL_ASYNC)
|
|
756
|
+
async def _instrumented_model_context_call_async(
|
|
757
|
+
self: Model,
|
|
758
|
+
*,
|
|
759
|
+
ctx: Context[DepsT],
|
|
760
|
+
messages: Sequence[Message],
|
|
761
|
+
tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
|
|
762
|
+
| AsyncContextToolkit[DepsT]
|
|
763
|
+
| None = None,
|
|
764
|
+
format: FormatParam = None,
|
|
765
|
+
) -> AsyncContextResponse[DepsT, None] | AsyncContextResponse[DepsT, FormattableT]:
|
|
766
|
+
"""Returns a GenAI-instrumented result of `Model.context_call_async`."""
|
|
767
|
+
with _start_model_span(
|
|
768
|
+
self,
|
|
769
|
+
messages=messages,
|
|
770
|
+
tools=tools,
|
|
771
|
+
format=format,
|
|
772
|
+
activate=True,
|
|
773
|
+
) as span_ctx:
|
|
774
|
+
response = await _ORIGINAL_MODEL_CONTEXT_CALL_ASYNC(
|
|
775
|
+
self,
|
|
776
|
+
ctx=ctx,
|
|
777
|
+
messages=messages,
|
|
778
|
+
tools=tools,
|
|
779
|
+
format=format,
|
|
780
|
+
)
|
|
781
|
+
if span_ctx.span is not None:
|
|
782
|
+
_attach_response(
|
|
783
|
+
span_ctx.span,
|
|
784
|
+
response,
|
|
785
|
+
request_messages=messages,
|
|
786
|
+
)
|
|
787
|
+
_record_dropped_params(span_ctx.span, span_ctx.dropped_params)
|
|
788
|
+
return response
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _wrap_model_context_call_async() -> None:
|
|
792
|
+
"""Returns None. Replaces `Model.context_call_async` with the instrumented wrapper."""
|
|
793
|
+
global _MODEL_CONTEXT_CALL_ASYNC_WRAPPED
|
|
794
|
+
if _MODEL_CONTEXT_CALL_ASYNC_WRAPPED:
|
|
795
|
+
return
|
|
796
|
+
Model.context_call_async = _instrumented_model_context_call_async
|
|
797
|
+
_MODEL_CONTEXT_CALL_ASYNC_WRAPPED = True
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _unwrap_model_context_call_async() -> None:
|
|
801
|
+
"""Returns None. Restores the original `Model.context_call_async` implementation."""
|
|
802
|
+
global _MODEL_CONTEXT_CALL_ASYNC_WRAPPED
|
|
803
|
+
if not _MODEL_CONTEXT_CALL_ASYNC_WRAPPED:
|
|
804
|
+
return
|
|
805
|
+
Model.context_call_async = _ORIGINAL_MODEL_CONTEXT_CALL_ASYNC
|
|
806
|
+
_MODEL_CONTEXT_CALL_ASYNC_WRAPPED = False
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@overload
|
|
810
|
+
def _instrumented_model_stream(
|
|
811
|
+
self: Model,
|
|
812
|
+
*,
|
|
813
|
+
messages: Sequence[Message],
|
|
814
|
+
tools: Sequence[Tool] | Toolkit | None = None,
|
|
815
|
+
format: None = None,
|
|
816
|
+
) -> StreamResponse: ...
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
@overload
|
|
820
|
+
def _instrumented_model_stream(
|
|
821
|
+
self: Model,
|
|
822
|
+
*,
|
|
823
|
+
messages: Sequence[Message],
|
|
824
|
+
tools: Sequence[Tool] | Toolkit | None = None,
|
|
825
|
+
format: type[FormattableT] | Format[FormattableT],
|
|
826
|
+
) -> StreamResponse[FormattableT]: ...
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
@overload
|
|
830
|
+
def _instrumented_model_stream(
|
|
831
|
+
self: Model,
|
|
832
|
+
*,
|
|
833
|
+
messages: Sequence[Message],
|
|
834
|
+
tools: Sequence[Tool] | Toolkit | None = None,
|
|
835
|
+
format: type[FormattableT] | Format[FormattableT] | None = None,
|
|
836
|
+
) -> StreamResponse | StreamResponse[FormattableT]: ...
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
@wraps(_ORIGINAL_MODEL_STREAM)
|
|
840
|
+
def _instrumented_model_stream(
|
|
841
|
+
self: Model,
|
|
842
|
+
*,
|
|
843
|
+
messages: Sequence[Message],
|
|
844
|
+
tools: Sequence[Tool] | Toolkit | None = None,
|
|
845
|
+
format: FormatParam = None,
|
|
846
|
+
) -> StreamResponse | StreamResponse[FormattableT]:
|
|
847
|
+
"""Returns a GenAI-instrumented result of `Model.stream`."""
|
|
848
|
+
span_cm = _start_model_span(
|
|
849
|
+
self,
|
|
850
|
+
messages=messages,
|
|
851
|
+
tools=tools,
|
|
852
|
+
format=format,
|
|
853
|
+
activate=False,
|
|
854
|
+
)
|
|
855
|
+
span_ctx = span_cm.__enter__()
|
|
856
|
+
if span_ctx.span is None:
|
|
857
|
+
response = _ORIGINAL_MODEL_STREAM(
|
|
858
|
+
self,
|
|
859
|
+
messages=messages,
|
|
860
|
+
tools=tools,
|
|
861
|
+
format=format,
|
|
862
|
+
)
|
|
863
|
+
span_cm.__exit__(None, None, None)
|
|
864
|
+
return response
|
|
865
|
+
|
|
866
|
+
try:
|
|
867
|
+
with otel_trace.use_span(span_ctx.span, end_on_exit=False):
|
|
868
|
+
response = _ORIGINAL_MODEL_STREAM(
|
|
869
|
+
self,
|
|
870
|
+
messages=messages,
|
|
871
|
+
tools=tools,
|
|
872
|
+
format=format,
|
|
873
|
+
)
|
|
874
|
+
except Exception as exc:
|
|
875
|
+
span_cm.__exit__(type(exc), exc, exc.__traceback__)
|
|
876
|
+
raise
|
|
877
|
+
|
|
878
|
+
_record_dropped_params(span_ctx.span, span_ctx.dropped_params)
|
|
879
|
+
|
|
880
|
+
try:
|
|
881
|
+
_attach_stream_span_handlers(
|
|
882
|
+
response=response,
|
|
883
|
+
span_cm=span_cm,
|
|
884
|
+
span=span_ctx.span,
|
|
885
|
+
request_messages=messages,
|
|
886
|
+
)
|
|
887
|
+
except Exception as exc: # pragma: no cover
|
|
888
|
+
span_cm.__exit__(type(exc), exc, exc.__traceback__)
|
|
889
|
+
raise
|
|
890
|
+
|
|
891
|
+
return response
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _attach_stream_span_handlers(
|
|
895
|
+
*,
|
|
896
|
+
response: ContextStreamResponse[DepsT, FormattableT | None]
|
|
897
|
+
| StreamResponse[FormattableT | None],
|
|
898
|
+
span_cm: AbstractContextManager[SpanContext],
|
|
899
|
+
span: Span,
|
|
900
|
+
request_messages: Sequence[Message],
|
|
901
|
+
) -> None:
|
|
902
|
+
"""Returns None. Closes the span when streaming completes."""
|
|
903
|
+
chunk_iterator: Iterator[StreamResponseChunk] = response._chunk_iterator
|
|
904
|
+
|
|
905
|
+
response_ref = weakref.ref(response)
|
|
906
|
+
closed = False
|
|
907
|
+
|
|
908
|
+
def _close_span(
|
|
909
|
+
exc_type: type[BaseException] | None,
|
|
910
|
+
exc: BaseException | None,
|
|
911
|
+
tb: TracebackType | None,
|
|
912
|
+
) -> None:
|
|
913
|
+
nonlocal closed
|
|
914
|
+
if closed:
|
|
915
|
+
return
|
|
916
|
+
closed = True
|
|
917
|
+
response_obj = response_ref()
|
|
918
|
+
if response_obj is not None:
|
|
919
|
+
_attach_response(
|
|
920
|
+
span,
|
|
921
|
+
response_obj,
|
|
922
|
+
request_messages=request_messages,
|
|
923
|
+
)
|
|
924
|
+
span_cm.__exit__(exc_type, exc, tb)
|
|
925
|
+
|
|
926
|
+
def _wrapped_iterator() -> Iterator[StreamResponseChunk]:
|
|
927
|
+
with otel_trace.use_span(span, end_on_exit=False):
|
|
928
|
+
try:
|
|
929
|
+
yield from chunk_iterator
|
|
930
|
+
except Exception as exc: # noqa: BLE001
|
|
931
|
+
_close_span(type(exc), exc, exc.__traceback__)
|
|
932
|
+
raise
|
|
933
|
+
else:
|
|
934
|
+
_close_span(None, None, None)
|
|
935
|
+
finally:
|
|
936
|
+
_close_span(None, None, None)
|
|
937
|
+
|
|
938
|
+
response._chunk_iterator = _wrapped_iterator()
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def _wrap_model_stream() -> None:
|
|
942
|
+
"""Returns None. Replaces `Model.stream` with the instrumented wrapper."""
|
|
943
|
+
global _MODEL_STREAM_WRAPPED
|
|
944
|
+
if _MODEL_STREAM_WRAPPED:
|
|
945
|
+
return
|
|
946
|
+
Model.stream = _instrumented_model_stream
|
|
947
|
+
_MODEL_STREAM_WRAPPED = True
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _unwrap_model_stream() -> None:
|
|
951
|
+
"""Returns None. Restores the original `Model.stream` implementation."""
|
|
952
|
+
global _MODEL_STREAM_WRAPPED
|
|
953
|
+
if not _MODEL_STREAM_WRAPPED:
|
|
954
|
+
return
|
|
955
|
+
Model.stream = _ORIGINAL_MODEL_STREAM
|
|
956
|
+
_MODEL_STREAM_WRAPPED = False
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@overload
|
|
960
|
+
def _instrumented_model_context_stream(
|
|
961
|
+
self: Model,
|
|
962
|
+
*,
|
|
963
|
+
ctx: Context[DepsT],
|
|
964
|
+
messages: Sequence[Message],
|
|
965
|
+
tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
|
|
966
|
+
format: None = None,
|
|
967
|
+
) -> ContextStreamResponse[DepsT, None]: ...
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
@overload
|
|
971
|
+
def _instrumented_model_context_stream(
|
|
972
|
+
self: Model,
|
|
973
|
+
*,
|
|
974
|
+
ctx: Context[DepsT],
|
|
975
|
+
messages: Sequence[Message],
|
|
976
|
+
tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
|
|
977
|
+
format: type[FormattableT] | Format[FormattableT],
|
|
978
|
+
) -> ContextStreamResponse[DepsT, FormattableT]: ...
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
@overload
|
|
982
|
+
def _instrumented_model_context_stream(
|
|
983
|
+
self: Model,
|
|
984
|
+
*,
|
|
985
|
+
ctx: Context[DepsT],
|
|
986
|
+
messages: Sequence[Message],
|
|
987
|
+
tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
|
|
988
|
+
format: type[FormattableT] | Format[FormattableT] | None = None,
|
|
989
|
+
) -> (
|
|
990
|
+
ContextStreamResponse[DepsT, None] | ContextStreamResponse[DepsT, FormattableT]
|
|
991
|
+
): ...
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
@wraps(_ORIGINAL_MODEL_CONTEXT_STREAM)
|
|
995
|
+
def _instrumented_model_context_stream(
|
|
996
|
+
self: Model,
|
|
997
|
+
*,
|
|
998
|
+
ctx: Context[DepsT],
|
|
999
|
+
messages: Sequence[Message],
|
|
1000
|
+
tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
|
|
1001
|
+
format: FormatParam = None,
|
|
1002
|
+
) -> ContextStreamResponse[DepsT, None] | ContextStreamResponse[DepsT, FormattableT]:
|
|
1003
|
+
"""Returns a GenAI-instrumented result of `Model.context_stream`."""
|
|
1004
|
+
span_cm = _start_model_span(
|
|
1005
|
+
self,
|
|
1006
|
+
messages=messages,
|
|
1007
|
+
tools=tools,
|
|
1008
|
+
format=format,
|
|
1009
|
+
activate=False,
|
|
1010
|
+
)
|
|
1011
|
+
span_ctx = span_cm.__enter__()
|
|
1012
|
+
if span_ctx.span is None:
|
|
1013
|
+
response = _ORIGINAL_MODEL_CONTEXT_STREAM(
|
|
1014
|
+
self,
|
|
1015
|
+
ctx=ctx,
|
|
1016
|
+
messages=messages,
|
|
1017
|
+
tools=tools,
|
|
1018
|
+
format=format,
|
|
1019
|
+
)
|
|
1020
|
+
span_cm.__exit__(None, None, None)
|
|
1021
|
+
return response
|
|
1022
|
+
|
|
1023
|
+
try:
|
|
1024
|
+
with otel_trace.use_span(span_ctx.span, end_on_exit=False):
|
|
1025
|
+
response = _ORIGINAL_MODEL_CONTEXT_STREAM(
|
|
1026
|
+
self,
|
|
1027
|
+
ctx=ctx,
|
|
1028
|
+
messages=messages,
|
|
1029
|
+
tools=tools,
|
|
1030
|
+
format=format,
|
|
1031
|
+
)
|
|
1032
|
+
except Exception as exc:
|
|
1033
|
+
span_cm.__exit__(type(exc), exc, exc.__traceback__)
|
|
1034
|
+
raise
|
|
1035
|
+
|
|
1036
|
+
_record_dropped_params(span_ctx.span, span_ctx.dropped_params)
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
_attach_stream_span_handlers(
|
|
1040
|
+
response=response,
|
|
1041
|
+
span_cm=span_cm,
|
|
1042
|
+
span=span_ctx.span,
|
|
1043
|
+
request_messages=messages,
|
|
1044
|
+
)
|
|
1045
|
+
except Exception as exc: # pragma: no cover
|
|
1046
|
+
span_cm.__exit__(type(exc), exc, exc.__traceback__)
|
|
1047
|
+
raise
|
|
1048
|
+
|
|
1049
|
+
return response
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _wrap_model_context_stream() -> None:
|
|
1053
|
+
"""Returns None. Replaces `Model.context_stream` with the instrumented wrapper."""
|
|
1054
|
+
global _MODEL_CONTEXT_STREAM_WRAPPED
|
|
1055
|
+
if _MODEL_CONTEXT_STREAM_WRAPPED:
|
|
1056
|
+
return
|
|
1057
|
+
Model.context_stream = _instrumented_model_context_stream
|
|
1058
|
+
_MODEL_CONTEXT_STREAM_WRAPPED = True
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def _unwrap_model_context_stream() -> None:
|
|
1062
|
+
"""Returns None. Restores the original `Model.context_stream` implementation."""
|
|
1063
|
+
global _MODEL_CONTEXT_STREAM_WRAPPED
|
|
1064
|
+
if not _MODEL_CONTEXT_STREAM_WRAPPED:
|
|
1065
|
+
return
|
|
1066
|
+
Model.context_stream = _ORIGINAL_MODEL_CONTEXT_STREAM
|
|
1067
|
+
_MODEL_CONTEXT_STREAM_WRAPPED = False
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _attach_async_stream_span_handlers(
|
|
1071
|
+
*,
|
|
1072
|
+
response: AsyncContextStreamResponse[DepsT, FormattableT | None],
|
|
1073
|
+
span_cm: AbstractContextManager[SpanContext],
|
|
1074
|
+
span: Span,
|
|
1075
|
+
request_messages: Sequence[Message],
|
|
1076
|
+
) -> None:
|
|
1077
|
+
"""Returns None. Closes the span when async streaming completes."""
|
|
1078
|
+
chunk_iterator: AsyncIterator[StreamResponseChunk] = response._chunk_iterator
|
|
1079
|
+
|
|
1080
|
+
response_ref = weakref.ref(response)
|
|
1081
|
+
closed = False
|
|
1082
|
+
|
|
1083
|
+
def _close_span(
|
|
1084
|
+
exc_type: type[BaseException] | None,
|
|
1085
|
+
exc: BaseException | None,
|
|
1086
|
+
tb: TracebackType | None,
|
|
1087
|
+
) -> None:
|
|
1088
|
+
nonlocal closed
|
|
1089
|
+
if closed:
|
|
1090
|
+
return
|
|
1091
|
+
closed = True
|
|
1092
|
+
response_obj = response_ref()
|
|
1093
|
+
if response_obj is not None:
|
|
1094
|
+
_attach_response(
|
|
1095
|
+
span,
|
|
1096
|
+
response_obj,
|
|
1097
|
+
request_messages=request_messages,
|
|
1098
|
+
)
|
|
1099
|
+
span_cm.__exit__(exc_type, exc, tb)
|
|
1100
|
+
|
|
1101
|
+
async def _wrapped_iterator() -> AsyncIterator[StreamResponseChunk]:
|
|
1102
|
+
try:
|
|
1103
|
+
async for chunk in chunk_iterator:
|
|
1104
|
+
yield chunk
|
|
1105
|
+
except Exception as exc: # noqa: BLE001
|
|
1106
|
+
_close_span(type(exc), exc, exc.__traceback__)
|
|
1107
|
+
raise
|
|
1108
|
+
else:
|
|
1109
|
+
_close_span(None, None, None)
|
|
1110
|
+
finally:
|
|
1111
|
+
_close_span(None, None, None)
|
|
1112
|
+
|
|
1113
|
+
response._chunk_iterator = _wrapped_iterator()
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
@overload
|
|
1117
|
+
async def _instrumented_model_context_stream_async(
|
|
1118
|
+
self: Model,
|
|
1119
|
+
*,
|
|
1120
|
+
ctx: Context[DepsT],
|
|
1121
|
+
messages: Sequence[Message],
|
|
1122
|
+
tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
|
|
1123
|
+
| AsyncContextToolkit[DepsT]
|
|
1124
|
+
| None = None,
|
|
1125
|
+
format: None = None,
|
|
1126
|
+
) -> AsyncContextStreamResponse[DepsT, None]: ...
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
@overload
|
|
1130
|
+
async def _instrumented_model_context_stream_async(
|
|
1131
|
+
self: Model,
|
|
1132
|
+
*,
|
|
1133
|
+
ctx: Context[DepsT],
|
|
1134
|
+
messages: Sequence[Message],
|
|
1135
|
+
tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
|
|
1136
|
+
| AsyncContextToolkit[DepsT]
|
|
1137
|
+
| None = None,
|
|
1138
|
+
format: type[FormattableT] | Format[FormattableT],
|
|
1139
|
+
) -> AsyncContextStreamResponse[DepsT, FormattableT]: ...
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
@overload
|
|
1143
|
+
async def _instrumented_model_context_stream_async(
|
|
1144
|
+
self: Model,
|
|
1145
|
+
*,
|
|
1146
|
+
ctx: Context[DepsT],
|
|
1147
|
+
messages: Sequence[Message],
|
|
1148
|
+
tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
|
|
1149
|
+
| AsyncContextToolkit[DepsT]
|
|
1150
|
+
| None = None,
|
|
1151
|
+
format: type[FormattableT] | Format[FormattableT] | None = None,
|
|
1152
|
+
) -> (
|
|
1153
|
+
AsyncContextStreamResponse[DepsT, None]
|
|
1154
|
+
| AsyncContextStreamResponse[DepsT, FormattableT]
|
|
1155
|
+
): ...
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
@wraps(_ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC)
|
|
1159
|
+
async def _instrumented_model_context_stream_async(
|
|
1160
|
+
self: Model,
|
|
1161
|
+
*,
|
|
1162
|
+
ctx: Context[DepsT],
|
|
1163
|
+
messages: Sequence[Message],
|
|
1164
|
+
tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
|
|
1165
|
+
| AsyncContextToolkit[DepsT]
|
|
1166
|
+
| None = None,
|
|
1167
|
+
format: FormatParam = None,
|
|
1168
|
+
) -> (
|
|
1169
|
+
AsyncContextStreamResponse[DepsT, None]
|
|
1170
|
+
| AsyncContextStreamResponse[DepsT, FormattableT]
|
|
1171
|
+
):
|
|
1172
|
+
"""Returns a GenAI-instrumented result of `Model.context_stream_async`."""
|
|
1173
|
+
span_cm = _start_model_span(
|
|
1174
|
+
self,
|
|
1175
|
+
messages=messages,
|
|
1176
|
+
tools=tools,
|
|
1177
|
+
format=format,
|
|
1178
|
+
activate=False,
|
|
1179
|
+
)
|
|
1180
|
+
span_ctx = span_cm.__enter__()
|
|
1181
|
+
if span_ctx.span is None:
|
|
1182
|
+
response = await _ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC(
|
|
1183
|
+
self,
|
|
1184
|
+
ctx=ctx,
|
|
1185
|
+
messages=messages,
|
|
1186
|
+
tools=tools,
|
|
1187
|
+
format=format,
|
|
1188
|
+
)
|
|
1189
|
+
span_cm.__exit__(None, None, None)
|
|
1190
|
+
return response
|
|
1191
|
+
|
|
1192
|
+
try:
|
|
1193
|
+
with otel_trace.use_span(span_ctx.span, end_on_exit=False):
|
|
1194
|
+
response = await _ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC(
|
|
1195
|
+
self,
|
|
1196
|
+
ctx=ctx,
|
|
1197
|
+
messages=messages,
|
|
1198
|
+
tools=tools,
|
|
1199
|
+
format=format,
|
|
1200
|
+
)
|
|
1201
|
+
except Exception as exc:
|
|
1202
|
+
span_cm.__exit__(type(exc), exc, exc.__traceback__)
|
|
1203
|
+
raise
|
|
1204
|
+
|
|
1205
|
+
_record_dropped_params(span_ctx.span, span_ctx.dropped_params)
|
|
1206
|
+
|
|
1207
|
+
try:
|
|
1208
|
+
_attach_async_stream_span_handlers(
|
|
1209
|
+
response=response,
|
|
1210
|
+
span_cm=span_cm,
|
|
1211
|
+
span=span_ctx.span,
|
|
1212
|
+
request_messages=messages,
|
|
1213
|
+
)
|
|
1214
|
+
except Exception as exc: # pragma: no cover
|
|
1215
|
+
span_cm.__exit__(type(exc), exc, exc.__traceback__)
|
|
1216
|
+
raise
|
|
1217
|
+
|
|
1218
|
+
return response
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _wrap_model_context_stream_async() -> None:
|
|
1222
|
+
"""Returns None. Replaces `Model.context_stream_async` with the instrumented wrapper."""
|
|
1223
|
+
global _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED
|
|
1224
|
+
if _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED:
|
|
1225
|
+
return
|
|
1226
|
+
Model.context_stream_async = _instrumented_model_context_stream_async
|
|
1227
|
+
_MODEL_CONTEXT_STREAM_ASYNC_WRAPPED = True
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def _unwrap_model_context_stream_async() -> None:
|
|
1231
|
+
"""Returns None. Restores the original `Model.context_stream_async` implementation."""
|
|
1232
|
+
global _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED
|
|
1233
|
+
if not _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED:
|
|
1234
|
+
return
|
|
1235
|
+
Model.context_stream_async = _ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC
|
|
1236
|
+
_MODEL_CONTEXT_STREAM_ASYNC_WRAPPED = False
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def instrument_llm() -> None:
|
|
1240
|
+
"""Enable GenAI 1.38 span emission for future `llm.Model` calls and streams.
|
|
1241
|
+
|
|
1242
|
+
Uses the tracer provider configured via `ops.configure()`. If no provider
|
|
1243
|
+
was configured, uses the global OpenTelemetry tracer provider.
|
|
1244
|
+
|
|
1245
|
+
Example:
|
|
1246
|
+
|
|
1247
|
+
Enable instrumentation with a custom provider:
|
|
1248
|
+
```python
|
|
1249
|
+
from mirascope import ops
|
|
1250
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
1251
|
+
|
|
1252
|
+
provider = TracerProvider()
|
|
1253
|
+
ops.configure(tracer_provider=provider)
|
|
1254
|
+
ops.instrument_llm()
|
|
1255
|
+
```
|
|
1256
|
+
"""
|
|
1257
|
+
if otel_trace is None: # pragma: no cover
|
|
1258
|
+
raise ImportError(
|
|
1259
|
+
"OpenTelemetry is not installed. Run `pip install mirascope[otel]` "
|
|
1260
|
+
"and ensure `opentelemetry-api` is available before calling "
|
|
1261
|
+
"`instrument_llm`."
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
os.environ.setdefault("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_latest_experimental")
|
|
1265
|
+
|
|
1266
|
+
if get_tracer() is None: # pragma: no cover
|
|
1267
|
+
raise RuntimeError(
|
|
1268
|
+
"You must call `configure()` before calling `instrument_llm()`."
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
_wrap_model_call()
|
|
1272
|
+
_wrap_model_call_async()
|
|
1273
|
+
_wrap_model_context_call()
|
|
1274
|
+
_wrap_model_context_call_async()
|
|
1275
|
+
_wrap_model_stream()
|
|
1276
|
+
_wrap_model_context_stream()
|
|
1277
|
+
_wrap_model_context_stream_async()
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def uninstrument_llm() -> None:
|
|
1281
|
+
"""Disable previously configured instrumentation."""
|
|
1282
|
+
_unwrap_model_call()
|
|
1283
|
+
_unwrap_model_call_async()
|
|
1284
|
+
_unwrap_model_context_call()
|
|
1285
|
+
_unwrap_model_context_call_async()
|
|
1286
|
+
_unwrap_model_stream()
|
|
1287
|
+
_unwrap_model_context_stream()
|
|
1288
|
+
_unwrap_model_context_stream_async()
|