Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/integration/security/_sweeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"/plugin-proxy/{plugin_name}/{path:path}": "reverse proxy to a plugin process",
"/openai_passthrough/{endpoint:path}": "forwards to a provider, not a proxy read",
"/get/latest_release_info": "fetches the latest release from api.github.com",
"/roi-calculator/repositories": "lists repositories from the configured GitHub API, api.github.com by default",
}
)

Expand Down
90 changes: 29 additions & 61 deletions tests/local_testing/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest
from openai import OpenAI

import litellm
from litellm import RateLimitError, Timeout, completion, completion_cost, embedding
Expand Down Expand Up @@ -1580,7 +1582,7 @@ class EventsList(BaseModel):
def test_completion_text_openai():
try:
# litellm.set_verbose =True
response = completion(model="gpt-3.5-turbo-instruct", messages=messages)
response = completion(model="text-completion-openai/gpt-5.4-nano", messages=messages)
print(response["choices"][0]["message"]["content"])
except Exception as e:
print(e)
Expand All @@ -1592,75 +1594,41 @@ async def test_completion_text_openai_async():
try:
# litellm.set_verbose =True
response = await litellm.acompletion(
model="gpt-3.5-turbo-instruct", messages=messages
model="text-completion-openai/gpt-5.4-nano", messages=messages
)
print(response["choices"][0]["message"]["content"])
except Exception as e:
print(e)
pytest.fail(f"Error occurred: {e}")


def custom_callback(
kwargs, # kwargs to completion
completion_response, # response from completion
start_time,
end_time, # start/end time
):
# Your custom code here
try:
print("LITELLM: in custom callback function")
print("\nkwargs\n", kwargs)
model = kwargs["model"]
messages = kwargs["messages"]
user = kwargs.get("user")

#################################################

print(
f"""
Model: {model},
Messages: {messages},
User: {user},
Seed: {kwargs["seed"]},
temperature: {kwargs["temperature"]},
"""
)

assert kwargs["user"] == "ishaans app"
assert kwargs["model"] == "gpt-3.5-turbo-1106"
assert kwargs["seed"] == 12
assert kwargs["temperature"] == 0.5
except Exception as e:
pytest.fail(f"Error occurred: {e}")


def test_completion_openai_with_optional_params():
# [Proxy PROD TEST] WARNING: DO NOT DELETE THIS TEST
# assert that `user` gets passed to the completion call
# Note: This tests that we actually send the optional params to the completion call
# We use custom callbacks to test this
try:
litellm.set_verbose = True
litellm.success_callback = [custom_callback]
response = completion(
model="gpt-3.5-turbo-1106",
messages=[
{"role": "user", "content": "respond in valid, json - what is the day"}
],
temperature=0.5,
top_p=0.1,
seed=12,
response_format={"type": "json_object"},
logit_bias=None,
user="ishaans app",
)
# Add any assertions here to check the response

print(response)
litellm.success_callback = [] # unset callbacks
on_request = MagicMock()
client = OpenAI(http_client=httpx.Client(event_hooks={"request": [on_request]}))
response = completion(
model="gpt-6-luna",
reasoning_effort="none",
messages=[{"role": "user", "content": "respond in valid, json - what is the day"}],
temperature=0.5,
top_p=0.1,
seed=12,
response_format={"type": "json_object"},
logit_bias=None,
user="ishaans app",
client=client,
)

except Exception as e:
pytest.fail(f"Error occurred: {e}")
assert response.choices[0].message.content
on_request.assert_called_once()
sent = json.loads(on_request.call_args.args[0].content)
assert sent["model"] == "gpt-6-luna"
assert sent["user"] == "ishaans app"
assert sent["seed"] == 12
assert sent["temperature"] == 0.5
assert sent["top_p"] == 0.1
assert sent["response_format"] == {"type": "json_object"}
assert "logit_bias" not in sent


