85742ab165
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
567 lines
23 KiB
Python
567 lines
23 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Test the LLMProxy class. Still under development.
|
|
|
|
General TODOs:
|
|
|
|
1. Add tests for retries
|
|
2. Add tests for timeout
|
|
3. Add tests for multiple models in model list
|
|
4. Add tests for multi-modal models
|
|
|
|
There are some specific TODOs for each test function.
|
|
"""
|
|
|
|
import ast
|
|
import asyncio
|
|
import json
|
|
from typing import Any, Dict, List, Sequence, Type, Union, cast
|
|
|
|
import anthropic
|
|
import openai
|
|
import pytest
|
|
from litellm.integrations.custom_logger import CustomLogger
|
|
from portpicker import pick_unused_port
|
|
|
|
from agentlightning import LlmProxyTraceToTriplet
|
|
from agentlightning.llm_proxy import LLMProxy, _reset_litellm_logging_worker # pyright: ignore[reportPrivateUsage]
|
|
from agentlightning.store import LightningStore, LightningStoreServer, LightningStoreThreaded
|
|
from agentlightning.store.memory import InMemoryLightningStore
|
|
from agentlightning.types import LLM, Span
|
|
|
|
from ..common.tracer import clear_tracer_provider
|
|
from ..common.vllm import VLLM_VERSION, RemoteOpenAIServer
|
|
|
|
pytestmark = [pytest.mark.gpu, pytest.mark.llmproxy]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def qwen25_model():
|
|
with RemoteOpenAIServer(
|
|
model="Qwen/Qwen2.5-0.5B-Instruct",
|
|
vllm_serve_args=[
|
|
"--gpu-memory-utilization",
|
|
"0.7",
|
|
"--enable-auto-tool-choice",
|
|
"--tool-call-parser",
|
|
"hermes",
|
|
"--port",
|
|
str(pick_unused_port()),
|
|
],
|
|
) as server:
|
|
yield server
|
|
|
|
|
|
def test_qwen25_model_sanity(qwen25_model: RemoteOpenAIServer):
|
|
client = qwen25_model.get_client()
|
|
response = client.chat.completions.create(
|
|
model="Qwen/Qwen2.5-0.5B-Instruct",
|
|
messages=[{"role": "user", "content": "Hello, world!"}],
|
|
stream=False,
|
|
)
|
|
assert response.choices[0].message.content is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("otlp_enabled", [True, False])
|
|
async def test_basic_integration(qwen25_model: RemoteOpenAIServer, otlp_enabled: bool):
|
|
clear_tracer_provider()
|
|
inmemory_store = InMemoryLightningStore()
|
|
if otlp_enabled:
|
|
store = LightningStoreServer(store=inmemory_store, host="127.0.0.1", port=pick_unused_port())
|
|
await store.start()
|
|
else:
|
|
store = LightningStoreThreaded(inmemory_store)
|
|
proxy = LLMProxy(
|
|
port=pick_unused_port(),
|
|
model_list=[
|
|
{
|
|
"model_name": "gpt-4o-arbitrary",
|
|
"litellm_params": {
|
|
"model": "hosted_vllm/" + qwen25_model.model,
|
|
"api_base": qwen25_model.url_for("v1"),
|
|
},
|
|
}
|
|
],
|
|
store=store,
|
|
launch_mode="thread" if not otlp_enabled else "mp",
|
|
)
|
|
|
|
rollout = await store.start_rollout(None)
|
|
|
|
await proxy.start()
|
|
|
|
resource = proxy.as_resource(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
|
|
client = openai.OpenAI(base_url=resource.endpoint, api_key="token-abc123")
|
|
response = client.chat.completions.create(
|
|
model="gpt-4o-arbitrary",
|
|
messages=[{"role": "user", "content": "Repeat after me: Hello, world!"}],
|
|
stream=False,
|
|
)
|
|
assert response.choices[0].message.content is not None
|
|
assert "hello, world" in response.choices[0].message.content.lower()
|
|
|
|
await proxy.stop()
|
|
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
|
|
if isinstance(store, LightningStoreServer):
|
|
await store.stop()
|
|
|
|
# Verify all spans have correct rollout_id, attempt_id, and sequence_id
|
|
assert len(spans) > 0, "Should have captured spans"
|
|
for span in spans:
|
|
assert span.rollout_id == rollout.rollout_id, f"Span {span.name} has incorrect rollout_id"
|
|
assert span.attempt_id == rollout.attempt.attempt_id, f"Span {span.name} has incorrect attempt_id"
|
|
assert span.sequence_id == 1, f"Span {span.name} has incorrect sequence_id"
|
|
|
|
# Verify start time and end time
|
|
# TODO: Remove this when this PR is merged: https://github.com/BerriAI/litellm/pull/16558
|
|
print(f">>> Span: {span.name}")
|
|
print(f">>> Start time: {span.start_time}")
|
|
print(f">>> End time: {span.end_time}")
|
|
print(f">>> Attributes: {span.attributes.keys()}")
|
|
assert span.start_time is not None, f"Span {span.name} has no start time"
|
|
assert span.end_time is not None, f"Span {span.name} has no end time"
|
|
|
|
# Find the raw_gen_ai_request span and verify token IDs
|
|
raw_gen_ai_spans = [s for s in spans if s.name == "raw_gen_ai_request"]
|
|
assert len(raw_gen_ai_spans) == 1, f"Expected 1 raw_gen_ai_request span, found {len(raw_gen_ai_spans)}"
|
|
raw_span = raw_gen_ai_spans[0]
|
|
|
|
# Verify prompt_token_ids is present and non-empty
|
|
assert (
|
|
"llm.hosted_vllm.prompt_token_ids" in raw_span.attributes
|
|
), "prompt_token_ids not found in raw_gen_ai_request span"
|
|
prompt_token_ids: list[int] = ast.literal_eval(raw_span.attributes["llm.hosted_vllm.prompt_token_ids"]) # type: ignore
|
|
assert isinstance(prompt_token_ids, list), "prompt_token_ids should be a list"
|
|
assert len(prompt_token_ids) > 0, "prompt_token_ids should not be empty"
|
|
assert all(isinstance(tid, int) for tid in prompt_token_ids), "All prompt token IDs should be integers"
|
|
|
|
# Verify response token_ids is present in choices
|
|
assert "llm.hosted_vllm.choices" in raw_span.attributes, "choices not found in raw_gen_ai_request span"
|
|
choices: list[dict[str, Any]] = ast.literal_eval(raw_span.attributes["llm.hosted_vllm.choices"]) # type: ignore
|
|
assert len(choices) > 0, "Should have at least one choice"
|
|
if VLLM_VERSION >= (0, 10, 2):
|
|
assert "token_ids" in choices[0], "token_ids not found in choice"
|
|
response_token_ids: list[int] = choices[0]["token_ids"]
|
|
else:
|
|
assert (
|
|
"llm.hosted_vllm.response_token_ids" in raw_span.attributes
|
|
), "response_token_ids not found in raw_gen_ai_request span"
|
|
response_token_ids_list: list[list[int]] = ast.literal_eval(raw_span.attributes["llm.hosted_vllm.response_token_ids"]) # type: ignore
|
|
assert isinstance(response_token_ids_list, list), "response_token_ids_list should be a list"
|
|
assert len(response_token_ids_list) > 0, "response_token_ids_list should not be empty"
|
|
assert all(
|
|
isinstance(tid_list, list) for tid_list in response_token_ids_list
|
|
), "All response token IDs should be lists"
|
|
assert all(
|
|
isinstance(tid, int) for tid_list in response_token_ids_list for tid in tid_list
|
|
), "All response token IDs should be integers"
|
|
response_token_ids = response_token_ids_list[0]
|
|
assert isinstance(response_token_ids, list), "response token_ids should be a list"
|
|
assert len(response_token_ids) > 0, "response token_ids should not be empty"
|
|
assert all(isinstance(tid, int) for tid in response_token_ids), "All response token IDs should be integers"
|
|
|
|
# Find the litellm_request span and verify gen_ai prompts/completions
|
|
litellm_spans = [s for s in spans if s.name == "litellm_request"]
|
|
assert len(litellm_spans) == 1, f"Expected 1 litellm_request span, found {len(litellm_spans)}"
|
|
litellm_span = litellm_spans[0]
|
|
|
|
# Verify gen_ai.prompt attributes
|
|
assert "gen_ai.prompt.0.role" in litellm_span.attributes, "gen_ai.prompt.0.role not found"
|
|
assert litellm_span.attributes["gen_ai.prompt.0.role"] == "user", "Expected user role in prompt"
|
|
assert "gen_ai.prompt.0.content" in litellm_span.attributes, "gen_ai.prompt.0.content not found"
|
|
assert litellm_span.attributes["gen_ai.prompt.0.content"] == "Repeat after me: Hello, world!"
|
|
|
|
# Verify gen_ai.completion attributes
|
|
assert "gen_ai.completion.0.role" in litellm_span.attributes, "gen_ai.completion.0.role not found"
|
|
assert litellm_span.attributes["gen_ai.completion.0.role"] == "assistant", "Expected assistant role in completion"
|
|
assert "gen_ai.completion.0.content" in litellm_span.attributes, "gen_ai.completion.0.content not found"
|
|
assert "gen_ai.completion.0.finish_reason" in litellm_span.attributes, "gen_ai.completion.0.finish_reason not found"
|
|
|
|
|
|
async def _make_proxy_and_store(
|
|
qwen25_model: RemoteOpenAIServer,
|
|
*,
|
|
retries: int = 0,
|
|
gunicorn: bool = False,
|
|
callbacks: List[Union[Type[CustomLogger], str]] | None = None,
|
|
otlp_enabled: bool = False,
|
|
):
|
|
clear_tracer_provider()
|
|
_reset_litellm_logging_worker() # type: ignore
|
|
store = InMemoryLightningStore()
|
|
if otlp_enabled:
|
|
store = LightningStoreServer(store=store, host="127.0.0.1", port=pick_unused_port())
|
|
# When the server is forked into subprocess, it automatically becomes a client of the store
|
|
await store.start()
|
|
else:
|
|
# Backward compatibility with legacy thread + non-otlp mode
|
|
store = LightningStoreThreaded(store)
|
|
proxy = LLMProxy(
|
|
model_list=[
|
|
{
|
|
"model_name": "gpt-4o-arbitrary",
|
|
"litellm_params": {
|
|
"model": "hosted_vllm/" + qwen25_model.model,
|
|
"api_base": qwen25_model.url_for("v1"),
|
|
},
|
|
}
|
|
],
|
|
launch_mode="thread" if not otlp_enabled else "mp",
|
|
port=pick_unused_port(),
|
|
num_workers=4 if gunicorn else 1,
|
|
store=store,
|
|
num_retries=retries,
|
|
callbacks=callbacks,
|
|
)
|
|
await proxy.start()
|
|
return proxy, store
|
|
|
|
|
|
async def _new_resource(proxy: LLMProxy, store: LightningStore):
|
|
rollout = await store.start_rollout(None)
|
|
return proxy.as_resource(rollout.rollout_id, rollout.attempt.attempt_id), rollout
|
|
|
|
|
|
def _get_client_for_resource(resource: LLM):
|
|
return openai.OpenAI(base_url=resource.endpoint, api_key="token-abc123", timeout=120, max_retries=0)
|
|
|
|
|
|
def _get_async_client_for_resource(resource: LLM):
|
|
return openai.AsyncOpenAI(base_url=resource.endpoint, api_key="token-abc123", timeout=120, max_retries=0)
|
|
|
|
|
|
def _find_span(spans: Sequence[Span], name: str):
|
|
return [s for s in spans if s.name == name]
|
|
|
|
|
|
def _attr(s: Span, key: str, default: Any = None): # type: ignore
|
|
return s.attributes.get(key, default)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("otlp_enabled", [True, False])
|
|
async def test_multiple_requests_one_attempt(qwen25_model: RemoteOpenAIServer, otlp_enabled: bool):
|
|
proxy, store = await _make_proxy_and_store(qwen25_model, otlp_enabled=otlp_enabled)
|
|
try:
|
|
resource, rollout = await _new_resource(proxy, store)
|
|
client = _get_client_for_resource(resource)
|
|
|
|
for i in range(3):
|
|
r = client.chat.completions.create(
|
|
model="gpt-4o-arbitrary",
|
|
messages=[{"role": "user", "content": f"Say ping {i}"}],
|
|
stream=False,
|
|
)
|
|
assert r.choices[0].message.content
|
|
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
assert len(spans) > 0
|
|
# Different requests have different sequence_ids
|
|
assert {s.sequence_id for s in spans} == {1, 2, 3}
|
|
# At least 3 requests recorded
|
|
assert len(_find_span(spans, "raw_gen_ai_request")) == 3
|
|
# TODO: Check response contents and token ids for the 3 requests respectively
|
|
finally:
|
|
await proxy.stop()
|
|
if isinstance(store, LightningStoreServer):
|
|
await store.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mode", ["gunicorn", "thread", "uvicorn"])
|
|
async def test_ten_concurrent_requests(qwen25_model: RemoteOpenAIServer, mode: str):
|
|
proxy, store = await _make_proxy_and_store(qwen25_model, gunicorn=mode == "gunicorn", otlp_enabled=mode != "thread")
|
|
try:
|
|
resource, rollout = await _new_resource(proxy, store)
|
|
aclient = _get_async_client_for_resource(resource)
|
|
|
|
async def _one(i: int):
|
|
r = await aclient.chat.completions.create(
|
|
model="gpt-4o-arbitrary",
|
|
messages=[{"role": "user", "content": f"Return #{i}"}],
|
|
stream=False,
|
|
)
|
|
return r.choices[0].message.content
|
|
|
|
outs = await asyncio.gather(*[_one(i) for i in range(10)])
|
|
assert len([o for o in outs if o]) == 10
|
|
await asyncio.sleep(1.0) # Allow some extra time for the spans to be recorded
|
|
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
assert len(_find_span(spans, "raw_gen_ai_request")) == 10
|
|
assert {s.sequence_id for s in spans} == {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
|
# TODO: Check whether the sequence ids get mixed up or not
|
|
finally:
|
|
await proxy.stop()
|
|
if isinstance(store, LightningStoreServer):
|
|
await store.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("otlp_enabled", [True, False])
|
|
async def test_anthropic_client_compat(qwen25_model: RemoteOpenAIServer, otlp_enabled: bool):
|
|
# litellm proxy accepts Anthropic schema and forwards to OpenAI backend
|
|
proxy, store = await _make_proxy_and_store(qwen25_model, otlp_enabled=otlp_enabled)
|
|
try:
|
|
resource, rollout = await _new_resource(proxy, store)
|
|
|
|
a = anthropic.Anthropic(base_url=resource.endpoint, api_key="token-abc123", timeout=120)
|
|
msg = a.messages.create(
|
|
model="gpt-4o-arbitrary",
|
|
max_tokens=64,
|
|
messages=[{"role": "user", "content": "Respond with the word: OK"}],
|
|
)
|
|
# Anthropic SDK returns content list
|
|
txt = "".join([b.text for b in msg.content if b.type == "text"])
|
|
assert "OK" in txt.upper()
|
|
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
assert len(spans) > 0
|
|
finally:
|
|
await proxy.stop()
|
|
if isinstance(store, LightningStoreServer):
|
|
await store.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("otlp_enabled", [True, False])
|
|
async def test_tool_call_roundtrip(qwen25_model: RemoteOpenAIServer, otlp_enabled: bool):
|
|
proxy, store = await _make_proxy_and_store(qwen25_model, otlp_enabled=otlp_enabled)
|
|
try:
|
|
resource, rollout = await _new_resource(proxy, store)
|
|
client = _get_client_for_resource(resource)
|
|
|
|
tools = [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "echo",
|
|
"description": "Echo a string",
|
|
"parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]},
|
|
},
|
|
}
|
|
]
|
|
|
|
r1 = client.chat.completions.create(
|
|
model="gpt-4o-arbitrary",
|
|
messages=[{"role": "user", "content": "Call the echo tool with text=hello"}],
|
|
tools=cast(Any, tools),
|
|
tool_choice="auto",
|
|
stream=False,
|
|
)
|
|
# If the small model does not tool-call, skip gracefully
|
|
tool_calls = r1.choices[0].message.tool_calls or []
|
|
if not tool_calls:
|
|
pytest.skip("model did not emit tool calls in this environment")
|
|
|
|
call = tool_calls[0]
|
|
assert call.type == "function"
|
|
assert call.function and call.function.name == "echo"
|
|
args = json.loads(call.function.arguments)
|
|
assert "text" in args
|
|
|
|
r2 = client.chat.completions.create(
|
|
model="gpt-4o-arbitrary",
|
|
messages=cast(
|
|
Any,
|
|
[
|
|
{"role": "user", "content": "Call the echo tool with text=hello"},
|
|
{
|
|
"role": "assistant",
|
|
"tool_calls": [
|
|
{
|
|
"id": call.id,
|
|
"type": "function",
|
|
"function": {"name": "echo", "arguments": call.function.arguments},
|
|
}
|
|
],
|
|
},
|
|
{"role": "tool", "tool_call_id": call.id, "name": "echo", "content": args["text"]},
|
|
],
|
|
),
|
|
stream=False,
|
|
)
|
|
assert args["text"] in (r2.choices[0].message.content or "")
|
|
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
assert len(_find_span(spans, "litellm_request")) == 2
|
|
assert len(_find_span(spans, "raw_gen_ai_request")) == 2
|
|
|
|
# TODO: Check response contents and token ids for the 2 requests respectively
|
|
finally:
|
|
await proxy.stop()
|
|
if isinstance(store, LightningStoreServer):
|
|
await store.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("otlp_enabled", [True, False])
|
|
async def test_streaming_chunks(qwen25_model: RemoteOpenAIServer, otlp_enabled: bool):
|
|
proxy, store = await _make_proxy_and_store(qwen25_model, otlp_enabled=otlp_enabled)
|
|
try:
|
|
resource, rollout = await _new_resource(proxy, store)
|
|
client = _get_client_for_resource(resource)
|
|
|
|
stream = client.chat.completions.create(
|
|
model="gpt-4o-arbitrary",
|
|
messages=[{"role": "user", "content": "Say the word 'apple'"}],
|
|
stream=True,
|
|
)
|
|
collected: list[str] = []
|
|
for evt in stream:
|
|
print(f">>> Event: {evt}")
|
|
for c in evt.choices:
|
|
if c.delta and getattr(c.delta, "content", None):
|
|
assert isinstance(c.delta.content, str)
|
|
collected.append(c.delta.content)
|
|
# Sometimes the model responds with "hello" instead of "apple"
|
|
assert "apple" in "".join(collected).lower() or "hello" in "".join(collected).lower()
|
|
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
assert len(spans) > 0
|
|
for span in spans:
|
|
print(f">>> Span {span.name}: {span.attributes}")
|
|
if span.name == "raw_gen_ai_request":
|
|
assert "llm.hosted_vllm.prompt_token_ids" in span.attributes
|
|
assert "llm.hosted_vllm.choices" in span.attributes
|
|
if span.name == "litellm_request":
|
|
assert "gen_ai.completion.0.content" in span.attributes
|
|
finally:
|
|
await proxy.stop()
|
|
if isinstance(store, LightningStoreServer):
|
|
await store.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("otlp_enabled", [True, False])
|
|
async def test_anthropic_token_ids(qwen25_model: RemoteOpenAIServer, otlp_enabled: bool):
|
|
proxy, store = await _make_proxy_and_store(qwen25_model, otlp_enabled=otlp_enabled)
|
|
try:
|
|
resource, rollout = await _new_resource(proxy, store)
|
|
adapter = LlmProxyTraceToTriplet()
|
|
client = anthropic.Anthropic(base_url=resource.endpoint, api_key="token-abc123", timeout=120)
|
|
|
|
# non-stream
|
|
response = client.messages.create(
|
|
model="gpt-4o-arbitrary",
|
|
max_tokens=64,
|
|
messages=[{"role": "user", "content": "Say the word: banana"}],
|
|
)
|
|
|
|
txt = "".join([b.text for b in response.content if b.type == "text"])
|
|
assert "banana" in txt.lower(), f"Response does not contain 'banana': {txt}"
|
|
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
for i, span in enumerate(spans):
|
|
print(f">>> Span {i}: {span.name}, attributes: {span.attributes}")
|
|
assert len(spans) > 0
|
|
|
|
triplets = adapter.adapt(spans)
|
|
for i, triplet in enumerate(triplets):
|
|
print(f">>> Triplet {i}: {triplet}")
|
|
assert len(triplets) == 1
|
|
assert triplets[0].prompt["token_ids"]
|
|
assert triplets[0].response["token_ids"]
|
|
|
|
# stream
|
|
response = client.messages.create(
|
|
model="gpt-4o-arbitrary",
|
|
max_tokens=64,
|
|
messages=[{"role": "user", "content": "Say the word: banana"}],
|
|
stream=True,
|
|
)
|
|
chunk_number: int = 0
|
|
for chunk in response:
|
|
print(f">>> Chunk: {chunk}")
|
|
chunk_number += 1
|
|
assert chunk_number >= 1
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
for i, span in enumerate(spans):
|
|
print(f">>> Span {i}: {span.name}, attributes: {span.attributes}")
|
|
if span.name == "raw_gen_ai_request":
|
|
assert "llm.hosted_vllm.prompt_token_ids" in span.attributes
|
|
assert "llm.hosted_vllm.choices" in span.attributes
|
|
if span.name == "litellm_request":
|
|
assert "gen_ai.completion.0.content" in span.attributes
|
|
assert len(spans) > 0
|
|
triplets = adapter.adapt(spans)
|
|
for i, triplet in enumerate(triplets):
|
|
print(f">>> Triplet {i}: {triplet}")
|
|
assert triplet.prompt["token_ids"]
|
|
assert triplet.response["token_ids"]
|
|
assert len(triplets) == 2
|
|
finally:
|
|
await proxy.stop()
|
|
if isinstance(store, LightningStoreServer):
|
|
await store.stop()
|
|
|
|
|
|
class LogprobsCallback(CustomLogger):
|
|
|
|
async def async_pre_call_hook(self, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: # type: ignore
|
|
return {**data, "logprobs": 1}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("otlp_enabled", [True, False])
|
|
async def test_anthropic_logprobs(qwen25_model: RemoteOpenAIServer, otlp_enabled: bool):
|
|
proxy, store = await _make_proxy_and_store(
|
|
qwen25_model, callbacks=[LogprobsCallback, "return_token_ids", "opentelemetry"], otlp_enabled=otlp_enabled
|
|
)
|
|
try:
|
|
resource, rollout = await _new_resource(proxy, store)
|
|
client = anthropic.Anthropic(base_url=resource.endpoint, api_key="token-abc123", timeout=120)
|
|
adapter = LlmProxyTraceToTriplet()
|
|
|
|
# test streaming case only
|
|
response = client.messages.create(
|
|
model="gpt-4o-arbitrary",
|
|
max_tokens=64,
|
|
messages=[{"role": "user", "content": "Say the word: banana"}],
|
|
stream=True,
|
|
)
|
|
|
|
chunk_number: int = 0
|
|
for chunk in response:
|
|
print(f">>> Chunk: {chunk}")
|
|
chunk_number += 1
|
|
assert chunk_number >= 1
|
|
|
|
spans = await store.query_spans(rollout.rollout_id, rollout.attempt.attempt_id)
|
|
for i, span in enumerate(spans):
|
|
print(f">>> Span {i}: {span.name}, attributes: {span.attributes}")
|
|
if span.name == "raw_gen_ai_request":
|
|
assert "llm.hosted_vllm.prompt_token_ids" in span.attributes
|
|
assert "llm.hosted_vllm.choices" in span.attributes
|
|
choices: list[dict[str, Any]] = ast.literal_eval(span.attributes["llm.hosted_vllm.choices"]) # type: ignore
|
|
|
|
# Check for token IDs and logprobs in the first choice
|
|
assert len(choices) > 0
|
|
if VLLM_VERSION >= (0, 10, 2):
|
|
assert "token_ids" in choices[0]
|
|
assert choices[0]["token_ids"]
|
|
assert "logprobs" in choices[0]
|
|
assert "content" in choices[0]["logprobs"]
|
|
assert len(choices[0]["logprobs"]["content"]) > 0
|
|
assert isinstance(choices[0]["logprobs"]["content"][0], dict)
|
|
assert "token" in choices[0]["logprobs"]["content"][0]
|
|
assert "logprob" in choices[0]["logprobs"]["content"][0]
|
|
assert isinstance(choices[0]["logprobs"]["content"][0]["logprob"], float)
|
|
|
|
assert len(spans) > 0
|
|
|
|
triplets = adapter.adapt(spans)
|
|
for i, triplet in enumerate(triplets):
|
|
print(f">>> Triplet {i}: {triplet}")
|
|
assert triplet.prompt["token_ids"]
|
|
assert triplet.response["token_ids"]
|
|
# TODO: Check logprobs
|
|
finally:
|
|
await proxy.stop()
|
|
if isinstance(store, LightningStoreServer):
|
|
await store.stop()
|