traceAI-minimax 0.1.0__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.
- traceai_minimax/__init__.py +106 -0
- traceai_minimax/_request_attributes_extractor.py +115 -0
- traceai_minimax/_response_attributes_extractor.py +190 -0
- traceai_minimax/_utils.py +91 -0
- traceai_minimax/_with_span.py +40 -0
- traceai_minimax/_wrappers.py +274 -0
- traceai_minimax/version.py +1 -0
- traceai_minimax-0.1.0.dist-info/METADATA +291 -0
- traceai_minimax-0.1.0.dist-info/RECORD +10 -0
- traceai_minimax-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
from typing import Any, Collection
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
from fi_instrumentation import FITracer, TraceConfig
|
|
8
|
+
from opentelemetry import trace as trace_api
|
|
9
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
10
|
+
from wrapt import wrap_function_wrapper
|
|
11
|
+
|
|
12
|
+
from traceai_minimax._wrappers import (
|
|
13
|
+
_AsyncChatCompletionWrapper,
|
|
14
|
+
_ChatCompletionWrapper,
|
|
15
|
+
)
|
|
16
|
+
from traceai_minimax.version import __version__
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
logger.addHandler(logging.NullHandler())
|
|
20
|
+
|
|
21
|
+
_instruments = ("openai >= 1.0.0",)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MiniMaxInstrumentor(BaseInstrumentor):
|
|
25
|
+
"""
|
|
26
|
+
OpenTelemetry instrumentor for MiniMax.
|
|
27
|
+
|
|
28
|
+
MiniMax uses the OpenAI SDK with a custom base_url (https://api.minimax.io/v1).
|
|
29
|
+
This instrumentor wraps OpenAI client methods and only instruments calls
|
|
30
|
+
when the client is configured with a MiniMax base URL.
|
|
31
|
+
|
|
32
|
+
Supports:
|
|
33
|
+
- MiniMax-M2.7 (latest flagship model with enhanced reasoning and coding)
|
|
34
|
+
- MiniMax-M2.7-highspeed (high-speed version of M2.7 for low-latency scenarios)
|
|
35
|
+
- MiniMax-M2.5 (204K context window)
|
|
36
|
+
- MiniMax-M2.5-highspeed (204K context window, faster inference)
|
|
37
|
+
- Streaming and non-streaming responses
|
|
38
|
+
- Function/tool calling
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__slots__ = (
|
|
42
|
+
"_original_chat_create",
|
|
43
|
+
"_original_async_chat_create",
|
|
44
|
+
"_tracer",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
48
|
+
return _instruments
|
|
49
|
+
|
|
50
|
+
def _instrument(self, **kwargs: Any) -> None:
|
|
51
|
+
if not (tracer_provider := kwargs.get("tracer_provider")):
|
|
52
|
+
tracer_provider = trace_api.get_tracer_provider()
|
|
53
|
+
if not (config := kwargs.get("config")):
|
|
54
|
+
config = TraceConfig()
|
|
55
|
+
else:
|
|
56
|
+
assert isinstance(config, TraceConfig)
|
|
57
|
+
|
|
58
|
+
self._tracer = FITracer(
|
|
59
|
+
trace_api.get_tracer(__name__, __version__, tracer_provider),
|
|
60
|
+
config=config,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Import openai module
|
|
64
|
+
try:
|
|
65
|
+
openai_module = import_module("openai")
|
|
66
|
+
except ImportError:
|
|
67
|
+
logger.warning("openai package not installed, MiniMax instrumentation skipped")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Wrap OpenAI client chat.completions.create
|
|
71
|
+
# The wrapper checks if the client is configured for MiniMax
|
|
72
|
+
try:
|
|
73
|
+
# Sync client
|
|
74
|
+
if hasattr(openai_module, "OpenAI"):
|
|
75
|
+
from openai.resources.chat import completions
|
|
76
|
+
|
|
77
|
+
self._original_chat_create = completions.Completions.create
|
|
78
|
+
wrap_function_wrapper(
|
|
79
|
+
module="openai.resources.chat.completions",
|
|
80
|
+
name="Completions.create",
|
|
81
|
+
wrapper=_ChatCompletionWrapper(tracer=self._tracer),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Async client
|
|
85
|
+
if hasattr(openai_module, "AsyncOpenAI"):
|
|
86
|
+
from openai.resources.chat import completions as async_completions
|
|
87
|
+
|
|
88
|
+
self._original_async_chat_create = async_completions.AsyncCompletions.create
|
|
89
|
+
wrap_function_wrapper(
|
|
90
|
+
module="openai.resources.chat.completions",
|
|
91
|
+
name="AsyncCompletions.create",
|
|
92
|
+
wrapper=_AsyncChatCompletionWrapper(tracer=self._tracer),
|
|
93
|
+
)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning(f"Failed to instrument OpenAI for MiniMax: {e}")
|
|
96
|
+
|
|
97
|
+
def _uninstrument(self, **kwargs: Any) -> None:
|
|
98
|
+
try:
|
|
99
|
+
from openai.resources.chat import completions
|
|
100
|
+
|
|
101
|
+
if hasattr(self, "_original_chat_create"):
|
|
102
|
+
completions.Completions.create = self._original_chat_create
|
|
103
|
+
if hasattr(self, "_original_async_chat_create"):
|
|
104
|
+
completions.AsyncCompletions.create = self._original_async_chat_create
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning(f"Failed to uninstrument: {e}")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, Iterator, Mapping, Tuple
|
|
3
|
+
|
|
4
|
+
from fi_instrumentation import safe_json_dumps
|
|
5
|
+
from fi_instrumentation.fi_types import (
|
|
6
|
+
FiMimeTypeValues,
|
|
7
|
+
FiSpanKindValues,
|
|
8
|
+
MessageAttributes,
|
|
9
|
+
SpanAttributes,
|
|
10
|
+
)
|
|
11
|
+
from opentelemetry.util.types import AttributeValue
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
logger.addHandler(logging.NullHandler())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _ChatCompletionRequestAttributesExtractor:
|
|
18
|
+
"""Extract span attributes from MiniMax chat completion request."""
|
|
19
|
+
|
|
20
|
+
def get_attributes_from_request(
|
|
21
|
+
self, request_parameters: Mapping[str, Any]
|
|
22
|
+
) -> Iterator[Tuple[str, AttributeValue]]:
|
|
23
|
+
"""Extract primary attributes from request."""
|
|
24
|
+
yield SpanAttributes.GEN_AI_SPAN_KIND, FiSpanKindValues.LLM.value
|
|
25
|
+
yield SpanAttributes.GEN_AI_PROVIDER_NAME, "minimax"
|
|
26
|
+
|
|
27
|
+
model = request_parameters.get("model", "MiniMax-M2.7")
|
|
28
|
+
yield SpanAttributes.GEN_AI_REQUEST_MODEL, model
|
|
29
|
+
|
|
30
|
+
yield SpanAttributes.INPUT_MIME_TYPE, FiMimeTypeValues.JSON.value
|
|
31
|
+
yield SpanAttributes.OUTPUT_MIME_TYPE, FiMimeTypeValues.JSON.value
|
|
32
|
+
|
|
33
|
+
# Invocation parameters
|
|
34
|
+
invocation_params = self._extract_invocation_parameters(request_parameters)
|
|
35
|
+
if invocation_params:
|
|
36
|
+
yield SpanAttributes.GEN_AI_REQUEST_PARAMETERS, safe_json_dumps(invocation_params)
|
|
37
|
+
|
|
38
|
+
def get_extra_attributes_from_request(
|
|
39
|
+
self, request_parameters: Mapping[str, Any]
|
|
40
|
+
) -> Iterator[Tuple[str, AttributeValue]]:
|
|
41
|
+
"""Extract extra attributes including messages."""
|
|
42
|
+
messages = request_parameters.get("messages", [])
|
|
43
|
+
|
|
44
|
+
# Track input messages
|
|
45
|
+
for i, msg in enumerate(messages):
|
|
46
|
+
role = msg.get("role", "")
|
|
47
|
+
content = msg.get("content", "")
|
|
48
|
+
|
|
49
|
+
# Handle content that might be a list (for multimodal)
|
|
50
|
+
if isinstance(content, list):
|
|
51
|
+
text_parts = []
|
|
52
|
+
for part in content:
|
|
53
|
+
if isinstance(part, dict) and part.get("type") == "text":
|
|
54
|
+
text_parts.append(part.get("text", ""))
|
|
55
|
+
content = "\n".join(text_parts)
|
|
56
|
+
|
|
57
|
+
yield f"{SpanAttributes.GEN_AI_INPUT_MESSAGES}.{i}.{MessageAttributes.MESSAGE_ROLE}", role
|
|
58
|
+
yield f"{SpanAttributes.GEN_AI_INPUT_MESSAGES}.{i}.{MessageAttributes.MESSAGE_CONTENT}", str(content)
|
|
59
|
+
|
|
60
|
+
# Handle tool calls in assistant messages
|
|
61
|
+
if role == "assistant" and "tool_calls" in msg:
|
|
62
|
+
tool_calls = msg.get("tool_calls", [])
|
|
63
|
+
for j, tc in enumerate(tool_calls):
|
|
64
|
+
yield f"{SpanAttributes.GEN_AI_INPUT_MESSAGES}.{i}.tool_calls.{j}.id", tc.get("id", "")
|
|
65
|
+
yield f"{SpanAttributes.GEN_AI_INPUT_MESSAGES}.{i}.tool_calls.{j}.type", tc.get("type", "function")
|
|
66
|
+
if "function" in tc:
|
|
67
|
+
yield f"{SpanAttributes.GEN_AI_INPUT_MESSAGES}.{i}.tool_calls.{j}.function.name", tc["function"].get("name", "")
|
|
68
|
+
yield f"{SpanAttributes.GEN_AI_INPUT_MESSAGES}.{i}.tool_calls.{j}.function.arguments", tc["function"].get("arguments", "")
|
|
69
|
+
|
|
70
|
+
# System prompt extraction
|
|
71
|
+
for msg in messages:
|
|
72
|
+
if msg.get("role") == "system":
|
|
73
|
+
yield SpanAttributes.GEN_AI_PROVIDER_NAME_PROMPT, str(msg.get("content", ""))
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
# User input for display
|
|
77
|
+
for msg in reversed(messages):
|
|
78
|
+
if msg.get("role") == "user":
|
|
79
|
+
content = msg.get("content", "")
|
|
80
|
+
if isinstance(content, list):
|
|
81
|
+
text_parts = [p.get("text", "") for p in content if isinstance(p, dict) and p.get("type") == "text"]
|
|
82
|
+
content = "\n".join(text_parts)
|
|
83
|
+
yield SpanAttributes.INPUT_VALUE, str(content)
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
# Tools
|
|
87
|
+
if "tools" in request_parameters:
|
|
88
|
+
tools = request_parameters.get("tools", [])
|
|
89
|
+
yield "minimax.tools_count", len(tools)
|
|
90
|
+
for i, tool in enumerate(tools[:10]): # Limit to first 10
|
|
91
|
+
if "function" in tool:
|
|
92
|
+
func = tool["function"]
|
|
93
|
+
yield f"minimax.tools.{i}.name", func.get("name", "")
|
|
94
|
+
yield f"minimax.tools.{i}.description", func.get("description", "")[:200] # Truncate
|
|
95
|
+
|
|
96
|
+
# Response format
|
|
97
|
+
if "response_format" in request_parameters:
|
|
98
|
+
resp_format = request_parameters.get("response_format", {})
|
|
99
|
+
if isinstance(resp_format, dict):
|
|
100
|
+
yield "minimax.response_format", resp_format.get("type", "text")
|
|
101
|
+
|
|
102
|
+
def _extract_invocation_parameters(
|
|
103
|
+
self, request_parameters: Mapping[str, Any]
|
|
104
|
+
) -> Dict[str, Any]:
|
|
105
|
+
"""Extract model invocation parameters."""
|
|
106
|
+
params = {}
|
|
107
|
+
param_keys = [
|
|
108
|
+
"temperature", "max_tokens", "top_p", "stop", "stream",
|
|
109
|
+
"frequency_penalty", "presence_penalty", "seed", "logprobs",
|
|
110
|
+
"top_logprobs", "n"
|
|
111
|
+
]
|
|
112
|
+
for key in param_keys:
|
|
113
|
+
if key in request_parameters:
|
|
114
|
+
params[key] = request_parameters[key]
|
|
115
|
+
return params
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, Iterator, Mapping, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from fi_instrumentation.fi_types import (
|
|
5
|
+
MessageAttributes,
|
|
6
|
+
SpanAttributes,
|
|
7
|
+
)
|
|
8
|
+
from opentelemetry.util.types import AttributeValue
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
logger.addHandler(logging.NullHandler())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _ChatCompletionResponseAttributesExtractor:
|
|
15
|
+
"""Extract span attributes from MiniMax chat completion response."""
|
|
16
|
+
|
|
17
|
+
def get_attributes(
|
|
18
|
+
self,
|
|
19
|
+
response: Optional[Dict[str, Any]],
|
|
20
|
+
is_streaming: bool = False,
|
|
21
|
+
) -> Iterator[Tuple[str, AttributeValue]]:
|
|
22
|
+
"""Extract primary attributes from response."""
|
|
23
|
+
if response is None:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
# Token usage
|
|
27
|
+
usage = response.get("usage", {})
|
|
28
|
+
if usage:
|
|
29
|
+
if "prompt_tokens" in usage:
|
|
30
|
+
yield SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS, usage["prompt_tokens"]
|
|
31
|
+
if "completion_tokens" in usage:
|
|
32
|
+
yield SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, usage["completion_tokens"]
|
|
33
|
+
if "total_tokens" in usage:
|
|
34
|
+
yield SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, usage["total_tokens"]
|
|
35
|
+
|
|
36
|
+
# Model info
|
|
37
|
+
if "model" in response:
|
|
38
|
+
yield SpanAttributes.GEN_AI_REQUEST_MODEL, response["model"]
|
|
39
|
+
|
|
40
|
+
# Response ID
|
|
41
|
+
if "id" in response:
|
|
42
|
+
yield "minimax.response_id", response["id"]
|
|
43
|
+
|
|
44
|
+
def get_extra_attributes(
|
|
45
|
+
self,
|
|
46
|
+
response: Optional[Dict[str, Any]],
|
|
47
|
+
request_parameters: Mapping[str, Any],
|
|
48
|
+
is_streaming: bool = False,
|
|
49
|
+
) -> Iterator[Tuple[str, AttributeValue]]:
|
|
50
|
+
"""Extract extra attributes including output."""
|
|
51
|
+
if response is None:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
choices = response.get("choices", [])
|
|
55
|
+
|
|
56
|
+
for i, choice in enumerate(choices):
|
|
57
|
+
message = choice.get("message", {})
|
|
58
|
+
|
|
59
|
+
# Role
|
|
60
|
+
role = message.get("role", "assistant")
|
|
61
|
+
yield f"{SpanAttributes.GEN_AI_OUTPUT_MESSAGES}.{i}.{MessageAttributes.MESSAGE_ROLE}", role
|
|
62
|
+
|
|
63
|
+
# Content
|
|
64
|
+
content = message.get("content", "")
|
|
65
|
+
if content:
|
|
66
|
+
yield f"{SpanAttributes.GEN_AI_OUTPUT_MESSAGES}.{i}.{MessageAttributes.MESSAGE_CONTENT}", content
|
|
67
|
+
|
|
68
|
+
# Tool calls
|
|
69
|
+
tool_calls = message.get("tool_calls", [])
|
|
70
|
+
if tool_calls:
|
|
71
|
+
yield "minimax.tool_calls_count", len(tool_calls)
|
|
72
|
+
for j, tc in enumerate(tool_calls[:10]): # Limit
|
|
73
|
+
yield f"minimax.tool_calls.{j}.id", tc.get("id", "")
|
|
74
|
+
yield f"minimax.tool_calls.{j}.type", tc.get("type", "function")
|
|
75
|
+
if "function" in tc:
|
|
76
|
+
yield f"minimax.tool_calls.{j}.function.name", tc["function"].get("name", "")
|
|
77
|
+
yield f"minimax.tool_calls.{j}.function.arguments", tc["function"].get("arguments", "")
|
|
78
|
+
|
|
79
|
+
# Finish reason
|
|
80
|
+
finish_reason = choice.get("finish_reason", "")
|
|
81
|
+
if finish_reason:
|
|
82
|
+
yield "minimax.finish_reason", finish_reason
|
|
83
|
+
|
|
84
|
+
# Logprobs
|
|
85
|
+
if "logprobs" in choice and choice["logprobs"]:
|
|
86
|
+
yield "minimax.has_logprobs", True
|
|
87
|
+
|
|
88
|
+
# Primary output
|
|
89
|
+
if choices:
|
|
90
|
+
first_choice = choices[0]
|
|
91
|
+
message = first_choice.get("message", {})
|
|
92
|
+
content = message.get("content", "")
|
|
93
|
+
if content:
|
|
94
|
+
yield SpanAttributes.OUTPUT_VALUE, content
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _StreamingChatCompletionResponseExtractor:
|
|
98
|
+
"""Extract attributes from streaming MiniMax response chunks."""
|
|
99
|
+
|
|
100
|
+
def __init__(self):
|
|
101
|
+
self._content_parts = []
|
|
102
|
+
self._tool_calls = {}
|
|
103
|
+
self._finish_reason = None
|
|
104
|
+
self._usage = {}
|
|
105
|
+
self._model = None
|
|
106
|
+
self._response_id = None
|
|
107
|
+
|
|
108
|
+
def process_chunk(self, chunk: Dict[str, Any]) -> None:
|
|
109
|
+
"""Process a single streaming chunk."""
|
|
110
|
+
if "id" in chunk and not self._response_id:
|
|
111
|
+
self._response_id = chunk["id"]
|
|
112
|
+
|
|
113
|
+
if "model" in chunk and not self._model:
|
|
114
|
+
self._model = chunk["model"]
|
|
115
|
+
|
|
116
|
+
# Handle usage in final chunk
|
|
117
|
+
if "usage" in chunk:
|
|
118
|
+
self._usage = chunk["usage"]
|
|
119
|
+
|
|
120
|
+
choices = chunk.get("choices", [])
|
|
121
|
+
for choice in choices:
|
|
122
|
+
delta = choice.get("delta", {})
|
|
123
|
+
|
|
124
|
+
# Content
|
|
125
|
+
if "content" in delta and delta["content"]:
|
|
126
|
+
self._content_parts.append(delta["content"])
|
|
127
|
+
|
|
128
|
+
# Tool calls
|
|
129
|
+
if "tool_calls" in delta:
|
|
130
|
+
for tc in delta["tool_calls"]:
|
|
131
|
+
idx = tc.get("index", 0)
|
|
132
|
+
if idx not in self._tool_calls:
|
|
133
|
+
self._tool_calls[idx] = {
|
|
134
|
+
"id": tc.get("id", ""),
|
|
135
|
+
"type": tc.get("type", "function"),
|
|
136
|
+
"function": {"name": "", "arguments": ""}
|
|
137
|
+
}
|
|
138
|
+
if "id" in tc and tc["id"]:
|
|
139
|
+
self._tool_calls[idx]["id"] = tc["id"]
|
|
140
|
+
if "function" in tc:
|
|
141
|
+
if "name" in tc["function"] and tc["function"]["name"]:
|
|
142
|
+
self._tool_calls[idx]["function"]["name"] = tc["function"]["name"]
|
|
143
|
+
if "arguments" in tc["function"]:
|
|
144
|
+
self._tool_calls[idx]["function"]["arguments"] += tc["function"]["arguments"]
|
|
145
|
+
|
|
146
|
+
# Finish reason
|
|
147
|
+
if "finish_reason" in choice and choice["finish_reason"]:
|
|
148
|
+
self._finish_reason = choice["finish_reason"]
|
|
149
|
+
|
|
150
|
+
def get_attributes(self) -> Iterator[Tuple[str, AttributeValue]]:
|
|
151
|
+
"""Get attributes from accumulated stream data."""
|
|
152
|
+
# Token usage
|
|
153
|
+
if self._usage:
|
|
154
|
+
if "prompt_tokens" in self._usage:
|
|
155
|
+
yield SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS, self._usage["prompt_tokens"]
|
|
156
|
+
if "completion_tokens" in self._usage:
|
|
157
|
+
yield SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, self._usage["completion_tokens"]
|
|
158
|
+
if "total_tokens" in self._usage:
|
|
159
|
+
yield SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, self._usage["total_tokens"]
|
|
160
|
+
|
|
161
|
+
if self._model:
|
|
162
|
+
yield SpanAttributes.GEN_AI_REQUEST_MODEL, self._model
|
|
163
|
+
|
|
164
|
+
if self._response_id:
|
|
165
|
+
yield "minimax.response_id", self._response_id
|
|
166
|
+
|
|
167
|
+
def get_extra_attributes(
|
|
168
|
+
self, request_parameters: Mapping[str, Any]
|
|
169
|
+
) -> Iterator[Tuple[str, AttributeValue]]:
|
|
170
|
+
"""Get extra attributes from accumulated stream."""
|
|
171
|
+
content = "".join(self._content_parts)
|
|
172
|
+
|
|
173
|
+
# Output message
|
|
174
|
+
yield f"{SpanAttributes.GEN_AI_OUTPUT_MESSAGES}.0.{MessageAttributes.MESSAGE_ROLE}", "assistant"
|
|
175
|
+
if content:
|
|
176
|
+
yield f"{SpanAttributes.GEN_AI_OUTPUT_MESSAGES}.0.{MessageAttributes.MESSAGE_CONTENT}", content
|
|
177
|
+
yield SpanAttributes.OUTPUT_VALUE, content
|
|
178
|
+
|
|
179
|
+
# Tool calls
|
|
180
|
+
if self._tool_calls:
|
|
181
|
+
yield "minimax.tool_calls_count", len(self._tool_calls)
|
|
182
|
+
for idx, tc in self._tool_calls.items():
|
|
183
|
+
yield f"minimax.tool_calls.{idx}.id", tc["id"]
|
|
184
|
+
yield f"minimax.tool_calls.{idx}.type", tc["type"]
|
|
185
|
+
yield f"minimax.tool_calls.{idx}.function.name", tc["function"]["name"]
|
|
186
|
+
yield f"minimax.tool_calls.{idx}.function.arguments", tc["function"]["arguments"]
|
|
187
|
+
|
|
188
|
+
# Finish reason
|
|
189
|
+
if self._finish_reason:
|
|
190
|
+
yield "minimax.finish_reason", self._finish_reason
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Dict, Iterator, Mapping, Optional, Tuple
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
from fi_instrumentation import safe_json_dumps
|
|
7
|
+
from opentelemetry import trace as trace_api
|
|
8
|
+
from opentelemetry.util.types import AttributeValue
|
|
9
|
+
|
|
10
|
+
from traceai_minimax._with_span import _WithSpan
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
logger.addHandler(logging.NullHandler())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MiniMaxBaseURL(str, Enum):
|
|
17
|
+
"""MiniMax API base URL hostnames."""
|
|
18
|
+
API_IO = "api.minimax.io"
|
|
19
|
+
API_CHAT = "api.minimax.chat"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_minimax_client(instance: Any) -> bool:
|
|
23
|
+
"""Check if the OpenAI client is configured for MiniMax."""
|
|
24
|
+
try:
|
|
25
|
+
base_url = getattr(instance, "base_url", None)
|
|
26
|
+
if base_url is None:
|
|
27
|
+
# Check _client for wrapped clients
|
|
28
|
+
client = getattr(instance, "_client", None)
|
|
29
|
+
if client is not None:
|
|
30
|
+
base_url = getattr(client, "base_url", None)
|
|
31
|
+
|
|
32
|
+
if base_url is not None:
|
|
33
|
+
parsed = urlparse(str(base_url))
|
|
34
|
+
hostname = (parsed.hostname or "").lower()
|
|
35
|
+
return hostname in {url.value for url in MiniMaxBaseURL}
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _to_dict(obj: Any) -> Dict[str, Any]:
|
|
42
|
+
"""Convert an object to a dictionary."""
|
|
43
|
+
if obj is None:
|
|
44
|
+
return {}
|
|
45
|
+
if isinstance(obj, dict):
|
|
46
|
+
return obj
|
|
47
|
+
if hasattr(obj, "model_dump"):
|
|
48
|
+
return obj.model_dump()
|
|
49
|
+
if hasattr(obj, "dict"):
|
|
50
|
+
return obj.dict()
|
|
51
|
+
if hasattr(obj, "__dict__"):
|
|
52
|
+
return obj.__dict__
|
|
53
|
+
return {"value": str(obj)}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _flatten(
|
|
57
|
+
mapping: Mapping[str, Any], prefix: str = ""
|
|
58
|
+
) -> Iterator[Tuple[str, AttributeValue]]:
|
|
59
|
+
"""Flatten a nested dictionary into dot-separated keys."""
|
|
60
|
+
for key, value in mapping.items():
|
|
61
|
+
if value is None:
|
|
62
|
+
continue
|
|
63
|
+
full_key = f"{prefix}.{key}" if prefix else key
|
|
64
|
+
if isinstance(value, Mapping):
|
|
65
|
+
yield from _flatten(value, full_key)
|
|
66
|
+
elif isinstance(value, (list, tuple)):
|
|
67
|
+
if value and isinstance(value[0], Mapping):
|
|
68
|
+
for i, item in enumerate(value):
|
|
69
|
+
yield from _flatten(item, f"{full_key}.{i}")
|
|
70
|
+
else:
|
|
71
|
+
yield full_key, safe_json_dumps(value)
|
|
72
|
+
else:
|
|
73
|
+
yield full_key, value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _finish_tracing(
|
|
77
|
+
status: trace_api.Status,
|
|
78
|
+
with_span: _WithSpan,
|
|
79
|
+
attributes: Optional[Iterator[Tuple[str, AttributeValue]]] = None,
|
|
80
|
+
extra_attributes: Optional[Iterator[Tuple[str, AttributeValue]]] = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Finish tracing with status and attributes."""
|
|
83
|
+
if attributes:
|
|
84
|
+
for key, value in attributes:
|
|
85
|
+
if value is not None:
|
|
86
|
+
with_span._span.set_attribute(key, value)
|
|
87
|
+
if extra_attributes:
|
|
88
|
+
for key, value in extra_attributes:
|
|
89
|
+
if value is not None:
|
|
90
|
+
with_span._span.set_attribute(key, value)
|
|
91
|
+
with_span.finish_tracing(status=status)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from opentelemetry import trace as trace_api
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _WithSpan:
|
|
7
|
+
"""Helper class to manage span lifecycle and attributes."""
|
|
8
|
+
|
|
9
|
+
__slots__ = ("_span", "_context_attributes", "_extra_attributes")
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
span: trace_api.Span,
|
|
14
|
+
context_attributes: Optional[Dict[str, Any]] = None,
|
|
15
|
+
extra_attributes: Optional[Dict[str, Any]] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
self._span = span
|
|
18
|
+
self._context_attributes = context_attributes or {}
|
|
19
|
+
self._extra_attributes = extra_attributes or {}
|
|
20
|
+
|
|
21
|
+
def record_exception(self, exception: Exception) -> None:
|
|
22
|
+
"""Record an exception on the span."""
|
|
23
|
+
if self._span.is_recording():
|
|
24
|
+
self._span.record_exception(exception)
|
|
25
|
+
|
|
26
|
+
def finish_tracing(
|
|
27
|
+
self,
|
|
28
|
+
status: Optional[trace_api.Status] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Finish the span with optional status."""
|
|
31
|
+
if self._span.is_recording():
|
|
32
|
+
if status is not None:
|
|
33
|
+
self._span.set_status(status)
|
|
34
|
+
for key, value in self._context_attributes.items():
|
|
35
|
+
if value is not None:
|
|
36
|
+
self._span.set_attribute(key, value)
|
|
37
|
+
for key, value in self._extra_attributes.items():
|
|
38
|
+
if value is not None:
|
|
39
|
+
self._span.set_attribute(key, value)
|
|
40
|
+
self._span.end()
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, Tuple
|
|
5
|
+
|
|
6
|
+
import opentelemetry.context as context_api
|
|
7
|
+
from fi_instrumentation import get_attributes_from_context, safe_json_dumps
|
|
8
|
+
from fi_instrumentation.fi_types import SpanAttributes
|
|
9
|
+
from opentelemetry import trace as trace_api
|
|
10
|
+
from opentelemetry.trace import INVALID_SPAN
|
|
11
|
+
from opentelemetry.util.types import AttributeValue
|
|
12
|
+
|
|
13
|
+
from traceai_minimax._request_attributes_extractor import (
|
|
14
|
+
_ChatCompletionRequestAttributesExtractor,
|
|
15
|
+
)
|
|
16
|
+
from traceai_minimax._response_attributes_extractor import (
|
|
17
|
+
_ChatCompletionResponseAttributesExtractor,
|
|
18
|
+
_StreamingChatCompletionResponseExtractor,
|
|
19
|
+
)
|
|
20
|
+
from traceai_minimax._utils import _finish_tracing, _to_dict, is_minimax_client
|
|
21
|
+
from traceai_minimax._with_span import _WithSpan
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
logger.addHandler(logging.NullHandler())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _WithTracer(ABC):
|
|
28
|
+
"""Base class for wrappers that need a tracer."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, tracer: trace_api.Tracer, *args: Any, **kwargs: Any) -> None:
|
|
31
|
+
super().__init__(*args, **kwargs)
|
|
32
|
+
self._tracer = tracer
|
|
33
|
+
|
|
34
|
+
@contextmanager
|
|
35
|
+
def _start_as_current_span(
|
|
36
|
+
self,
|
|
37
|
+
span_name: str,
|
|
38
|
+
attributes: Iterable[Tuple[str, AttributeValue]],
|
|
39
|
+
context_attributes: Iterable[Tuple[str, AttributeValue]],
|
|
40
|
+
extra_attributes: Iterable[Tuple[str, AttributeValue]],
|
|
41
|
+
) -> Iterator[_WithSpan]:
|
|
42
|
+
try:
|
|
43
|
+
span = self._tracer.start_span(
|
|
44
|
+
name=span_name, attributes=dict(extra_attributes)
|
|
45
|
+
)
|
|
46
|
+
except Exception:
|
|
47
|
+
span = INVALID_SPAN
|
|
48
|
+
with trace_api.use_span(
|
|
49
|
+
span,
|
|
50
|
+
end_on_exit=False,
|
|
51
|
+
record_exception=False,
|
|
52
|
+
set_status_on_exception=False,
|
|
53
|
+
) as span:
|
|
54
|
+
yield _WithSpan(
|
|
55
|
+
span=span,
|
|
56
|
+
context_attributes=dict(context_attributes),
|
|
57
|
+
extra_attributes=dict(attributes),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _ChatCompletionWrapper(_WithTracer):
|
|
62
|
+
"""Wrapper for MiniMax chat completions (via OpenAI client)."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
65
|
+
super().__init__(*args, **kwargs)
|
|
66
|
+
self._request_extractor = _ChatCompletionRequestAttributesExtractor()
|
|
67
|
+
self._response_extractor = _ChatCompletionResponseAttributesExtractor()
|
|
68
|
+
|
|
69
|
+
def __call__(
|
|
70
|
+
self,
|
|
71
|
+
wrapped: Callable[..., Any],
|
|
72
|
+
instance: Any,
|
|
73
|
+
args: Tuple[Any, ...],
|
|
74
|
+
kwargs: Mapping[str, Any],
|
|
75
|
+
) -> Any:
|
|
76
|
+
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
|
|
77
|
+
return wrapped(*args, **kwargs)
|
|
78
|
+
|
|
79
|
+
# Check if this is a MiniMax client
|
|
80
|
+
# Get the parent client from the completions instance
|
|
81
|
+
client = getattr(instance, "_client", None)
|
|
82
|
+
if client is None:
|
|
83
|
+
# Try to get from chat.completions structure
|
|
84
|
+
chat = getattr(instance, "_parent", None) or getattr(instance, "chat", None)
|
|
85
|
+
if chat:
|
|
86
|
+
client = getattr(chat, "_client", None)
|
|
87
|
+
|
|
88
|
+
if not is_minimax_client(client) and not is_minimax_client(instance):
|
|
89
|
+
return wrapped(*args, **kwargs)
|
|
90
|
+
|
|
91
|
+
request_parameters = dict(kwargs)
|
|
92
|
+
is_streaming = request_parameters.get("stream", False)
|
|
93
|
+
|
|
94
|
+
span_name = "minimax.chat.completions"
|
|
95
|
+
|
|
96
|
+
with self._start_as_current_span(
|
|
97
|
+
span_name=span_name,
|
|
98
|
+
attributes=self._request_extractor.get_attributes_from_request(request_parameters),
|
|
99
|
+
context_attributes=get_attributes_from_context(),
|
|
100
|
+
extra_attributes=self._request_extractor.get_extra_attributes_from_request(request_parameters),
|
|
101
|
+
) as span:
|
|
102
|
+
try:
|
|
103
|
+
response = wrapped(*args, **kwargs)
|
|
104
|
+
except Exception as exception:
|
|
105
|
+
span.record_exception(exception)
|
|
106
|
+
status = trace_api.Status(
|
|
107
|
+
status_code=trace_api.StatusCode.ERROR,
|
|
108
|
+
description=f"{type(exception).__name__}: {exception}",
|
|
109
|
+
)
|
|
110
|
+
span.finish_tracing(status=status)
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
if is_streaming:
|
|
114
|
+
return self._handle_streaming_response(span, response, request_parameters)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
response_dict = _to_dict(response)
|
|
118
|
+
_finish_tracing(
|
|
119
|
+
status=trace_api.Status(status_code=trace_api.StatusCode.OK),
|
|
120
|
+
with_span=span,
|
|
121
|
+
attributes=self._response_extractor.get_attributes(response_dict),
|
|
122
|
+
extra_attributes=self._response_extractor.get_extra_attributes(
|
|
123
|
+
response_dict, request_parameters
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
except Exception:
|
|
127
|
+
logger.exception("Failed to finalize response")
|
|
128
|
+
span.finish_tracing()
|
|
129
|
+
return response
|
|
130
|
+
|
|
131
|
+
def _handle_streaming_response(
|
|
132
|
+
self, span: _WithSpan, response: Any, request_parameters: Dict[str, Any]
|
|
133
|
+
):
|
|
134
|
+
"""Handle streaming response."""
|
|
135
|
+
stream_extractor = _StreamingChatCompletionResponseExtractor()
|
|
136
|
+
|
|
137
|
+
def streaming_wrapper():
|
|
138
|
+
try:
|
|
139
|
+
for chunk in response:
|
|
140
|
+
chunk_dict = _to_dict(chunk)
|
|
141
|
+
stream_extractor.process_chunk(chunk_dict)
|
|
142
|
+
yield chunk
|
|
143
|
+
|
|
144
|
+
except Exception as exception:
|
|
145
|
+
span.record_exception(exception)
|
|
146
|
+
status = trace_api.Status(
|
|
147
|
+
status_code=trace_api.StatusCode.ERROR,
|
|
148
|
+
description=f"{type(exception).__name__}: {exception}",
|
|
149
|
+
)
|
|
150
|
+
span.finish_tracing(status=status)
|
|
151
|
+
raise
|
|
152
|
+
else:
|
|
153
|
+
try:
|
|
154
|
+
_finish_tracing(
|
|
155
|
+
status=trace_api.Status(status_code=trace_api.StatusCode.OK),
|
|
156
|
+
with_span=span,
|
|
157
|
+
attributes=stream_extractor.get_attributes(),
|
|
158
|
+
extra_attributes=stream_extractor.get_extra_attributes(request_parameters),
|
|
159
|
+
)
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.exception("Failed to finalize streaming response")
|
|
162
|
+
span.finish_tracing()
|
|
163
|
+
finally:
|
|
164
|
+
if span._span.is_recording():
|
|
165
|
+
span.finish_tracing()
|
|
166
|
+
|
|
167
|
+
return streaming_wrapper()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class _AsyncChatCompletionWrapper(_WithTracer):
|
|
171
|
+
"""Async wrapper for MiniMax chat completions."""
|
|
172
|
+
|
|
173
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
174
|
+
super().__init__(*args, **kwargs)
|
|
175
|
+
self._request_extractor = _ChatCompletionRequestAttributesExtractor()
|
|
176
|
+
self._response_extractor = _ChatCompletionResponseAttributesExtractor()
|
|
177
|
+
|
|
178
|
+
async def __call__(
|
|
179
|
+
self,
|
|
180
|
+
wrapped: Callable[..., Any],
|
|
181
|
+
instance: Any,
|
|
182
|
+
args: Tuple[Any, ...],
|
|
183
|
+
kwargs: Mapping[str, Any],
|
|
184
|
+
) -> Any:
|
|
185
|
+
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
|
|
186
|
+
return await wrapped(*args, **kwargs)
|
|
187
|
+
|
|
188
|
+
# Check if this is a MiniMax client
|
|
189
|
+
client = getattr(instance, "_client", None)
|
|
190
|
+
if client is None:
|
|
191
|
+
chat = getattr(instance, "_parent", None) or getattr(instance, "chat", None)
|
|
192
|
+
if chat:
|
|
193
|
+
client = getattr(chat, "_client", None)
|
|
194
|
+
|
|
195
|
+
if not is_minimax_client(client) and not is_minimax_client(instance):
|
|
196
|
+
return await wrapped(*args, **kwargs)
|
|
197
|
+
|
|
198
|
+
request_parameters = dict(kwargs)
|
|
199
|
+
is_streaming = request_parameters.get("stream", False)
|
|
200
|
+
|
|
201
|
+
span_name = "minimax.chat.completions"
|
|
202
|
+
|
|
203
|
+
with self._start_as_current_span(
|
|
204
|
+
span_name=span_name,
|
|
205
|
+
attributes=self._request_extractor.get_attributes_from_request(request_parameters),
|
|
206
|
+
context_attributes=get_attributes_from_context(),
|
|
207
|
+
extra_attributes=self._request_extractor.get_extra_attributes_from_request(request_parameters),
|
|
208
|
+
) as span:
|
|
209
|
+
try:
|
|
210
|
+
response = await wrapped(*args, **kwargs)
|
|
211
|
+
except Exception as exception:
|
|
212
|
+
span.record_exception(exception)
|
|
213
|
+
status = trace_api.Status(
|
|
214
|
+
status_code=trace_api.StatusCode.ERROR,
|
|
215
|
+
description=f"{type(exception).__name__}: {exception}",
|
|
216
|
+
)
|
|
217
|
+
span.finish_tracing(status=status)
|
|
218
|
+
raise
|
|
219
|
+
|
|
220
|
+
if is_streaming:
|
|
221
|
+
return self._handle_async_streaming_response(span, response, request_parameters)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
response_dict = _to_dict(response)
|
|
225
|
+
_finish_tracing(
|
|
226
|
+
status=trace_api.Status(status_code=trace_api.StatusCode.OK),
|
|
227
|
+
with_span=span,
|
|
228
|
+
attributes=self._response_extractor.get_attributes(response_dict),
|
|
229
|
+
extra_attributes=self._response_extractor.get_extra_attributes(
|
|
230
|
+
response_dict, request_parameters
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
except Exception:
|
|
234
|
+
logger.exception("Failed to finalize response")
|
|
235
|
+
span.finish_tracing()
|
|
236
|
+
return response
|
|
237
|
+
|
|
238
|
+
def _handle_async_streaming_response(
|
|
239
|
+
self, span: _WithSpan, response: Any, request_parameters: Dict[str, Any]
|
|
240
|
+
):
|
|
241
|
+
"""Handle async streaming response."""
|
|
242
|
+
stream_extractor = _StreamingChatCompletionResponseExtractor()
|
|
243
|
+
|
|
244
|
+
async def async_streaming_wrapper():
|
|
245
|
+
try:
|
|
246
|
+
async for chunk in response:
|
|
247
|
+
chunk_dict = _to_dict(chunk)
|
|
248
|
+
stream_extractor.process_chunk(chunk_dict)
|
|
249
|
+
yield chunk
|
|
250
|
+
|
|
251
|
+
except Exception as exception:
|
|
252
|
+
span.record_exception(exception)
|
|
253
|
+
status = trace_api.Status(
|
|
254
|
+
status_code=trace_api.StatusCode.ERROR,
|
|
255
|
+
description=f"{type(exception).__name__}: {exception}",
|
|
256
|
+
)
|
|
257
|
+
span.finish_tracing(status=status)
|
|
258
|
+
raise
|
|
259
|
+
else:
|
|
260
|
+
try:
|
|
261
|
+
_finish_tracing(
|
|
262
|
+
status=trace_api.Status(status_code=trace_api.StatusCode.OK),
|
|
263
|
+
with_span=span,
|
|
264
|
+
attributes=stream_extractor.get_attributes(),
|
|
265
|
+
extra_attributes=stream_extractor.get_extra_attributes(request_parameters),
|
|
266
|
+
)
|
|
267
|
+
except Exception:
|
|
268
|
+
logger.exception("Failed to finalize async streaming response")
|
|
269
|
+
span.finish_tracing()
|
|
270
|
+
finally:
|
|
271
|
+
if span._span.is_recording():
|
|
272
|
+
span.finish_tracing()
|
|
273
|
+
|
|
274
|
+
return async_streaming_wrapper()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0" # x-release-please-version
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: traceAI-minimax
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenTelemetry instrumentation for MiniMax - chat completions via OpenAI-compatible API
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: traceai,futureagi,opentelemetry,tracing,observability,llm,minimax
|
|
7
|
+
Author: Future AGI
|
|
8
|
+
Author-email: no-reply@futureagi.com
|
|
9
|
+
Requires-Python: >=3.10,<3.14
|
|
10
|
+
Classifier: Framework :: OpenTelemetry
|
|
11
|
+
Classifier: Framework :: OpenTelemetry :: Instrumentations
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Requires-Dist: fi-instrumentation-otel (>=0.1.11)
|
|
24
|
+
Requires-Dist: openai (>=1.0.0)
|
|
25
|
+
Project-URL: Changelog, https://github.com/future-agi/traceAI/releases
|
|
26
|
+
Project-URL: Documentation, https://docs.futureagi.com/docs/tracing
|
|
27
|
+
Project-URL: Homepage, https://futureagi.com
|
|
28
|
+
Project-URL: Issues, https://github.com/future-agi/traceAI/issues
|
|
29
|
+
Project-URL: Repository, https://github.com/future-agi/traceAI/tree/dev/python/frameworks/minimax
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# TraceAI MiniMax Instrumentation
|
|
33
|
+
|
|
34
|
+
OpenTelemetry instrumentation for [MiniMax](https://www.minimax.io/) - chat completions via the OpenAI-compatible API.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install traceai-minimax
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- Automatic tracing of MiniMax API calls via OpenAI SDK
|
|
45
|
+
- Support for MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, and MiniMax-M2.5-highspeed models
|
|
46
|
+
- Streaming response support
|
|
47
|
+
- Token usage tracking
|
|
48
|
+
- Function/tool calling support
|
|
49
|
+
- Full OpenTelemetry semantic conventions compliance
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Basic Setup
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from openai import OpenAI
|
|
57
|
+
from opentelemetry import trace
|
|
58
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
59
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
|
|
60
|
+
|
|
61
|
+
from traceai_minimax import MiniMaxInstrumentor
|
|
62
|
+
|
|
63
|
+
# Set up tracing
|
|
64
|
+
provider = TracerProvider()
|
|
65
|
+
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
|
|
66
|
+
trace.set_tracer_provider(provider)
|
|
67
|
+
|
|
68
|
+
# Instrument MiniMax
|
|
69
|
+
MiniMaxInstrumentor().instrument(tracer_provider=provider)
|
|
70
|
+
|
|
71
|
+
# Use MiniMax via OpenAI SDK
|
|
72
|
+
client = OpenAI(
|
|
73
|
+
api_key="your-minimax-api-key",
|
|
74
|
+
base_url="https://api.minimax.io/v1"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
response = client.chat.completions.create(
|
|
78
|
+
model="MiniMax-M2.7",
|
|
79
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
80
|
+
)
|
|
81
|
+
print(response.choices[0].message.content)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### MiniMax Chat
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from openai import OpenAI
|
|
88
|
+
|
|
89
|
+
client = OpenAI(
|
|
90
|
+
api_key="your-minimax-api-key",
|
|
91
|
+
base_url="https://api.minimax.io/v1"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Simple chat
|
|
95
|
+
response = client.chat.completions.create(
|
|
96
|
+
model="MiniMax-M2.7",
|
|
97
|
+
messages=[
|
|
98
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
|
99
|
+
{"role": "user", "content": "What is machine learning?"}
|
|
100
|
+
],
|
|
101
|
+
temperature=0.7,
|
|
102
|
+
max_tokens=1024
|
|
103
|
+
)
|
|
104
|
+
print(response.choices[0].message.content)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Streaming Responses
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from openai import OpenAI
|
|
111
|
+
|
|
112
|
+
client = OpenAI(
|
|
113
|
+
api_key="your-minimax-api-key",
|
|
114
|
+
base_url="https://api.minimax.io/v1"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Streaming chat
|
|
118
|
+
stream = client.chat.completions.create(
|
|
119
|
+
model="MiniMax-M2.7",
|
|
120
|
+
messages=[{"role": "user", "content": "Tell me a story"}],
|
|
121
|
+
stream=True
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
for chunk in stream:
|
|
125
|
+
if chunk.choices[0].delta.content:
|
|
126
|
+
print(chunk.choices[0].delta.content, end="", flush=True)
|
|
127
|
+
print()
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Function Calling / Tools
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from openai import OpenAI
|
|
134
|
+
import json
|
|
135
|
+
|
|
136
|
+
client = OpenAI(
|
|
137
|
+
api_key="your-minimax-api-key",
|
|
138
|
+
base_url="https://api.minimax.io/v1"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
tools = [
|
|
142
|
+
{
|
|
143
|
+
"type": "function",
|
|
144
|
+
"function": {
|
|
145
|
+
"name": "get_weather",
|
|
146
|
+
"description": "Get the current weather for a location",
|
|
147
|
+
"parameters": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"location": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"description": "The city name"
|
|
153
|
+
},
|
|
154
|
+
"unit": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"enum": ["celsius", "fahrenheit"],
|
|
157
|
+
"description": "Temperature unit"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"required": ["location"]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
response = client.chat.completions.create(
|
|
167
|
+
model="MiniMax-M2.7",
|
|
168
|
+
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
|
|
169
|
+
tools=tools,
|
|
170
|
+
tool_choice="auto"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
message = response.choices[0].message
|
|
174
|
+
if message.tool_calls:
|
|
175
|
+
for tool_call in message.tool_calls:
|
|
176
|
+
print(f"Function: {tool_call.function.name}")
|
|
177
|
+
print(f"Arguments: {tool_call.function.arguments}")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Async Usage
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import asyncio
|
|
184
|
+
from openai import AsyncOpenAI
|
|
185
|
+
|
|
186
|
+
async def main():
|
|
187
|
+
client = AsyncOpenAI(
|
|
188
|
+
api_key="your-minimax-api-key",
|
|
189
|
+
base_url="https://api.minimax.io/v1"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
response = await client.chat.completions.create(
|
|
193
|
+
model="MiniMax-M2.7",
|
|
194
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
195
|
+
)
|
|
196
|
+
print(response.choices[0].message.content)
|
|
197
|
+
|
|
198
|
+
asyncio.run(main())
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### JSON Mode
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from openai import OpenAI
|
|
205
|
+
|
|
206
|
+
client = OpenAI(
|
|
207
|
+
api_key="your-minimax-api-key",
|
|
208
|
+
base_url="https://api.minimax.io/v1"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
response = client.chat.completions.create(
|
|
212
|
+
model="MiniMax-M2.7",
|
|
213
|
+
messages=[
|
|
214
|
+
{"role": "system", "content": "Output valid JSON only."},
|
|
215
|
+
{"role": "user", "content": "List 3 programming languages with their main use cases"}
|
|
216
|
+
],
|
|
217
|
+
response_format={"type": "json_object"}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
import json
|
|
221
|
+
data = json.loads(response.choices[0].message.content)
|
|
222
|
+
print(data)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Configuration Options
|
|
226
|
+
|
|
227
|
+
### TraceConfig
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from fi_instrumentation import TraceConfig
|
|
231
|
+
from traceai_minimax import MiniMaxInstrumentor
|
|
232
|
+
|
|
233
|
+
config = TraceConfig(
|
|
234
|
+
hide_inputs=False,
|
|
235
|
+
hide_outputs=False,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
MiniMaxInstrumentor().instrument(
|
|
239
|
+
tracer_provider=provider,
|
|
240
|
+
config=config
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Captured Attributes
|
|
245
|
+
|
|
246
|
+
### Common Attributes
|
|
247
|
+
|
|
248
|
+
| Attribute | Description |
|
|
249
|
+
|-----------|-------------|
|
|
250
|
+
| `fi.span.kind` | "LLM" |
|
|
251
|
+
| `llm.system` | "minimax" |
|
|
252
|
+
| `llm.provider` | "minimax" |
|
|
253
|
+
| `llm.model` | Model name (MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, MiniMax-M2.5-highspeed) |
|
|
254
|
+
| `llm.token_count.prompt` | Input token count |
|
|
255
|
+
| `llm.token_count.completion` | Output token count |
|
|
256
|
+
| `llm.token_count.total` | Total token count |
|
|
257
|
+
|
|
258
|
+
### MiniMax-Specific Attributes
|
|
259
|
+
|
|
260
|
+
| Attribute | Description |
|
|
261
|
+
|-----------|-------------|
|
|
262
|
+
| `minimax.response_id` | Unique response ID |
|
|
263
|
+
| `minimax.finish_reason` | Response finish reason (stop, tool_calls, length) |
|
|
264
|
+
| `minimax.tool_calls_count` | Number of tool calls |
|
|
265
|
+
| `minimax.tools_count` | Number of tools provided |
|
|
266
|
+
|
|
267
|
+
## Available Models
|
|
268
|
+
|
|
269
|
+
| Model | Description |
|
|
270
|
+
|-------|-------------|
|
|
271
|
+
| `MiniMax-M2.7` | Latest flagship model with enhanced reasoning and coding |
|
|
272
|
+
| `MiniMax-M2.7-highspeed` | High-speed version of M2.7 for low-latency scenarios |
|
|
273
|
+
| `MiniMax-M2.5` | General-purpose model with 204K context window |
|
|
274
|
+
| `MiniMax-M2.5-highspeed` | Faster inference variant with 204K context window |
|
|
275
|
+
|
|
276
|
+
## Important Notes
|
|
277
|
+
|
|
278
|
+
1. **OpenAI SDK Required**: MiniMax uses the OpenAI-compatible API, so you need the `openai` package installed.
|
|
279
|
+
|
|
280
|
+
2. **Base URL**: Always set `base_url="https://api.minimax.io/v1"` when creating the client.
|
|
281
|
+
|
|
282
|
+
3. **API Key**: Get your API key from the [MiniMax Platform](https://platform.minimax.chat/).
|
|
283
|
+
|
|
284
|
+
4. **Selective Instrumentation**: The instrumentor only traces calls to MiniMax's API. Regular OpenAI API calls are not affected.
|
|
285
|
+
|
|
286
|
+
5. **Temperature**: MiniMax requires temperature to be in the range (0.0, 1.0]. A value of exactly 0 is not accepted.
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
Apache-2.0
|
|
291
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
traceai_minimax/__init__.py,sha256=N5ZpvSFDFzU136BFCrsh-BXcZrufGDxOYVOKCxTSVeE,3910
|
|
2
|
+
traceai_minimax/_request_attributes_extractor.py,sha256=fp5k7KK5k4F133NQAnRrOi4KseBxzuWpJNeyWFJCtzQ,5141
|
|
3
|
+
traceai_minimax/_response_attributes_extractor.py,sha256=zjrkkLGCp-LPjcYbCSQZ165Z4xeADqvh9O5yS7q_C1s,7550
|
|
4
|
+
traceai_minimax/_utils.py,sha256=bu8GIPQyj_8xJqWUbsVmx45DphScGPcbLduncM7Sv7g,2985
|
|
5
|
+
traceai_minimax/_with_span.py,sha256=SPzQfr85aprbb5Kdajk81Bbp71XRIF85DrjM4-RQwp4,1399
|
|
6
|
+
traceai_minimax/_wrappers.py,sha256=-3F_W_WbyfdX2EKlJJYEpsaa_PH65J1ZD76tgVGbdGw,10882
|
|
7
|
+
traceai_minimax/version.py,sha256=IOEzhCbX916JIkvZr03GFY4bl9YqSkfjdqLO4qJ2FOs,50
|
|
8
|
+
traceai_minimax-0.1.0.dist-info/METADATA,sha256=RGZiocCInopTOmr_ZD567kCtwiF0gkp16cZ_w64rwY0,8082
|
|
9
|
+
traceai_minimax-0.1.0.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
10
|
+
traceai_minimax-0.1.0.dist-info/RECORD,,
|