Merge pull request #38637 from BerriAI/litellm_/red-tests-review-fbb718

test: refresh the suites that drifted from langfuse and OpenAI's retired Assistants API
This commit is contained in:
yuneng-jiang 2026-08-28 09:58:44 -07:00 committed by GitHub
commit a4a0386717
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 165 additions and 494 deletions

View file

@ -2421,45 +2421,6 @@ jobs:
- wait_for_service:
url: http://localhost:4000
timeout: "300"
# Add Ruby installation and testing before the existing Node.js and Python tests
- run:
name: Install Ruby and Bundler
command: |
# Clone RVM at pinned tag and verify the commit SHA matches the
# published tag before running its install script.
RVM_VERSION="1.29.12"
RVM_EXPECTED_SHA="6bfc9213c9d6914fe756f524eb034a403d51db81"
git clone --depth 1 --branch "$RVM_VERSION" https://github.com/rvm/rvm.git /tmp/rvm
RVM_ACTUAL_SHA="$(git -C /tmp/rvm rev-parse HEAD)"
if [ "$RVM_ACTUAL_SHA" != "$RVM_EXPECTED_SHA" ]; then
echo "RVM tag $RVM_VERSION resolved to $RVM_ACTUAL_SHA; expected $RVM_EXPECTED_SHA" >&2
exit 1
fi
# Import RVM signing keys (used by `rvm install` to verify Ruby tarballs)
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
# Install RVM from the verified checkout. The install script
# sources `scripts/functions/installer` using paths relative to
# its own working directory, so it must be run from /tmp/rvm.
(cd /tmp/rvm && ./install --path "$HOME/.rvm")
source "$HOME/.rvm/scripts/rvm"
# Install Ruby 3.2.2 (RVM verifies the tarball PGP signature)
rvm install 3.2.2
rvm use 3.2.2 --default
# Install latest Bundler
gem install bundler
- run:
name: Run Ruby tests
command: |
source $HOME/.rvm/scripts/rvm
cd tests/pass_through_tests/ruby_passthrough_tests
bundle install
bundle exec rspec
no_output_timeout: 30m
# Install Node.js directly from nodejs.org with SHA256 verification,
# instead of piping NodeSource's setup_24.x apt-repo installer into
# sudo bash (which runs a mutable upstream script unattended).

View file

@ -1,6 +1,6 @@
{
"TQ001": {
"limit": 744
"limit": 736
},
"TQ002": {
"limit": 742
@ -12,7 +12,7 @@
"limit": 469
},
"TQ005": {
"limit": 2405
"limit": 2399
},
"TQ006": {
"limit": 34

View file

@ -1,4 +1,4 @@
import { expect, test, type Page as PlaywrightPage } from "@playwright/test";
import { expect, test, type Locator, type Page as PlaywrightPage } from "@playwright/test";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { navigateToPage } from "../../helpers/navigation";
import { Page } from "../../fixtures/pages";
@ -17,43 +17,49 @@ async function openTemplateSelect(page: PlaywrightPage) {
return trigger;
}
function pollPixelsBelowTrigger(trigger: Locator, popup: Locator) {
return expect.poll(async () => {
const triggerBox = await trigger.boundingBox();
const popupBox = await popup.boundingBox();
if (!triggerBox || !popupBox) return null;
return popupBox.y - (triggerBox.y + triggerBox.height);
});
}
function pollPopupOverlapsTrigger(trigger: Locator, popup: Locator) {
return expect.poll(async () => {
const triggerBox = await trigger.boundingBox();
const popupBox = await popup.boundingBox();
if (!triggerBox || !popupBox) return null;
return popupBox.y < triggerBox.y + triggerBox.height && popupBox.y + popupBox.height > triggerBox.y;
});
}
test.describe("Auto Router template select anchoring", () => {
test.use({ storageState: ADMIN_STORAGE_PATH });
test("opens the options below the trigger rather than over it", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
const trigger = await openTemplateSelect(page);
const triggerBox = await trigger.boundingBox();
await trigger.click();
const popup = page.locator('[data-slot="select-content"]');
await expect(popup).toBeVisible();
const popupBox = await popup.boundingBox();
expect(triggerBox).not.toBeNull();
expect(popupBox).not.toBeNull();
// Item-aligned mode reports "none" and puts the active item over the trigger.
await expect(popup).toHaveAttribute("data-side", "bottom");
expect(popupBox!.y).toBeGreaterThanOrEqual(triggerBox!.y + triggerBox!.height);
await pollPixelsBelowTrigger(trigger, popup).toBeGreaterThanOrEqual(0);
});
test("flips above the trigger instead of covering it when there is no room below", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 560 });
const trigger = await openTemplateSelect(page);
await trigger.scrollIntoViewIfNeeded();
const triggerBox = await trigger.boundingBox();
await trigger.click();
const popup = page.locator('[data-slot="select-content"]');
await expect(popup).toBeVisible();
const popupBox = await popup.boundingBox();
expect(triggerBox).not.toBeNull();
expect(popupBox).not.toBeNull();
const overlaps =
popupBox!.y < triggerBox!.y + triggerBox!.height && popupBox!.y + popupBox!.height > triggerBox!.y;
expect(overlaps).toBe(false);
await pollPopupOverlapsTrigger(trigger, popup).toBe(false);
});
});

