Skip to content

Commit 29eeba0

Browse files
committed
Add Litellm Instrument Unit Test
1 parent f74acd0 commit 29eeba0

9 files changed

Lines changed: 1572 additions & 0 deletions

File tree

instrumentation-loongsuite/loongsuite-instrumentation-litellm/src/opentelemetry/instrumentation/litellm/_embedding_wrapper.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,31 @@ def __call__(self, *args, **kwargs):
9494
response.usage, "total_tokens", None
9595
)
9696

97+
# Extract embedding dimension count
98+
if (
99+
hasattr(response, "data")
100+
and response.data
101+
and len(response.data) > 0
102+
):
103+
try:
104+
first_embedding = response.data[0]
105+
# Handle dict response
106+
if (
107+
isinstance(first_embedding, dict)
108+
and "embedding" in first_embedding
109+
):
110+
embedding_vector = first_embedding["embedding"]
111+
if isinstance(embedding_vector, list):
112+
invocation.dimension_count = len(embedding_vector)
113+
# Handle object response
114+
elif hasattr(first_embedding, "embedding"):
115+
embedding_vector = first_embedding.embedding
116+
if isinstance(embedding_vector, list):
117+
invocation.dimension_count = len(embedding_vector)
118+
except (IndexError, AttributeError, KeyError, TypeError):
119+
# If we can't extract dimension, just skip it
120+
pass
121+
97122
# End Embedding invocation successfully
98123
self._handler.stop_embedding(invocation)
99124

@@ -170,6 +195,31 @@ async def __call__(self, *args, **kwargs):
170195
response.usage, "total_tokens", None
171196
)
172197