# test_completion_openai_with_optional_params()
Expand Down Expand Up @@ -4008,7 +3976,7 @@ def test_deepseek_reasoning_content_completion():
def test_qwen_text_completion():
# litellm._turn_on_debug()
resp = litellm.completion(
model="gpt-3.5-turbo-instruct",
model="text-completion-openai/gpt-5.4-nano",
messages=[{"content": "hello", "role": "user"}],
stream=False,
logprobs=1,
Expand Down
78 changes: 32 additions & 46 deletions tests/local_testing/test_http_parsing_utils.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,61 @@
from collections.abc import Awaitable, Callable

import pytest
from fastapi import Request
from fastapi.testclient import TestClient
from starlette.datastructures import Headers
from starlette.requests import HTTPConnection

from starlette.types import Message

from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
from litellm.proxy._types import ProxyException
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body


@pytest.mark.asyncio
async def test_read_request_body_valid_json():
"""Test the function with a valid JSON payload."""
def _request(receive: Callable[[], Awaitable[Message]]) -> Request:
return Request(
{
"type": "http",
"method": "POST",
"path": "/v1/chat/completions",
"headers": [(b"content-type", b"application/json")],
},
receive,
)

class MockRequest:
async def body(self):
return b'{"key": "value"}'

request = MockRequest()
result = await _read_request_body(request)
assert result == {"key": "value"}
def _request_with_body(body: bytes) -> Request:
async def receive() -> Message:
return {"type": "http.request", "body": body, "more_body": False}

return _request(receive)


@pytest.mark.asyncio
async def test_read_request_body_empty_body():
"""Test the function with an empty body."""
async def test_read_request_body_valid_json():
result = await _read_request_body(_request_with_body(b'{"key": "value"}'))
assert result == {"key": "value"}

class MockRequest:
async def body(self):
return b""

request = MockRequest()
result = await _read_request_body(request)
@pytest.mark.asyncio
async def test_read_request_body_empty_body():
result = await _read_request_body(_request_with_body(b""))
assert result == {}


@pytest.mark.asyncio
async def test_read_request_body_invalid_json():
"""Test the function with an invalid JSON payload."""

class MockRequest:
async def body(self):
return b'{"key": value}' # Missing quotes around `value`

request = MockRequest()
with pytest.raises(ProxyException):
await _read_request_body(request)
await _read_request_body(_request_with_body(b'{"key": value}'))


@pytest.mark.asyncio
async def test_read_request_body_large_payload():
"""Test the function with a very large payload."""
large_payload = '{"key":' + '"a"' * 10**6 + "}" # Large payload

class MockRequest:
async def body(self):
return large_payload.encode()

request = MockRequest()
large_payload = '{"key":' + '"a"' * 10**6 + "}"
with pytest.raises(ProxyException):
await _read_request_body(request)
await _read_request_body(_request_with_body(large_payload.encode()))


@pytest.mark.asyncio
async def test_read_request_body_unexpected_error():
"""Test the function when an unexpected error occurs."""
async def receive() -> Message:
raise ValueError("Unexpected error")

class MockRequest:
async def body(self):
raise ValueError("Unexpected error")

request = MockRequest()
result = await _read_request_body(request)
assert result == {} # Ensure fallback behavior
result = await _read_request_body(_request(receive))
assert result == {}
31 changes: 21 additions & 10 deletions tests/local_testing/test_text_completion.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import asyncio
from typing import Final
import json
import os
import traceback
from types import MappingProxyType

from dotenv import load_dotenv

Expand All @@ -26,6 +28,14 @@
litellm.num_retries = 3


FIREWORKS_TEXT_COMPLETION: Final = MappingProxyType(
{
"model": "text-completion-openai/accounts/fireworks/models/glm-5p3-flash",
"api_base": "https://api.fireworks.ai/inference/v1",
"api_key": os.environ.get("FIREWORKS_AI_API_KEY"),
}
)

token_prompt = [
[
32,
Expand Down Expand Up @@ -3778,8 +3788,9 @@ def test_completion_openai_prompt():
try:
print("\n text 003 test\n")
response = text_completion(
model="gpt-3.5-turbo-instruct",
prompt=["What's the weather in SF?", "How is Manchester?"],
max_tokens=5,
**FIREWORKS_TEXT_COMPLETION,
)
print(response)
assert len(response.choices) == 2
Expand Down Expand Up @@ -3841,9 +3852,9 @@ def test_completion_chatgpt_prompt():
def test_completion_gpt_instruct():
try:
response = text_completion(
model="gpt-3.5-turbo-instruct-0914",
model="gpt-5.4-nano",
prompt="What's the weather in SF?",
custom_llm_provider="openai",
custom_llm_provider="text-completion-openai",
)
print(response)
response_str = response["choices"][0]["text"]
Expand All @@ -3862,7 +3873,7 @@ def test_text_completion_basic():
print("\n test 003 with logprobs \n")
litellm.set_verbose = False
response = text_completion(
model="gpt-3.5-turbo-instruct",
model="text-completion-openai/gpt-5.4-nano",
prompt="good morning",
max_tokens=10,
logprobs=10,
Expand All @@ -3886,13 +3897,11 @@ def test_completion_text_003_prompt_array():
try:
litellm.set_verbose = False
response = text_completion(
model="gpt-3.5-turbo-instruct",
prompt=token_prompt, # token prompt is a 2d list
max_tokens=5,
**FIREWORKS_TEXT_COMPLETION,
)
print("\n\n response")

print(response)
# response_str = response["choices"][0]["text"]
assert len(response.choices) == len(token_prompt)
except Exception as e:
pytest.fail(f"Error occurred: {e}")

Expand Down Expand Up @@ -4151,8 +4160,8 @@ def test_completion_fireworks_ai_multiple_choices():
def test_text_completion_with_echo(stream):
litellm.set_verbose = True
response = litellm.text_completion(
model="davinci-002",
prompt="hello",
**FIREWORKS_TEXT_COMPLETION,
max_tokens=1, # only see the first token
stop="\n", # stop at the first newline
logprobs=1, # return log prob
Expand All @@ -4166,6 +4175,8 @@ def test_text_completion_with_echo(stream):
print(chunk)
else:
assert isinstance(response, TextCompletionResponse)
assert response.choices[0].text.startswith("hello")
assert response.choices[0].logprobs.token_logprobs


def test_text_completion_ollama():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"user": "",
"team_id": "",
"organization_id": "",
"metadata": "{\"applied_guardrails\": [], \"attempted_fallbacks\": null, \"original_model_group\": null, \"batch_models\": null, \"batch_successful_requests\": null, \"batch_failed_requests\": null, \"mcp_tool_call_metadata\": null, \"vector_store_request_metadata\": null, \"routing_decision\": null, \"internal_call_origin\": null, \"router_metadata\": null, \"autorouter_savings_estimate\": null, \"autorouter_baseline_observation\": null, \"azure_spillover\": null, \"guardrail_information\": null, \"compression_savings\": null, \"litellm_gateway_injected_cache\": null, \"usage_object\": {\"completion_tokens\": 20, \"prompt_tokens\": 10, \"total_tokens\": 30, \"completion_tokens_details\": null, \"prompt_tokens_details\": null}, \"model_map_information\": {\"model_map_key\": \"gpt-4o\", \"model_map_value\": {\"key\": \"gpt-4o\", \"max_tokens\": 16384, \"max_input_tokens\": 128000, \"max_output_tokens\": 16384, \"input_cost_per_token\": 2.5e-06, \"cache_creation_input_token_cost\": null, \"cache_read_input_token_cost\": 1.25e-06, \"input_cost_per_character\": null, \"input_cost_per_token_above_128k_tokens\": null, \"input_cost_per_token_above_200k_tokens\": null, \"input_cost_per_query\": null, \"input_cost_per_second\": null, \"input_cost_per_audio_token\": null, \"input_cost_per_token_batches\": 1.25e-06, \"output_cost_per_token_batches\": 5e-06, \"output_cost_per_token\": 1e-05, \"output_cost_per_audio_token\": null, \"output_cost_per_character\": null, \"output_cost_per_token_above_128k_tokens\": null, \"output_cost_per_character_above_128k_tokens\": null, \"output_cost_per_token_above_200k_tokens\": null, \"output_cost_per_second\": null, \"output_cost_per_image\": null, \"output_vector_size\": null, \"litellm_provider\": \"openai\", \"mode\": \"chat\", \"supports_system_messages\": true, \"supports_response_schema\": true, \"supports_vision\": true, \"supports_function_calling\": true, \"supports_tool_choice\": true, \"supports_assistant_prefill\": false, \"supports_prompt_caching\": true, \"supports_audio_input\": false, \"supports_audio_output\": false, \"supports_pdf_input\": false, \"supports_embedding_image_input\": false, \"supports_native_streaming\": null, \"supports_web_search\": true, \"supports_reasoning\": false, \"search_context_cost_per_query\": {\"search_context_size_low\": 0.03, \"search_context_size_medium\": 0.035, \"search_context_size_high\": 0.05}, \"tpm\": null, \"rpm\": null, \"supported_openai_params\": [\"frequency_penalty\", \"logit_bias\", \"logprobs\", \"top_logprobs\", \"max_tokens\", \"max_completion_tokens\", \"modalities\", \"prediction\", \"n\", \"presence_penalty\", \"seed\", \"stop\", \"stream\", \"stream_options\", \"temperature\", \"top_p\", \"tools\", \"tool_choice\", \"function_call\", \"functions\", \"max_retries\", \"extra_headers\", \"parallel_tool_calls\", \"audio\", \"response_format\", \"user\"]}}, \"additional_usage_values\": {\"completion_tokens_details\": null, \"prompt_tokens_details\": null}, \"user_api_key\": null, \"user_api_key_alias\": null, \"user_api_key_team_id\": null, \"user_api_key_project_id\": null, \"user_api_key_project_alias\": null, \"user_api_key_org_id\": null, \"user_api_key_user_id\": null, \"user_api_key_team_alias\": null, \"spend_logs_metadata\": null, \"requester_ip_address\": null, \"user_agent\": null, \"status\": null, \"proxy_server_request\": null, \"error_information\": null, \"attempted_retries\": null, \"max_retries\": null}",
"metadata": "{\"actor_agent_id\": null, \"target_agent_id\": null, \"billing_agent_id\": null, \"agent_execution_mode\": null, \"verified_human_user_id\": null, \"applied_guardrails\": [], \"attempted_fallbacks\": null, \"original_model_group\": null, \"batch_models\": null, \"batch_successful_requests\": null, \"batch_failed_requests\": null, \"mcp_tool_call_metadata\": null, \"vector_store_request_metadata\": null, \"routing_decision\": null, \"internal_call_origin\": null, \"router_metadata\": null, \"autorouter_savings_estimate\": null, \"autorouter_baseline_observation\": null, \"azure_spillover\": null, \"guardrail_information\": null, \"compression_savings\": null, \"litellm_gateway_injected_cache\": null, \"usage_object\": {\"completion_tokens\": 20, \"prompt_tokens\": 10, \"total_tokens\": 30, \"completion_tokens_details\": null, \"prompt_tokens_details\": null}, \"model_map_information\": {\"model_map_key\": \"gpt-4o\", \"model_map_value\": {\"key\": \"gpt-4o\", \"max_tokens\": 16384, \"max_input_tokens\": 128000, \"max_output_tokens\": 16384, \"input_cost_per_token\": 2.5e-06, \"cache_creation_input_token_cost\": null, \"cache_read_input_token_cost\": 1.25e-06, \"input_cost_per_character\": null, \"input_cost_per_token_above_128k_tokens\": null, \"input_cost_per_token_above_200k_tokens\": null, \"input_cost_per_query\": null, \"input_cost_per_second\": null, \"input_cost_per_audio_token\": null, \"input_cost_per_token_batches\": 1.25e-06, \"output_cost_per_token_batches\": 5e-06, \"output_cost_per_token\": 1e-05, \"output_cost_per_audio_token\": null, \"output_cost_per_character\": null, \"output_cost_per_token_above_128k_tokens\": null, \"output_cost_per_character_above_128k_tokens\": null, \"output_cost_per_token_above_200k_tokens\": null, \"output_cost_per_second\": null, \"output_cost_per_image\": null, \"output_vector_size\": null, \"litellm_provider\": \"openai\", \"mode\": \"chat\", \"supports_system_messages\": true, \"supports_response_schema\": true, \"supports_vision\": true, \"supports_function_calling\": true, \"supports_tool_choice\": true, \"supports_assistant_prefill\": false, \"supports_prompt_caching\": true, \"supports_audio_input\": false, \"supports_audio_output\": false, \"supports_pdf_input\": false, \"supports_embedding_image_input\": false, \"supports_native_streaming\": null, \"supports_web_search\": true, \"supports_reasoning\": false, \"search_context_cost_per_query\": {\"search_context_size_low\": 0.03, \"search_context_size_medium\": 0.035, \"search_context_size_high\": 0.05}, \"tpm\": null, \"rpm\": null, \"supported_openai_params\": [\"frequency_penalty\", \"logit_bias\", \"logprobs\", \"top_logprobs\", \"max_tokens\", \"max_completion_tokens\", \"modalities\", \"prediction\", \"n\", \"presence_penalty\", \"seed\", \"stop\", \"stream\", \"stream_options\", \"temperature\", \"top_p\", \"tools\", \"tool_choice\", \"function_call\", \"functions\", \"max_retries\", \"extra_headers\", \"parallel_tool_calls\", \"audio\", \"response_format\", \"user\"]}}, \"additional_usage_values\": {\"completion_tokens_details\": null, \"prompt_tokens_details\": null}, \"user_api_key\": null, \"user_api_key_alias\": null, \"user_api_key_team_id\": null, \"user_api_key_project_id\": null, \"user_api_key_project_alias\": null, \"user_api_key_org_id\": null, \"user_api_key_user_id\": null, \"user_api_key_team_alias\": null, \"spend_logs_metadata\": null, \"requester_ip_address\": null, \"user_agent\": null, \"status\": null, \"proxy_server_request\": null, \"error_information\": null, \"attempted_retries\": null, \"max_retries\": null}",
"cache_key": "Cache OFF",
"spend": 0.00022500000000000002,
"total_tokens": 30,
Expand All @@ -29,5 +29,6 @@
"proxy_server_request": "{}",
"status": "success",
"mcp_namespaced_tool_name": null,
"agent_id": null
"agent_id": null,
"billing_agent_id": null
}
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,7 @@ def _raw_batches_request(body: Dict[str, Any]) -> MagicMock:
request.url.__str__.return_value = "http://localhost/v1/batches"
request.url.path = "/v1/batches"
request.method = "POST"
request.scope = {"type": "http", "method": "POST", "path": "/v1/batches"}
request.query_params = {}
request.headers = {"Content-Type": "application/json"}
request.client = MagicMock()
Expand Down
Loading
Loading