View file

@ -19,32 +19,32 @@ from litellm import (
# litellm.set_verbose=True
TOLERATED_UPSTREAM_FAILURES = (Timeout, litellm.InternalServerError)
def test_batch_completions():
messages = [[{"role": "user", "content": "write a short poem"}] for _ in range(3)]
model = "gpt-3.5-turbo"
litellm.set_verbose = True
try:
result = batch_completion(
model=model,
messages=messages,
max_tokens=10,
temperature=0.2,
request_timeout=1,
)
print(result)
print(len(result))
assert len(result) == 3
for response in result:
assert response.choices[0].message.content is not None
except Timeout as e:
print(f"IN TIMEOUT")
pass
except litellm.InternalServerError as e:
print(f"IN INTERNAL SERVER ERROR")
pass
except Exception as e:
pytest.fail(f"An error occurred: {e}")
result = batch_completion(
model=model,
messages=messages,
max_tokens=10,
temperature=0.2,
request_timeout=1,
)
print(result)
assert len(result) == 3
for response in result:
if isinstance(response, TOLERATED_UPSTREAM_FAILURES):
continue
assert not isinstance(
response, Exception
), f"batch_completion returned {type(response).__name__}: {response}"
assert response.choices[0].message.content is not None
# test_batch_completions()

View file

@ -16,6 +16,8 @@ verbose_logger.setLevel(logging.DEBUG)
litellm.set_verbose = True
import time
INTERVAL_TOO_LONG_TO_FIRE_DURING_THIS_TEST = 3600
@pytest.mark.asyncio
async def test_opik_logging_http_request():
@ -23,70 +25,60 @@ async def test_opik_logging_http_request():
- Test that HTTP requests are made to Opik
- Traces and spans are batched correctly
"""
try:
from litellm.integrations.opik.opik import OpikLogger
from litellm.integrations.opik.opik import OpikLogger
os.environ["OPIK_URL_OVERRIDE"] = "https://fake.comet.com/opik/api"
os.environ["OPIK_API_KEY"] = "anything"
os.environ["OPIK_WORKSPACE"] = "anything"
os.environ["OPIK_URL_OVERRIDE"] = "https://fake.comet.com/opik/api"
os.environ["OPIK_API_KEY"] = "anything"
os.environ["OPIK_WORKSPACE"] = "anything"
# Initialize OpikLogger
test_opik_logger = OpikLogger()
test_opik_logger = OpikLogger()
test_opik_logger.flush_interval = INTERVAL_TOO_LONG_TO_FIRE_DURING_THIS_TEST
test_opik_logger.batch_size = 12
litellm.callbacks = [test_opik_logger]
test_opik_logger.batch_size = 12
litellm.set_verbose = True
litellm.callbacks = [test_opik_logger]
# Create a mock for the async_client's post method
mock_post = AsyncMock()
mock_post.return_value.status_code = 202
mock_post.return_value.text = "Accepted"
test_opik_logger.async_httpx_client.post = mock_post
mock_post = AsyncMock(return_value=Mock(status_code=202, text="Accepted"))
test_opik_logger.async_httpx_client.post = mock_post
# Make multiple calls to ensure we don't hit the batch size
for _ in range(5):
response = await litellm.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Test message"}],
max_tokens=10,
temperature=0.2,
mock_response="This is a mock response",
)
await asyncio.sleep(1)
def opik_batch_calls():
return [
call
for call in mock_post.call_args_list
if "/traces/batch" in str(call) or "/spans/batch" in str(call)
]
# Check batching of events and that the queue contains 5 trace events and 5 span events
assert (
mock_post.called == False
), "HTTP request was made but events should have been batched"
assert len(test_opik_logger.log_queue) == 10
for _ in range(5):
await litellm.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Test message"}],
max_tokens=10,
temperature=0.2,
mock_response="This is a mock response",
)
await asyncio.sleep(1)
# Now make calls to exceed the batch size
for _ in range(3):
response = await litellm.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Test message"}],
max_tokens=10,
temperature=0.2,
mock_response="This is a mock response",
)
assert opik_batch_calls() == [], "events below batch_size must stay queued"
assert len(test_opik_logger.log_queue) == 10
# Wait a short time for any asynchronous operations to complete
await asyncio.sleep(1)
for _ in range(3):
await litellm.acompletion(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Test message"}],
max_tokens=10,
temperature=0.2,
mock_response="This is a mock response",
)
await asyncio.sleep(1)
# Check that the queue was flushed after exceeding batch size
assert len(test_opik_logger.log_queue) < test_opik_logger.batch_size
assert opik_batch_calls(), "crossing batch_size must flush the queue"
events_left_over_after_the_size_triggered_flush = len(test_opik_logger.log_queue)
assert 0 < events_left_over_after_the_size_triggered_flush < test_opik_logger.batch_size
# Check that the data has been sent when it goes above the flush interval
await asyncio.sleep(test_opik_logger.flush_interval)
assert len(test_opik_logger.log_queue) == 0
calls_before_periodic_flush = len(opik_batch_calls())
await test_opik_logger.flush_queue()
# Clean up
for cb in litellm.callbacks:
if isinstance(cb, OpikLogger):
await cb.async_httpx_client.client.aclose()
except Exception as e:
pytest.fail(f"Error occurred: {e}")
assert len(opik_batch_calls()) > calls_before_periodic_flush
assert len(test_opik_logger.log_queue) == 0
def test_sync_opik_logging_http_request():

View file

@ -94,11 +94,13 @@ def test_langfuse_handler_accepts_secret_key_alias(monkeypatch):
langfuse_public_key=None,
langfuse_secret=None,
langfuse_host=None,
langfuse_environment=None,
allow_env_credentials=True,
):
captured["langfuse_public_key"] = langfuse_public_key
captured["langfuse_secret"] = langfuse_secret
captured["langfuse_host"] = langfuse_host
captured["langfuse_environment"] = langfuse_environment
captured["allow_env_credentials"] = allow_env_credentials
class FakeDynamicLoggingCache:
@ -117,6 +119,7 @@ def test_langfuse_handler_accepts_secret_key_alias(monkeypatch):
"langfuse_public_key": "dynamic-public",
"langfuse_secret_key": "dynamic-secret",
"langfuse_host": "https://langfuse.example",
"langfuse_environment": "dynamic-environment",
},
in_memory_dynamic_logger_cache=FakeDynamicLoggingCache(),
)
@ -124,6 +127,7 @@ def test_langfuse_handler_accepts_secret_key_alias(monkeypatch):
assert captured["langfuse_public_key"] == "dynamic-public"
assert captured["langfuse_secret"] == "dynamic-secret"
assert captured["langfuse_host"] == "https://langfuse.example"
assert captured["langfuse_environment"] == "dynamic-environment"
assert captured["allow_env_credentials"] is False
assert captured["cached_service_name"] == "langfuse"
assert captured["cached_logging_obj"] is logger

View file

@ -123,16 +123,12 @@ def test_get_langfuse_logger_for_request_with_dynamic_params(
assert result.secret_key == "test_secret"
assert result.langfuse_host == "https://test.langfuse.com"
# Check if the logger is cached
cached_logger = dynamic_logging_cache.get_cache(
credentials={
"langfuse_public_key": "test_public_key",
"langfuse_secret": "test_secret",
"langfuse_host": "https://test.langfuse.com",
},
service_name="langfuse",
logger_for_identical_repeat_request = LangFuseHandler.get_langfuse_logger_for_request(
standard_callback_dynamic_params=standard_params,
in_memory_dynamic_logger_cache=dynamic_logging_cache,
globalLangfuseLogger=globalLangfuseLogger,
)
assert cached_logger is result
assert logger_for_identical_repeat_request is result
@pytest.mark.parametrize("globalLangfuseLogger", [None, global_langfuse_logger])

View file

@ -1,4 +0,0 @@
source 'https://rubygems.org'
gem 'rspec'
gem 'ruby-openai'

View file

@ -1,42 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
base64 (0.2.0)
diff-lcs (1.6.0)
event_stream_parser (1.0.0)
faraday (2.8.1)
base64
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (3.0.2)
multipart-post (2.4.1)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.2)
ruby-openai (7.4.0)
event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1)
faraday-multipart (>= 1)
ruby2_keywords (0.0.5)
PLATFORMS
ruby
DEPENDENCIES
rspec
ruby-openai
BUNDLED WITH
2.6.5

View file

@ -1,96 +0,0 @@
require 'openai'
require 'rspec'
RSpec.describe 'OpenAI Assistants Passthrough' do
let(:client) do
OpenAI::Client.new(
access_token: "sk-1234",
uri_base: "http://0.0.0.0:4000/openai",
request_timeout: 600
)
end
it 'performs basic assistant operations' do
assistant = client.assistants.create(
parameters: {
name: "Math Tutor",
instructions: "You are a personal math tutor. Write and run code to answer math questions.",
tools: [{ type: "code_interpreter" }],
model: "gpt-4o"
}
)
expect(assistant).to include('id')
expect(assistant['name']).to eq("Math Tutor")
assistants_list = client.assistants.list
expect(assistants_list['data']).to be_an(Array)
expect(assistants_list['data']).to include(include('id' => assistant['id']))
retrieved_assistant = client.assistants.retrieve(id: assistant['id'])
expect(retrieved_assistant).to eq(assistant)
deleted_assistant = client.assistants.delete(id: assistant['id'])
expect(deleted_assistant['deleted']).to be true
expect(deleted_assistant['id']).to eq(assistant['id'])
end
it 'performs streaming assistant operations' do
puts "\n=== Starting Streaming Assistant Test ==="
assistant = client.assistants.create(
parameters: {
name: "Math Tutor",
instructions: "You are a personal math tutor. Write and run code to answer math questions.",
tools: [{ type: "code_interpreter" }],
model: "gpt-4o"
}
)
puts "Created assistant: #{assistant['id']}"
expect(assistant).to include('id')
thread = client.threads.create
puts "Created thread: #{thread['id']}"
expect(thread).to include('id')
message = client.messages.create(
thread_id: thread['id'],
parameters: {
role: "user",
content: "I need to solve the equation `3x + 11 = 14`. Can you help me?"
}
)
puts "Created message: #{message['id']}"
puts "User question: #{message['content']}"
expect(message).to include('id')
expect(message['role']).to eq('user')
puts "\nStarting streaming response:"
puts "------------------------"
run = client.runs.create(
thread_id: thread['id'],
parameters: {
assistant_id: assistant['id'],
max_prompt_tokens: 256,
max_completion_tokens: 16,
stream: proc do |chunk, _bytesize|
puts "Received chunk: #{chunk.inspect}" # Debug: Print raw chunk
if chunk["object"] == "thread.message.delta"
content = chunk.dig("delta", "content")
puts "Content: #{content.inspect}" # Debug: Print content structure
if content && content[0] && content[0]["text"]
print content[0]["text"]["value"]
$stdout.flush # Ensure output is printed immediately
end
end
end
}
)
puts "\n------------------------"
puts "Run completed: #{run['id']}"
expect(run).not_to be_nil
ensure
client.assistants.delete(id: assistant['id']) if assistant && assistant['id']
client.threads.delete(id: thread['id']) if thread && thread['id']
end
end

View file

@ -1,141 +1,22 @@
import pytest
import openai
import aiohttp
import asyncio
import tempfile
from typing_extensions import override
from openai import AssistantEventHandler
client = openai.OpenAI(base_url="http://0.0.0.0:4000/openai", api_key="sk-1234")
def test_pass_through_file_operations():
# Create a temporary file
with tempfile.NamedTemporaryFile(
mode="w+", suffix=".txt", delete=False
) as temp_file:
temp_file.write("This is a test file for the OpenAI Assistants API.")
temp_file.flush()
# create a file
file = client.files.create(
file=open(temp_file.name, "rb"),
purpose="assistants",
)
print("file created", file)
# delete the file
delete_file = client.files.delete(file.id)
print("file deleted", delete_file)
def test_openai_assistants_e2e_operations():
assistant = client.beta.assistants.create(
name="Math Tutor",
instructions="You are a personal math tutor. Write and run code to answer math questions.",
tools=[{"type": "code_interpreter"}],
model="gpt-4o",
)
print("assistant created", assistant)
get_assistant = client.beta.assistants.retrieve(assistant.id)
print(get_assistant)
delete_assistant = client.beta.assistants.delete(assistant.id)
print(delete_assistant)
class EventHandler(AssistantEventHandler):
@override
def on_text_created(self, text) -> None:
print(f"\nassistant > ", end="", flush=True)
@override
def on_text_delta(self, delta, snapshot):
print(delta.value, end="", flush=True)
def on_tool_call_created(self, tool_call):
print(f"\nassistant > {tool_call.type}\n", flush=True)
def on_tool_call_delta(self, delta, snapshot):
if delta.type == "code_interpreter":
if delta.code_interpreter.input:
print(delta.code_interpreter.input, end="", flush=True)
if delta.code_interpreter.outputs:
print(f"\n\noutput >", flush=True)
for output in delta.code_interpreter.outputs:
if output.type == "logs":
print(f"\n{output.logs}", flush=True)
def test_openai_assistants_e2e_operations_stream():
assistant = client.beta.assistants.create(
name="Math Tutor",
instructions="You are a personal math tutor. Write and run code to answer math questions.",
tools=[{"type": "code_interpreter"}],
model="gpt-4o",
)
print("assistant created", assistant)
thread = client.beta.threads.create()
print("thread created", thread)
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="I need to solve the equation `3x + 11 = 14`. Can you help me?",
)
print("message created", message)
# Then, we use the `stream` SDK helper
# with the `EventHandler` class to create the Run
# and stream the response.
with client.beta.threads.runs.stream(
thread_id=thread.id,
assistant_id=assistant.id,
instructions="Please address the user as Jane Doe. The user has a premium account.",
event_handler=EventHandler(),
) as stream:
stream.until_done()
def test_azure_openai_assistants_e2e_operations_stream():
from openai import AzureOpenAI
client = AzureOpenAI(
base_url="http://0.0.0.0:4000/azure-config-passthrough/openai",
api_key="sk-1234",
api_version="2025-01-01-preview",
)
assistant = client.beta.assistants.create(
name="Math Tutor",
instructions="You are a personal math tutor. Write and run code to answer math questions.",
tools=[{"type": "code_interpreter"}],
model="gpt-4o",
)
print("assistant created", assistant)
thread = client.beta.threads.create()
print("thread created", thread)
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="I need to solve the equation `3x + 11 = 14`. Can you help me?",
)
print("message created", message)
# Then, we use the `stream` SDK helper
# with the `EventHandler` class to create the Run
# and stream the response.
with client.beta.threads.runs.stream(
thread_id=thread.id,
assistant_id=assistant.id,
instructions="Please address the user as Jane Doe. The user has a premium account.",
event_handler=EventHandler(),
) as stream:
stream.until_done()

View file

@ -1,7 +1,6 @@
import json
import os
from datetime import datetime
from unittest.mock import AsyncMock, Mock, patch, MagicMock
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Optional
from fastapi import Request
import pytest
@ -31,17 +30,62 @@ class TestCustomLogger(CustomLogger):
self.logged_kwargs = kwargs
UPSTREAM_RESPONSE_BODY = {
"id": "modr-abc123",
"model": "omni-moderation-latest",
"results": [
{
"flagged": False,
"categories": {"violence": False},
"category_scores": {"violence": 1.2e-06},
}
],
}
@pytest.fixture
def upstream():
received: dict = {}
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_POST(self):
body = self.rfile.read(int(self.headers.get("content-length", 0) or 0))
received["path"] = self.path
received["body"] = json.loads(body or b"{}")
payload = json.dumps(UPSTREAM_RESPONSE_BODY).encode()
self.send_response(200)
self.send_header("content-type", "application/json")
self.send_header("content-length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
yield f"http://127.0.0.1:{server.server_port}", received
finally:
server.shutdown()
server.server_close()
@pytest.mark.asyncio
async def test_assistants_passthrough_logging():
async def test_passthrough_logging_payload_for_a_route_no_provider_handler_claims(
upstream,
):
base_url, upstream_received = upstream
test_custom_logger = TestCustomLogger()
litellm._async_success_callback = [test_custom_logger]
TARGET_URL = "https://api.openai.com/v1/assistants"
TARGET_URL = f"{base_url}/v1/moderations"
REQUEST_BODY = {
"instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.",
"name": "Math Tutor",
"tools": [{"type": "code_interpreter"}],
"model": "gpt-4.1-mini",
"model": "omni-moderation-latest",
"input": "I want to bake a cake for my friend's birthday.",
}
TARGET_METHOD = "POST"
@ -50,23 +94,18 @@ async def test_assistants_passthrough_logging():
scope={
"type": "http",
"method": TARGET_METHOD,
"path": "/v1/assistants",
"path": "/v1/moderations",
"query_string": b"",
"headers": [
(b"content-type", b"application/json"),
(
b"authorization",
f"Bearer {os.getenv('OPENAI_API_KEY')}".encode(),
),
(b"openai-beta", b"assistants=v2"),
(b"authorization", b"Bearer sk-test-passthrough"),
],
},
),
target=TARGET_URL,
custom_headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
"OpenAI-Beta": "assistants=v2",
"Authorization": "Bearer sk-test-passthrough",
},
user_api_key_dict=UserAPIKeyAuth(
api_key="test",
@ -83,6 +122,10 @@ async def test_assistants_passthrough_logging():
print("result status code", result.status_code)
print("result content", result.body)
assert upstream_received.get("path") == "/v1/moderations"
assert upstream_received.get("body") == REQUEST_BODY
assert result.status_code == 200
await asyncio.sleep(1)
assert test_custom_logger.logged_kwargs is not None
@ -92,79 +135,8 @@ async def test_assistants_passthrough_logging():
assert passthrough_logging_payload is not None
assert passthrough_logging_payload["url"] == TARGET_URL
assert passthrough_logging_payload["request_body"] == REQUEST_BODY
# assert that the response body content matches the response body content
client_facing_response_body = json.loads(result.body)
assert passthrough_logging_payload["response_body"] == client_facing_response_body
# assert that the request method is correct
assert passthrough_logging_payload["request_method"] == TARGET_METHOD
@pytest.mark.asyncio
async def test_threads_passthrough_logging():
test_custom_logger = TestCustomLogger()
litellm._async_success_callback = [test_custom_logger]
TARGET_URL = "https://api.openai.com/v1/threads"
REQUEST_BODY = {}
TARGET_METHOD = "POST"
result = await pass_through_request(
request=Request(
scope={
"type": "http",
"method": TARGET_METHOD,
"path": "/v1/threads",
"query_string": b"",
"headers": [
(b"content-type", b"application/json"),
(
b"authorization",
f"Bearer {os.getenv('OPENAI_API_KEY')}".encode(),
),
(b"openai-beta", b"assistants=v2"),
],
},
),
target=TARGET_URL,
custom_headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
"OpenAI-Beta": "assistants=v2",
},
user_api_key_dict=UserAPIKeyAuth(
api_key="test",
user_id="test",
team_id="test",
end_user_id="test",
),
custom_body=REQUEST_BODY,
forward_headers=False,
merge_query_params=False,
)
print("got result", result)
print("result status code", result.status_code)
print("result content", result.body)
await asyncio.sleep(1)
assert test_custom_logger.logged_kwargs is not None
passthrough_logging_payload = test_custom_logger.logged_kwargs[
"passthrough_logging_payload"
]
assert passthrough_logging_payload is not None
# Fix for TypedDict access errors
assert passthrough_logging_payload.get("url") == TARGET_URL
assert passthrough_logging_payload.get("request_body") == REQUEST_BODY
# Fix for json.loads error with potential memoryview
response_body = result.body
client_facing_response_body = json.loads(response_body)
assert (
passthrough_logging_payload.get("response_body") == client_facing_response_body
)
assert passthrough_logging_payload.get("request_method") == TARGET_METHOD
client_facing_response_body = json.loads(result.body)
assert client_facing_response_body == UPSTREAM_RESPONSE_BODY
assert passthrough_logging_payload["response_body"] == client_facing_response_body

View file

@ -23,9 +23,10 @@ from litellm.proxy.management_helpers.access_group_team_sync import (
sync_team_access_group_membership,
)
TEAM = "ags-team-a"
OTHER_TEAM = "ags-team-b"
GROUPS = ("ags-group-1", "ags-group-2", "ags-group-3")
_XDIST_WORKER = os.environ.get("PYTEST_XDIST_WORKER", "master")
TEAM = f"ags-team-a-{_XDIST_WORKER}"
OTHER_TEAM = f"ags-team-b-{_XDIST_WORKER}"
GROUPS = tuple(f"ags-group-{n}-{_XDIST_WORKER}" for n in (1, 2, 3))
_DELETE_SEEDED = 'DELETE FROM "LiteLLM_AccessGroupTable" WHERE access_group_id = ANY($1::TEXT[])'
_DELETE_TEAMS = 'DELETE FROM "LiteLLM_TeamTable" WHERE team_id = ANY($1::TEXT[])'