198+
# Extract embedding dimension count
199+
if (
200+
hasattr(response, "data")
201+
and response.data
202+
and len(response.data) > 0
203+
):
204+
try:
205+
first_embedding = response.data[0]
206+
# Handle dict response
207+
if (
208+
isinstance(first_embedding, dict)
209+
and "embedding" in first_embedding
210+
):
211+
embedding_vector = first_embedding["embedding"]
212+
if isinstance(embedding_vector, list):
213+
invocation.dimension_count = len(embedding_vector)
214+
# Handle object response
215+
elif hasattr(first_embedding, "embedding"):
216+
embedding_vector = first_embedding.embedding
217+
if isinstance(embedding_vector, list):
218+
invocation.dimension_count = len(embedding_vector)
219+
except (IndexError, AttributeError, KeyError, TypeError):
220+
# If we can't extract dimension, just skip it
221+
pass
222+
173223
# End Embedding invocation successfully
174224
self._handler.stop_embedding(invocation)
175225

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import asyncio
2+
import os
3+
from unittest.mock import patch
4+
5+
import litellm
6+
7+
from opentelemetry.instrumentation.litellm import LiteLLMInstrumentor
8+
from opentelemetry.test.test_base import TestBase
9+
from opentelemetry.util.genai.types import ContentCapturingMode
10+
11+
12+
class TestEmbedding(TestBase):
13+
"""
14+
Test embedding calls with LiteLLM.
15+
"""
16+
17+
def setUp(self):
18+
super().setUp()
19+
# Set up environment variables for testing
20+
os.environ["OPENAI_API_KEY"] = os.environ.get(
21+
"OPENAI_API_KEY", "sk-..."
22+
)
23+
os.environ["DASHSCOPE_API_KEY"] = os.environ.get(
24+
"DASHSCOPE_API_KEY", "sk-..."
25+
)
26+
os.environ["OPENAI_API_BASE"] = (
27+
"https://dashscope.aliyuncs.com/compatible-mode/v1"
28+
)
29+
os.environ["DASHSCOPE_API_BASE"] = (
30+
"https://dashscope.aliyuncs.com/compatible-mode/v1"
31+
)
32+
33+
# Force experiment mode for content capture
34+
self.patch_experimental = patch(
35+
"opentelemetry.util.genai.span_utils.is_experimental_mode",
36+
return_value=True,
37+
)
38+
self.patch_content_mode = patch(
39+
"opentelemetry.util.genai.span_utils.get_content_capturing_mode",
40+
return_value=ContentCapturingMode.SPAN_ONLY,
41+
)
42+
43+
self.patch_experimental.start()
44+
self.patch_content_mode.start()
45+
46+
# Instrument LiteLLM
47+
LiteLLMInstrumentor().instrument(
48+
tracer_provider=self.tracer_provider,
49+
)
50+
51+
def tearDown(self):
52+
super().tearDown()
53+
# Uninstrument to avoid affecting other tests
54+
LiteLLMInstrumentor().uninstrument()
55+
self.patch_experimental.stop()
56+
self.patch_content_mode.stop()
57+
58+
def test_sync_embedding_single_text(self):
59+
"""
60+
Test synchronous embedding with single text input.
61+
"""
62+
63+
# Business demo: Single text embedding
64+
response = litellm.embedding(
65+
model="openai/text-embedding-v1",
66+
api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
67+
input="The quick brown fox jumps over the lazy dog",
68+
encoding_format="float",
69+
)
70+
71+
# Verify the response
72+
self.assertIsNotNone(response)
73+
self.assertTrue(hasattr(response, "data"))
74+
self.assertGreater(len(response.data), 0)
75+
76+
# Verify embedding is a list of numbers
77+
embedding = response.data[0].get("embedding")
78+
self.assertIsInstance(embedding, list)
79+
self.assertGreater(len(embedding), 0)
80+
81+
# Get spans
82+
spans = self.get_finished_spans()
83+
self.assertEqual(
84+
len(spans), 1, "Expected exactly one span for embedding call"
85+
)
86+
span = spans[0]
87+
88+
# Verify span kind
89+
self.assertEqual(
90+
span.attributes.get("gen_ai.span.kind"),
91+
"EMBEDDING",
92+
"Span kind should be EMBEDDING",
93+
)
94+
95+
# Verify model
96+
self.assertIn("gen_ai.request.model", span.attributes)
97+
self.assertEqual(
98+
span.attributes.get("gen_ai.request.model"), "text-embedding-v1"
99+
)
100+
101+
# Verify token usage (required for embedding)
102+
self.assertIn("gen_ai.usage.input_tokens", span.attributes)
103+
self.assertGreater(span.attributes.get("gen_ai.usage.input_tokens"), 0)
104+
105+
# Verify embedding dimension count
106+
self.assertIn("gen_ai.embeddings.dimension.count", span.attributes)
107+
dimension = span.attributes.get("gen_ai.embeddings.dimension.count")
108+
self.assertEqual(dimension, len(embedding))
109+
self.assertGreater(dimension, 0)
110+
111+
def test_sync_embedding_multiple_texts(self):
112+
"""
113+
Test synchronous embedding with multiple text inputs.
114+
"""
115+
116+
# Business demo: Batch embedding
117+
texts = [
118+
"Hello, world!",
119+
"Artificial intelligence is fascinating.",
120+
"LiteLLM makes LLM integration easy.",
121+
]
122+
123+
response = litellm.embedding(
124+
model="openai/text-embedding-v1",
125+
api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
126+
input=texts,
127+
encoding_format="float",
128+
)
129+
130+
# Verify the response
131+
self.assertIsNotNone(response)
132+
self.assertTrue(hasattr(response, "data"))
133+
self.assertEqual(
134+
len(response.data),
135+
len(texts),
136+
"Should have embedding for each text",
137+
)
138+
139+
# Verify each embedding
140+
self.assertIsInstance(response.data[0].get("embedding"), list)
141+
self.assertGreater(len(response.data[0].get("embedding")), 0)
142+
143+
spans = self.get_finished_spans()
144+
self.assertEqual(len(spans), 1)
145+
span = spans[0]
146+
147+
self.assertEqual(span.attributes.get("gen_ai.span.kind"), "EMBEDDING")
148+
self.assertGreater(span.attributes.get("gen_ai.usage.input_tokens"), 0)
149+
150+
def test_async_embedding(self):
151+
"""
152+
Test asynchronous embedding call.
153+
"""
154+
155+
async def run_async_embedding():
156+
response = await litellm.aembedding(
157+
model="openai/text-embedding-v1",
158+
input="Async test",
159+
api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
160+
encoding_format="float",
161+
)
162+
return response
163+
164+
response = asyncio.run(run_async_embedding())
165+
166+
# Verify response
167+
self.assertIsNotNone(response)
168+
self.assertTrue(hasattr(response, "data"))
169+
self.assertGreater(len(response.data), 0)
170+
171+
spans = self.get_finished_spans()
172+
self.assertEqual(len(spans), 1)
173+
span = spans[0]
174+
175+
self.assertEqual(span.attributes.get("gen_ai.span.kind"), "EMBEDDING")
176+
self.assertIn("gen_ai.request.model", span.attributes)
177+
self.assertIn("gen_ai.usage.input_tokens", span.attributes)
178+
self.assertIn("gen_ai.embeddings.dimension.count", span.attributes)
179+
180+
def test_embedding_with_different_models(self):
181+
"""
182+
Test embedding with different model providers.
183+
"""
184+
185+
response = litellm.embedding(
186+
model="openai/text-embedding-v1",
187+
api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
188+
input="Testing different embedding models",
189+
encoding_format="float",
190+
)
191+
192+
self.assertIsNotNone(response)
193+
spans = self.get_finished_spans()
194+
self.assertEqual(len(spans), 1)
195+
196+
span = spans[0]
197+
self.assertIn("gen_ai.request.model", span.attributes)
198+
self.assertEqual(
199+
span.attributes.get("gen_ai.request.model"), "text-embedding-v1"
200+
)
201+
202+
def test_embedding_empty_input(self):
203+
"""
204+
Test embedding with edge case inputs.
205+
"""
206+
207+
response = litellm.embedding(
208+
model="openai/text-embedding-v1",
209+
api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
210+
input="Hi",
211+
encoding_format="float",
212+
)
213+
214+
# Verify response
215+
self.assertIsNotNone(response)
216+
self.assertTrue(hasattr(response, "data"))
217+
self.assertGreater(len(response.data), 0)
218+
219+
spans = self.get_finished_spans()
220+
self.assertEqual(len(spans), 1)
221+
222+
span = spans[0]
223+
self.assertEqual(span.attributes.get("gen_ai.span.kind"), "EMBEDDING")
224+
self.assertIn("gen_ai.usage.input_tokens", span.attributes)

0 commit comments

Comments
 (0)