aiqa-client 0.4.3__py3-none-any.whl → 0.5.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.
- aiqa/__init__.py +1 -1
- aiqa/client.py +108 -23
- aiqa/constants.py +3 -1
- aiqa/experiment_runner.py +12 -29
- aiqa/http_utils.py +143 -0
- aiqa/object_serialiser.py +136 -115
- aiqa/tracing.py +155 -267
- aiqa/tracing_llm_utils.py +191 -0
- {aiqa_client-0.4.3.dist-info → aiqa_client-0.5.2.dist-info}/METADATA +1 -1
- aiqa_client-0.5.2.dist-info/RECORD +14 -0
- aiqa/aiqa_exporter.py +0 -679
- aiqa/test_experiment_runner.py +0 -176
- aiqa/test_startup_reliability.py +0 -249
- aiqa/test_tracing.py +0 -230
- aiqa_client-0.4.3.dist-info/RECORD +0 -16
- {aiqa_client-0.4.3.dist-info → aiqa_client-0.5.2.dist-info}/WHEEL +0 -0
- {aiqa_client-0.4.3.dist-info → aiqa_client-0.5.2.dist-info}/licenses/LICENSE.txt +0 -0
- {aiqa_client-0.4.3.dist-info → aiqa_client-0.5.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Functions for extracting and setting LLM-specific attributes on a span.
|
|
2
|
+
import logging
|
|
3
|
+
from .constants import LOG_TAG
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(LOG_TAG)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_attribute_set(span: trace.Span, attribute_name: str) -> bool:
|
|
11
|
+
"""
|
|
12
|
+
Check if an attribute is already set on a span.
|
|
13
|
+
Returns True if the attribute exists, False otherwise.
|
|
14
|
+
Safe against exceptions.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
# Try multiple ways to access span attributes (SDK spans may store them differently)
|
|
18
|
+
# Check public 'attributes' property
|
|
19
|
+
if hasattr(span, "attributes"):
|
|
20
|
+
attrs = span.attributes
|
|
21
|
+
if attrs and attribute_name in attrs:
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
# Check private '_attributes' (common in OpenTelemetry SDK)
|
|
25
|
+
if hasattr(span, "_attributes"):
|
|
26
|
+
attrs = span._attributes
|
|
27
|
+
if attrs and attribute_name in attrs:
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
# If we can't find the attribute, assume not set (conservative approach)
|
|
31
|
+
return False
|
|
32
|
+
except Exception:
|
|
33
|
+
# If anything goes wrong, assume not set (conservative approach)
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def _extract_and_set_token_usage(span: trace.Span, result: Any) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Extract OpenAI API style token usage from result and add to span attributes
|
|
39
|
+
using OpenTelemetry semantic conventions for gen_ai.
|
|
40
|
+
|
|
41
|
+
Looks for usage dict with prompt_tokens, completion_tokens, and total_tokens.
|
|
42
|
+
Sets gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, and gen_ai.usage.total_tokens.
|
|
43
|
+
Only sets attributes that are not already set.
|
|
44
|
+
|
|
45
|
+
This function detects token usage from OpenAI API response patterns:
|
|
46
|
+
- OpenAI Chat Completions API: The 'usage' object contains 'prompt_tokens', 'completion_tokens', and 'total_tokens'.
|
|
47
|
+
See https://platform.openai.com/docs/api-reference/chat/object (usage field)
|
|
48
|
+
- OpenAI Completions API: The 'usage' object contains 'prompt_tokens', 'completion_tokens', and 'total_tokens'.
|
|
49
|
+
See https://platform.openai.com/docs/api-reference/completions/object (usage field)
|
|
50
|
+
|
|
51
|
+
This function is safe against exceptions and will not derail tracing or program execution.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
if not span.is_recording():
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
usage = None
|
|
58
|
+
|
|
59
|
+
# Check if result is a dict with 'usage' key
|
|
60
|
+
try:
|
|
61
|
+
if isinstance(result, dict):
|
|
62
|
+
usage = result.get("usage")
|
|
63
|
+
# Also check if result itself is a usage dict (OpenAI format)
|
|
64
|
+
if usage is None and all(key in result for key in ("prompt_tokens", "completion_tokens", "total_tokens")):
|
|
65
|
+
usage = result
|
|
66
|
+
# Also check if result itself is a usage dict (Bedrock format)
|
|
67
|
+
elif usage is None and all(key in result for key in ("input_tokens", "output_tokens")):
|
|
68
|
+
usage = result
|
|
69
|
+
|
|
70
|
+
# Check if result has a 'usage' attribute (e.g., OpenAI response object)
|
|
71
|
+
elif hasattr(result, "usage"):
|
|
72
|
+
usage = result.usage
|
|
73
|
+
except Exception:
|
|
74
|
+
# If accessing result properties fails, just return silently
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Extract token usage if found
|
|
78
|
+
if isinstance(usage, dict):
|
|
79
|
+
try:
|
|
80
|
+
# Support both OpenAI format (prompt_tokens/completion_tokens) and Bedrock format (input_tokens/output_tokens)
|
|
81
|
+
prompt_tokens = usage.get("prompt_tokens") or usage.get("PromptTokens")
|
|
82
|
+
completion_tokens = usage.get("completion_tokens") or usage.get("CompletionTokens")
|
|
83
|
+
input_tokens = usage.get("input_tokens") or usage.get("InputTokens")
|
|
84
|
+
output_tokens = usage.get("output_tokens") or usage.get("OutputTokens")
|
|
85
|
+
total_tokens = usage.get("total_tokens") or usage.get("TotalTokens")
|
|
86
|
+
|
|
87
|
+
# Use Bedrock format if OpenAI format not available
|
|
88
|
+
if prompt_tokens is None:
|
|
89
|
+
prompt_tokens = input_tokens
|
|
90
|
+
if completion_tokens is None:
|
|
91
|
+
completion_tokens = output_tokens
|
|
92
|
+
|
|
93
|
+
# Calculate total_tokens if not provided but we have input and output
|
|
94
|
+
if total_tokens is None and prompt_tokens is not None and completion_tokens is not None:
|
|
95
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
96
|
+
|
|
97
|
+
# Only set attributes that are not already set
|
|
98
|
+
if prompt_tokens is not None and not _is_attribute_set(span, "gen_ai.usage.input_tokens"):
|
|
99
|
+
span.set_attribute("gen_ai.usage.input_tokens", prompt_tokens)
|
|
100
|
+
if completion_tokens is not None and not _is_attribute_set(span, "gen_ai.usage.output_tokens"):
|
|
101
|
+
span.set_attribute("gen_ai.usage.output_tokens", completion_tokens)
|
|
102
|
+
if total_tokens is not None and not _is_attribute_set(span, "gen_ai.usage.total_tokens"):
|
|
103
|
+
span.set_attribute("gen_ai.usage.total_tokens", total_tokens)
|
|
104
|
+
except Exception:
|
|
105
|
+
# If setting attributes fails, log but don't raise
|
|
106
|
+
logger.debug(f"Failed to set token usage attributes on span")
|
|
107
|
+
except Exception:
|
|
108
|
+
# Catch any other exceptions to ensure this never derails tracing
|
|
109
|
+
logger.debug(f"Error in _extract_and_set_token_usage")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _extract_and_set_provider_and_model(span: trace.Span, result: Any) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Extract provider and model information from result and add to span attributes
|
|
115
|
+
using OpenTelemetry semantic conventions for gen_ai.
|
|
116
|
+
|
|
117
|
+
Looks for 'model', 'provider', 'provider_name' fields in the result.
|
|
118
|
+
Sets gen_ai.provider.name and gen_ai.request.model.
|
|
119
|
+
Only sets attributes that are not already set.
|
|
120
|
+
|
|
121
|
+
This function detects model information from common API response patterns:
|
|
122
|
+
- OpenAI Chat Completions API: The 'model' field is at the top level of the response.
|
|
123
|
+
See https://platform.openai.com/docs/api-reference/chat/object
|
|
124
|
+
- OpenAI Completions API: The 'model' field is at the top level of the response.
|
|
125
|
+
See https://platform.openai.com/docs/api-reference/completions/object
|
|
126
|
+
|
|
127
|
+
This function is safe against exceptions and will not derail tracing or program execution.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
if not span.is_recording():
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
model = None
|
|
134
|
+
provider = None
|
|
135
|
+
|
|
136
|
+
# Check if result is a dict
|
|
137
|
+
try:
|
|
138
|
+
if isinstance(result, dict):
|
|
139
|
+
model = result.get("model") or result.get("Model")
|
|
140
|
+
provider = result.get("provider") or result.get("Provider") or result.get("provider_name") or result.get("providerName")
|
|
141
|
+
|
|
142
|
+
# Check if result has attributes (e.g., OpenAI response object)
|
|
143
|
+
elif hasattr(result, "model"):
|
|
144
|
+
model = result.model
|
|
145
|
+
if hasattr(result, "provider"):
|
|
146
|
+
provider = result.provider
|
|
147
|
+
elif hasattr(result, "provider_name"):
|
|
148
|
+
provider = result.provider_name
|
|
149
|
+
elif hasattr(result, "providerName"):
|
|
150
|
+
provider = result.providerName
|
|
151
|
+
|
|
152
|
+
# Check nested structures (e.g., response.data.model)
|
|
153
|
+
if model is None and hasattr(result, "data"):
|
|
154
|
+
data = result.data
|
|
155
|
+
if isinstance(data, dict):
|
|
156
|
+
model = data.get("model") or data.get("Model")
|
|
157
|
+
elif hasattr(data, "model"):
|
|
158
|
+
model = data.model
|
|
159
|
+
|
|
160
|
+
# Check for model in choices (OpenAI pattern)
|
|
161
|
+
if model is None and isinstance(result, dict):
|
|
162
|
+
choices = result.get("choices")
|
|
163
|
+
if choices and isinstance(choices, list) and len(choices) > 0:
|
|
164
|
+
first_choice = choices[0]
|
|
165
|
+
if isinstance(first_choice, dict):
|
|
166
|
+
model = first_choice.get("model")
|
|
167
|
+
elif hasattr(first_choice, "model"):
|
|
168
|
+
model = first_choice.model
|
|
169
|
+
except Exception:
|
|
170
|
+
# If accessing result properties fails, just return silently
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Set attributes if found and not already set
|
|
174
|
+
try:
|
|
175
|
+
if model is not None and not _is_attribute_set(span, "gen_ai.request.model"):
|
|
176
|
+
# Convert to string if needed
|
|
177
|
+
model_str = str(model) if model is not None else None
|
|
178
|
+
if model_str:
|
|
179
|
+
span.set_attribute("gen_ai.request.model", model_str)
|
|
180
|
+
|
|
181
|
+
if provider is not None and not _is_attribute_set(span, "gen_ai.provider.name"):
|
|
182
|
+
# Convert to string if needed
|
|
183
|
+
provider_str = str(provider) if provider is not None else None
|
|
184
|
+
if provider_str:
|
|
185
|
+
span.set_attribute("gen_ai.provider.name", provider_str)
|
|
186
|
+
except Exception:
|
|
187
|
+
# If setting attributes fails, log but don't raise
|
|
188
|
+
logger.debug(f"Failed to set provider/model attributes on span")
|
|
189
|
+
except Exception:
|
|
190
|
+
# Catch any other exceptions to ensure this never derails tracing
|
|
191
|
+
logger.debug(f"Error in _extract_and_set_provider_and_model")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
aiqa/__init__.py,sha256=V1VLfpxU_BXfkvKyhGckZsPYF43NJhoXeVX6FkeHr9g,1721
|
|
2
|
+
aiqa/client.py,sha256=Ba3v-voBlTSCr-RU88INLXsF_5vqp42QiQWCFciSJbU,12542
|
|
3
|
+
aiqa/constants.py,sha256=tZuh7XvKs6hFvWc-YnQ5Na6uogJMsRrMy-rWOauvcIA,226
|
|
4
|
+
aiqa/experiment_runner.py,sha256=XAZsjVP70UH_QTk5ANSOQYAhmozuGXwKB5qWWHs-zeE,11186
|
|
5
|
+
aiqa/http_utils.py,sha256=OIB4tRI2TiDl4VKDmtbLWg9Q7TicMBeL7scLYEhVPXI,4944
|
|
6
|
+
aiqa/object_serialiser.py,sha256=DBv7EyXIwfwjwXHDsIwdZNFmQffRb5fKAE0r8qhoqgc,16958
|
|
7
|
+
aiqa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
aiqa/tracing.py,sha256=gdmtpvBVbvc_HrJHgtr78_XH9sIWRjUoEkXuCuNmuc0,45662
|
|
9
|
+
aiqa/tracing_llm_utils.py,sha256=rNx6v6Wh_Mhv-_DPU9_aWS7YQcO46oiv0YPdBK1KVL8,9338
|
|
10
|
+
aiqa_client-0.5.2.dist-info/licenses/LICENSE.txt,sha256=kIzkzLuzG0HHaWYm4F4W5FeJ1Yxut3Ec6bhLWyw798A,1062
|
|
11
|
+
aiqa_client-0.5.2.dist-info/METADATA,sha256=xMaQSnI3AiNE6lYs2vM6BV9VxQWMHXyDoIl6JXwdi3I,7705
|
|
12
|
+
aiqa_client-0.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
aiqa_client-0.5.2.dist-info/top_level.txt,sha256=nwcsuVVSuWu27iLxZd4n1evVzv1W6FVTrSnCXCc-NQs,5
|
|
14
|
+
aiqa_client-0.5.2.dist-info/RECORD,,
|