Merge pull request #21375 from BerriAI/litellm_evals_api

[feat] Add support for Openai Evals API
This commit is contained in:
Sameer Kankute
2026-02-17 21:01:36 +05:30
committed by GitHub
20 changed files with 6764 additions and 3 deletions
+441
View File
@@ -0,0 +1,441 @@
# /evals
LiteLLM Proxy supports OpenAI's Evaluations (Evals) API, allowing you to create, manage, and run evaluations to measure model performance against defined testing criteria.
## What are Evals?
OpenAI Evals API provides a structured way to:
- **Create Evaluations**: Define testing criteria and data sources for evaluating model outputs
- **Run Evaluations**: Execute evaluations against specific models and datasets
- **Track Results**: Monitor evaluation progress and review detailed results
## Quick Start
### Setup LiteLLM Proxy
First, start your LiteLLM Proxy server:
```bash
litellm --config config.yaml
# Proxy will run on http://localhost:4000
```
### Initialize OpenAI Client
```python
from openai import OpenAI
# Point to your LiteLLM Proxy
client = OpenAI(
api_key="sk-1234", # Your LiteLLM proxy API key
base_url="http://localhost:4000" # Your proxy URL
)
```
For async operations:
```python
from openai import AsyncOpenAI
client = AsyncOpenAI(
api_key="sk-1234",
base_url="http://localhost:4000"
)
```
---
## Evaluation Management
### Create an Evaluation
Create an evaluation with testing criteria and data source configuration.
#### Example: Sentiment Classification Eval
```python
from openai import OpenAI
client = OpenAI(
api_key="sk-1234",
base_url="http://localhost:4000"
)
# Create evaluation with label model grader
eval_obj = client.evals.create(
name="Sentiment Classification",
data_source_config={
"type": "stored_completions",
"metadata": {"usecase": "chatbot"}
},
testing_criteria=[
{
"type": "label_model",
"model": "gpt-4o-mini",
"input": [
{
"role": "developer",
"content": "Classify the sentiment of the following statement as one of 'positive', 'neutral', or 'negative'"
},
{
"role": "user",
"content": "Statement: {{item.input}}"
}
],
"passing_labels": ["positive"],
"labels": ["positive", "neutral", "negative"],
"name": "Sentiment Grader"
}
]
)
# Note: If you want to use model-specific credentials for this evaluation, you can specify the model name in the extra body parameters.
print(f"Created eval: {eval_obj.id}")
print(f"Eval name: {eval_obj.name}")
```
#### Example: Push Notifications Summarizer Monitoring
This example shows how to monitor prompt changes for regressions in a push notifications summarizer:
```python
from openai import AsyncOpenAI
client = AsyncOpenAI(
api_key="sk-1234",
base_url="http://localhost:4000"
)
# Define data source for stored completions
data_source_config = {
"type": "stored_completions",
"metadata": {
"usecase": "push_notifications_summarizer"
}
}
# Define grader criteria
GRADER_DEVELOPER_PROMPT = """
Label the following push notification summary as either correct or incorrect.
The push notification and the summary will be provided below.
A good push notification summary is concise and snappy.
If it is good, then label it as correct, if not, then incorrect.
"""
GRADER_TEMPLATE_PROMPT = """
Push notifications: {{item.input}}
Summary: {{sample.output_text}}
"""
push_notification_grader = {
"name": "Push Notification Summary Grader",
"type": "label_model",
"model": "gpt-4o-mini",
"input": [
{
"role": "developer",
"content": GRADER_DEVELOPER_PROMPT,
},
{
"role": "user",
"content": GRADER_TEMPLATE_PROMPT,
},
],
"passing_labels": ["correct"],
"labels": ["correct", "incorrect"],
}
# Create the evaluation
eval_result = await client.evals.create(
name="Push Notification Completion Monitoring",
metadata={"description": "This eval monitors completions"},
data_source_config=data_source_config,
testing_criteria=[push_notification_grader],
)
eval_id = eval_result.id
print(f"Created eval: {eval_id}")
```
### List Evaluations
Retrieve a list of all your evaluations with pagination support.
```python
# List all evaluations
evals_response = client.evals.list(
limit=20,
order="desc"
)
for eval in evals_response.data:
print(f"Eval ID: {eval.id}, Name: {eval.name}")
# Check if there are more evals
if evals_response.has_more:
# Fetch next page
next_evals = client.evals.list(
after=evals_response.last_id,
limit=20
)
```
### Get a Specific Evaluation
Retrieve details of a specific evaluation by ID.
```python
eval = client.evals.retrieve(
eval_id="eval_abc123"
)
print(f"Eval ID: {eval.id}")
print(f"Name: {eval.name}")
print(f"Data Source: {eval.data_source_config}")
print(f"Testing Criteria: {eval.testing_criteria}")
```
### Update an Evaluation
Update evaluation metadata or name.
```python
updated_eval = client.evals.update(
eval_id="eval_abc123",
name="Updated Evaluation Name",
metadata={
"version": "2.0",
"updated_by": "user@example.com"
}
)
print(f"Updated eval: {updated_eval.name}")
```
### Delete an Evaluation
Permanently delete an evaluation.
```python
delete_response = client.evals.delete(
eval_id="eval_abc123"
)
print(f"Deleted: {delete_response.deleted}") # True
```
---
## Evaluation Runs
### Create a Run
Execute an evaluation by creating a run. The run processes your data through the model and applies testing criteria.
#### Using Stored Completions
First, generate some test data by making chat completions with metadata:
```python
from openai import AsyncOpenAI
import asyncio
client = AsyncOpenAI(
api_key="sk-1234",
base_url="http://localhost:4000"
)
# Generate test data with different prompt versions
push_notification_data = [
"""
- New message from Sarah: "Can you call me later?"
- Your package has been delivered!
- Flash sale: 20% off electronics for the next 2 hours!
""",
"""
- Weather alert: Thunderstorm expected in your area.
- Reminder: Doctor's appointment at 3 PM.
- John liked your photo on Instagram.
"""
]
PROMPTS = [
(
"""
You are a helpful assistant that summarizes push notifications.
You are given a list of push notifications and you need to collapse them into a single one.
Output only the final summary, nothing else.
""",
"v1"
),
(
"""
You are a helpful assistant that summarizes push notifications.
You are given a list of push notifications and you need to collapse them into a single one.
The summary should be longer than it needs to be and include more information than is necessary.
Output only the final summary, nothing else.
""",
"v2"
)
]
# Create completions with metadata for tracking
tasks = []
for notifications in push_notification_data:
for (prompt, version) in PROMPTS:
tasks.append(client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "developer", "content": prompt},
{"role": "user", "content": notifications},
],
metadata={
"prompt_version": version,
"usecase": "push_notifications_summarizer"
}
))
await asyncio.gather(*tasks)
```
Now create runs to evaluate different prompt versions:
```python
# Grade prompt_version=v1
eval_run_result = await client.evals.runs.create(
eval_id=eval_id,
name="v1-run",
data_source={
"type": "completions",
"source": {
"type": "stored_completions",
"metadata": {
"prompt_version": "v1",
}
}
}
)
print(f"Run ID: {eval_run_result.id}")
print(f"Status: {eval_run_result.status}")
print(f"Report URL: {eval_run_result.report_url}")
# Grade prompt_version=v2
eval_run_result_v2 = await client.evals.runs.create(
eval_id=eval_id,
name="v2-run",
data_source={
"type": "completions",
"source": {
"type": "stored_completions",
"metadata": {
"prompt_version": "v2",
}
}
}
)
print(f"Run ID: {eval_run_result_v2.id}")
print(f"Report URL: {eval_run_result_v2.report_url}")
```
#### Using Completions with Different Models
Test how different models perform on the same inputs:
```python
# Test with GPT-4o using stored completions as input
tasks = []
for prompt_version in ["v1", "v2"]:
tasks.append(client.evals.runs.create(
eval_id=eval_id,
name=f"gpt-4o-run-{prompt_version}",
data_source={
"type": "completions",
"input_messages": {
"type": "item_reference",
"item_reference": "item.input",
},
"model": "gpt-4o",
"source": {
"type": "stored_completions",
"metadata": {
"prompt_version": prompt_version,
}
}
}
))
results = await asyncio.gather(*tasks)
for run in results:
print(f"Report URL: {run.report_url}")
```
### List Runs
Get all runs for a specific evaluation.
```python
# List all runs for an evaluation
runs_response = client.evals.runs.list(
eval_id="eval_abc123",
limit=20,
order="desc"
)
for run in runs_response.data:
print(f"Run ID: {run.id}")
print(f"Status: {run.status}")
print(f"Name: {run.name}")
if run.result_counts:
print(f"Results: {run.result_counts.passed}/{run.result_counts.total} passed")
```
### Get Run Details
Retrieve detailed information about a specific run, including results.
```python
run = client.evals.runs.retrieve(
eval_id="eval_abc123",
run_id="run_def456"
)
print(f"Run ID: {run.id}")
print(f"Status: {run.status}")
print(f"Started: {run.started_at}")
print(f"Completed: {run.completed_at}")
# Check results
if run.result_counts:
print(f"\nOverall Results:")
print(f"Total: {run.result_counts.total}")
print(f"Passed: {run.result_counts.passed}")
print(f"Failed: {run.result_counts.failed}")
print(f"Error: {run.result_counts.errored}")
# Per-criteria results
if run.per_testing_criteria_results:
for criteria_result in run.per_testing_criteria_results:
print(f"\nCriteria {criteria_result.testing_criteria_index}:")
print(f" Passed: {criteria_result.result_counts.passed}")
print(f" Average Score: {criteria_result.average_score}")
```
### Delete a Run
Permanently delete a run and its results.
```python
delete_response = await client.evals.runs.delete(
eval_id="eval_abc123",
run_id="run_def456"
)
print(f"Deleted: {delete_response.deleted}") # True
print(f"Run ID: {delete_response.run_id}")
```
+1
View File
@@ -573,6 +573,7 @@ const sidebars = {
"proxy/managed_finetuning",
]
},
"evals_api",
"generateContent",
"apply_guardrail",
"bedrock_invoke",
+53
View File
@@ -1152,6 +1152,28 @@ from .skills.main import (
delete_skill,
adelete_skill,
)
from .evals.main import (
create_eval,
acreate_eval,
list_evals,
alist_evals,
get_eval,
aget_eval,
delete_eval,
adelete_eval,
cancel_eval,
acancel_eval,
create_run,
acreate_run,
list_runs,
alist_runs,
get_run,
aget_run,
delete_run,
adelete_run,
cancel_run,
acancel_run,
)
from .integrations import *
from .llms.custom_httpx.async_client_cleanup import close_litellm_async_clients
from .exceptions import (
@@ -1732,6 +1754,37 @@ def __getattr__(name: str) -> Any:
_globals["_service_logger"] = litellm._service_logger
return _globals["_service_logger"]
# Lazy load evals module functions
if name in ["acreate_eval", "alist_evals", "aget_eval", "aupdate_eval", "adelete_eval", "acancel_eval",
"create_eval", "list_evals", "get_eval", "update_eval", "delete_eval", "cancel_eval",
"acreate_run", "alist_runs", "aget_run", "acancel_run", "adelete_run",
"create_run", "list_runs", "get_run", "cancel_run", "delete_run"]:
from litellm.evals.main import (
acreate_eval,
alist_evals,
aget_eval,
aupdate_eval,
adelete_eval,
acancel_eval,
create_eval,
list_evals,
get_eval,
update_eval,
delete_eval,
cancel_eval,
acreate_run,
alist_runs,
aget_run,
acancel_run,
adelete_run,
create_run,
list_runs,
get_run,
cancel_run,
delete_run,
)
return locals()[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+33
View File
@@ -0,0 +1,33 @@
"""
Evals API operations
"""
from .main import (
acancel_eval,
acreate_eval,
adelete_eval,
aget_eval,
alist_evals,
aupdate_eval,
cancel_eval,
create_eval,
delete_eval,
get_eval,
list_evals,
update_eval,
)
__all__ = [
"acreate_eval",
"alist_evals",
"aget_eval",
"aupdate_eval",
"adelete_eval",
"acancel_eval",
"create_eval",
"list_evals",
"get_eval",
"update_eval",
"delete_eval",
"cancel_eval",
]
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
"""
Base configuration for Evals API
"""
from .transformation import BaseEvalsAPIConfig
__all__ = ["BaseEvalsAPIConfig"]
@@ -0,0 +1,542 @@
"""
Base configuration class for Evals API
"""
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
import httpx
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.types.llms.openai_evals import (
CancelEvalResponse,
CancelRunResponse,
CreateEvalRequest,
CreateRunRequest,
DeleteEvalResponse,
Eval,
ListEvalsParams,
ListEvalsResponse,
ListRunsParams,
ListRunsResponse,
Run,
RunDeleteResponse,
UpdateEvalRequest,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
LiteLLMLoggingObj = _LiteLLMLoggingObj
else:
LiteLLMLoggingObj = Any
class BaseEvalsAPIConfig(ABC):
"""Base configuration for Evals API providers"""
def __init__(self):
pass
@property
@abstractmethod
def custom_llm_provider(self) -> LlmProviders:
pass
@abstractmethod
def validate_environment(
self, headers: dict, litellm_params: Optional[GenericLiteLLMParams]
) -> dict:
"""
Validate and update headers with provider-specific requirements
Args:
headers: Base headers dictionary
litellm_params: LiteLLM parameters
Returns:
Updated headers dictionary
"""
return headers
@abstractmethod
def get_complete_url(
self,
api_base: Optional[str],
endpoint: str,
eval_id: Optional[str] = None,
) -> str:
"""
Get the complete URL for the API request
Args:
api_base: Base API URL
endpoint: API endpoint (e.g., 'evals', 'evals/{id}')
eval_id: Optional eval ID for specific eval operations
Returns:
Complete URL
"""
if api_base is None:
raise ValueError("api_base is required")
return f"{api_base}/v1/{endpoint}"
@abstractmethod
def transform_create_eval_request(
self,
create_request: CreateEvalRequest,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Dict:
"""
Transform create eval request to provider-specific format
Args:
create_request: Eval creation parameters
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Provider-specific request body
"""
pass
@abstractmethod
def transform_create_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Eval:
"""
Transform provider response to Eval object
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
Eval object
"""
pass
@abstractmethod
def transform_list_evals_request(
self,
list_params: ListEvalsParams,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""
Transform list evals request parameters
Args:
list_params: List parameters (pagination, filters)
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, query_params)
"""
pass
@abstractmethod
def transform_list_evals_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ListEvalsResponse:
"""
Transform provider response to ListEvalsResponse
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
ListEvalsResponse object
"""
pass
@abstractmethod
def transform_get_eval_request(
self,
eval_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""
Transform get eval request
Args:
eval_id: Eval ID
api_base: Base API URL
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, headers)
"""
pass
@abstractmethod
def transform_get_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Eval:
"""
Transform provider response to Eval object
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
Eval object
"""
pass
@abstractmethod
def transform_update_eval_request(
self,
eval_id: str,
update_request: UpdateEvalRequest,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""
Transform update eval request
Args:
eval_id: Eval ID
update_request: Update parameters
api_base: Base API URL
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, headers, body)
"""
pass
@abstractmethod
def transform_update_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Eval:
"""
Transform provider response to Eval object
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
Eval object
"""
pass
@abstractmethod
def transform_delete_eval_request(
self,
eval_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""
Transform delete eval request
Args:
eval_id: Eval ID
api_base: Base API URL
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, headers)
"""
pass
@abstractmethod
def transform_delete_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> DeleteEvalResponse:
"""
Transform provider response to DeleteEvalResponse
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
DeleteEvalResponse object
"""
pass
@abstractmethod
def transform_cancel_eval_request(
self,
eval_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""
Transform cancel eval request
Args:
eval_id: Eval ID
api_base: Base API URL
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, headers, body)
"""
pass
@abstractmethod
def transform_cancel_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> CancelEvalResponse:
"""
Transform provider response to CancelEvalResponse
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
CancelEvalResponse object
"""
pass
# Run API Transformations
@abstractmethod
def transform_create_run_request(
self,
eval_id: str,
create_request: CreateRunRequest,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""
Transform create run request to provider-specific format
Args:
eval_id: Eval ID
create_request: Run creation parameters
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, request_body)
"""
pass
@abstractmethod
def transform_create_run_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Run:
"""
Transform provider response to Run object
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
Run object
"""
pass
@abstractmethod
def transform_list_runs_request(
self,
eval_id: str,
list_params: ListRunsParams,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""
Transform list runs request parameters
Args:
eval_id: Eval ID
list_params: List parameters (pagination, filters)
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, query_params)
"""
pass
@abstractmethod
def transform_list_runs_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ListRunsResponse:
"""
Transform provider response to ListRunsResponse
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
ListRunsResponse object
"""
pass
@abstractmethod
def transform_get_run_request(
self,
eval_id: str,
run_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""
Transform get run request
Args:
eval_id: Eval ID
run_id: Run ID
api_base: Base API URL
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, headers)
"""
pass
@abstractmethod
def transform_get_run_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Run:
"""
Transform provider response to Run object
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
Run object
"""
pass
@abstractmethod
def transform_cancel_run_request(
self,
eval_id: str,
run_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""
Transform cancel run request
Args:
eval_id: Eval ID
run_id: Run ID
api_base: Base API URL
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, headers, body)
"""
pass
@abstractmethod
def transform_cancel_run_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> CancelRunResponse:
"""
Transform provider response to CancelRunResponse
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
CancelRunResponse object
"""
pass
@abstractmethod
def transform_delete_run_request(
self,
eval_id: str,
run_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""
Transform delete run request
Args:
eval_id: Eval ID
run_id: Run ID
api_base: Base API URL
litellm_params: LiteLLM parameters
headers: Request headers
Returns:
Tuple of (url, headers, body)
"""
pass
@abstractmethod
def transform_delete_run_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> "RunDeleteResponse":
"""
Transform provider response to RunDeleteResponse
Args:
raw_response: Raw HTTP response
logging_obj: Logging object
Returns:
RunDeleteResponse object
"""
pass
def get_error_class(
self,
error_message: str,
status_code: int,
headers: dict,
) -> Exception:
"""Get appropriate error class for the provider."""
return BaseLLMException(
status_code=status_code,
message=error_message,
headers=headers,
)
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
"""
OpenAI Evals API configuration
"""
from .transformation import OpenAIEvalsConfig
__all__ = ["OpenAIEvalsConfig"]
+426
View File
@@ -0,0 +1,426 @@
"""
OpenAI Evals API configuration and transformations
"""
from typing import Any, Dict, Optional, Tuple
import httpx
from litellm._logging import verbose_logger
from litellm.llms.base_llm.evals.transformation import (
BaseEvalsAPIConfig,
LiteLLMLoggingObj,
)
from litellm.types.llms.openai_evals import (
CancelEvalResponse,
CancelRunResponse,
CreateEvalRequest,
CreateRunRequest,
DeleteEvalResponse,
Eval,
ListEvalsParams,
ListEvalsResponse,
ListRunsParams,
ListRunsResponse,
Run,
RunDeleteResponse,
UpdateEvalRequest,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
class OpenAIEvalsConfig(BaseEvalsAPIConfig):
"""OpenAI-specific Evals API configuration"""
@property
def custom_llm_provider(self) -> LlmProviders:
return LlmProviders.OPENAI
def validate_environment(
self, headers: dict, litellm_params: Optional[GenericLiteLLMParams]
) -> dict:
"""Add OpenAI-specific headers"""
import litellm
from litellm.secret_managers.main import get_secret_str
# Get API key following OpenAI pattern
api_key = None
if litellm_params:
api_key = litellm_params.api_key
api_key = (
api_key
or litellm.api_key
or litellm.openai_key
or get_secret_str("OPENAI_API_KEY")
)
if not api_key:
raise ValueError("OPENAI_API_KEY is required for Evals API")
# Add required headers
headers["Authorization"] = f"Bearer {api_key}"
headers["Content-Type"] = "application/json"
return headers
def get_complete_url(
self,
api_base: Optional[str],
endpoint: str,
eval_id: Optional[str] = None,
) -> str:
"""Get complete URL for OpenAI Evals API"""
if api_base is None:
api_base = "https://api.openai.com"
if eval_id:
return f"{api_base}/v1/evals/{eval_id}"
return f"{api_base}/v1/{endpoint}"
def transform_create_eval_request(
self,
create_request: CreateEvalRequest,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Dict:
"""Transform create eval request for OpenAI"""
verbose_logger.debug("Transforming create eval request: %s", create_request)
# OpenAI expects the request body directly
request_body = {k: v for k, v in create_request.items() if v is not None}
return request_body
def transform_create_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Eval:
"""Transform OpenAI response to Eval object"""
response_json = raw_response.json()
verbose_logger.debug("Transforming create eval response: %s", response_json)
return Eval(**response_json)
def transform_list_evals_request(
self,
list_params: ListEvalsParams,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""Transform list evals request for OpenAI"""
api_base = "https://api.openai.com"
if litellm_params and litellm_params.api_base:
api_base = litellm_params.api_base
url = self.get_complete_url(api_base=api_base, endpoint="evals")
# Build query parameters
query_params: Dict[str, Any] = {}
if "limit" in list_params and list_params["limit"]:
query_params["limit"] = list_params["limit"]
if "after" in list_params and list_params["after"]:
query_params["after"] = list_params["after"]
if "before" in list_params and list_params["before"]:
query_params["before"] = list_params["before"]
if "order" in list_params and list_params["order"]:
query_params["order"] = list_params["order"]
if "order_by" in list_params and list_params["order_by"]:
query_params["order_by"] = list_params["order_by"]
verbose_logger.debug(
"List evals request made to OpenAI Evals endpoint with params: %s",
query_params,
)
return url, query_params
def transform_list_evals_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ListEvalsResponse:
"""Transform OpenAI response to ListEvalsResponse"""
response_json = raw_response.json()
verbose_logger.debug("Transforming list evals response: %s", response_json)
return ListEvalsResponse(**response_json)
def transform_get_eval_request(
self,
eval_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""Transform get eval request for OpenAI"""
url = self.get_complete_url(
api_base=api_base, endpoint="evals", eval_id=eval_id
)
verbose_logger.debug("Get eval request - URL: %s", url)
return url, headers
def transform_get_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Eval:
"""Transform OpenAI response to Eval object"""
response_json = raw_response.json()
verbose_logger.debug("Transforming get eval response: %s", response_json)
return Eval(**response_json)
def transform_update_eval_request(
self,
eval_id: str,
update_request: UpdateEvalRequest,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""Transform update eval request for OpenAI"""
url = self.get_complete_url(
api_base=api_base, endpoint="evals", eval_id=eval_id
)
# Build request body
request_body = {k: v for k, v in update_request.items() if v is not None}
verbose_logger.debug(
"Update eval request - URL: %s, body: %s", url, request_body
)
return url, headers, request_body
def transform_update_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Eval:
"""Transform OpenAI response to Eval object"""
response_json = raw_response.json()
verbose_logger.debug("Transforming update eval response: %s", response_json)
return Eval(**response_json)
def transform_delete_eval_request(
self,
eval_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""Transform delete eval request for OpenAI"""
url = self.get_complete_url(
api_base=api_base, endpoint="evals", eval_id=eval_id
)
verbose_logger.debug("Delete eval request - URL: %s", url)
return url, headers
def transform_delete_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> DeleteEvalResponse:
"""Transform OpenAI response to DeleteEvalResponse"""
response_json = raw_response.json()
verbose_logger.debug("Transforming delete eval response: %s", response_json)
return DeleteEvalResponse(**response_json)
def transform_cancel_eval_request(
self,
eval_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""Transform cancel eval request for OpenAI"""
url = f"{self.get_complete_url(api_base=api_base, endpoint='evals', eval_id=eval_id)}/cancel"
# Empty body for cancel request
request_body: Dict[str, Any] = {}
verbose_logger.debug("Cancel eval request - URL: %s", url)
return url, headers, request_body
def transform_cancel_eval_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> CancelEvalResponse:
"""Transform OpenAI response to CancelEvalResponse"""
response_json = raw_response.json()
verbose_logger.debug("Transforming cancel eval response: %s", response_json)
return CancelEvalResponse(**response_json)
# Run API Transformations
def transform_create_run_request(
self,
eval_id: str,
create_request: CreateRunRequest,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""Transform create run request for OpenAI"""
api_base = "https://api.openai.com"
if litellm_params and litellm_params.api_base:
api_base = litellm_params.api_base
url = f"{api_base}/v1/evals/{eval_id}/runs"
# Build request body
request_body = {k: v for k, v in create_request.items() if v is not None}
verbose_logger.debug(
"Create run request - URL: %s, body: %s", url, request_body
)
return url, request_body
def transform_create_run_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Run:
"""Transform OpenAI response to Run object"""
response_json = raw_response.json()
verbose_logger.debug("Transforming create run response: %s", response_json)
return Run(**response_json)
def transform_list_runs_request(
self,
eval_id: str,
list_params: ListRunsParams,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""Transform list runs request for OpenAI"""
api_base = "https://api.openai.com"
if litellm_params and litellm_params.api_base:
api_base = litellm_params.api_base
url = f"{api_base}/v1/evals/{eval_id}/runs"
# Build query parameters
query_params: Dict[str, Any] = {}
if "limit" in list_params and list_params["limit"]:
query_params["limit"] = list_params["limit"]
if "after" in list_params and list_params["after"]:
query_params["after"] = list_params["after"]
if "before" in list_params and list_params["before"]:
query_params["before"] = list_params["before"]
if "order" in list_params and list_params["order"]:
query_params["order"] = list_params["order"]
verbose_logger.debug(
"List runs request made to OpenAI Evals endpoint with params: %s",
query_params,
)
return url, query_params
def transform_list_runs_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ListRunsResponse:
"""Transform OpenAI response to ListRunsResponse"""
response_json = raw_response.json()
verbose_logger.debug("Transforming list runs response: %s", response_json)
return ListRunsResponse(**response_json)
def transform_get_run_request(
self,
eval_id: str,
run_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""Transform get run request for OpenAI"""
url = f"{api_base}/v1/evals/{eval_id}/runs/{run_id}"
verbose_logger.debug("Get run request - URL: %s", url)
return url, headers
def transform_get_run_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> Run:
"""Transform OpenAI response to Run object"""
response_json = raw_response.json()
verbose_logger.debug("Transforming get run response: %s", response_json)
return Run(**response_json)
def transform_cancel_run_request(
self,
eval_id: str,
run_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""Transform cancel run request for OpenAI"""
url = f"{api_base}/v1/evals/{eval_id}/runs/{run_id}/cancel"
# Empty body for cancel request
request_body: Dict[str, Any] = {}
verbose_logger.debug("Cancel run request - URL: %s", url)
return url, headers, request_body
def transform_cancel_run_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> CancelRunResponse:
"""Transform OpenAI response to CancelRunResponse"""
response_json = raw_response.json()
verbose_logger.debug("Transforming cancel run response: %s", response_json)
return CancelRunResponse(**response_json)
def transform_delete_run_request(
self,
eval_id: str,
run_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict, Dict]:
"""Transform delete run request for OpenAI"""
url = f"{api_base}/v1/evals/{eval_id}/runs/{run_id}"
# Empty body for delete request
request_body: Dict[str, Any] = {}
verbose_logger.debug("Delete run request - URL: %s", url)
return url, headers, request_body
def transform_delete_run_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> RunDeleteResponse:
"""Transform OpenAI response to RunDeleteResponse"""
response_json = raw_response.json()
verbose_logger.debug("Transforming delete run response: %s", response_json)
return RunDeleteResponse(**response_json)
@@ -526,6 +526,17 @@ class ProxyBaseLLMRequestProcessing:
"acancel_interaction",
"asend_message",
"call_mcp_tool",
"acreate_eval",
"alist_evals",
"aget_eval",
"aupdate_eval",
"adelete_eval",
"acancel_eval",
"acreate_run",
"alist_runs",
"aget_run",
"acancel_run",
"adelete_run",
],
version: Optional[str] = None,
user_model: Optional[str] = None,
@@ -708,6 +719,17 @@ class ProxyBaseLLMRequestProcessing:
"acancel_interaction",
"acancel_batch",
"afile_delete",
"acreate_eval",
"alist_evals",
"aget_eval",
"aupdate_eval",
"adelete_eval",
"acancel_eval",
"acreate_run",
"alist_runs",
"aget_run",
"acancel_run",
"adelete_run",
],
proxy_logging_obj: ProxyLogging,
general_settings: dict,
@@ -0,0 +1,7 @@
"""
OpenAI Evals API endpoints
"""
from .endpoints import router
__all__ = ["router"]
File diff suppressed because it is too large Load Diff
+2
View File
@@ -411,6 +411,7 @@ from litellm.proxy.management_endpoints.user_agent_analytics_endpoints import (
from litellm.proxy.management_helpers.audit_logs import create_audit_log_for_update
from litellm.proxy.middleware.prometheus_auth_middleware import PrometheusAuthMiddleware
from litellm.proxy.ocr_endpoints.endpoints import router as ocr_router
from litellm.proxy.openai_evals_endpoints.endpoints import router as evals_router
from litellm.proxy.openai_files_endpoints.files_endpoints import (
router as openai_files_router,
)
@@ -12426,6 +12427,7 @@ app.include_router(llm_passthrough_router)
app.include_router(mcp_management_router)
app.include_router(anthropic_router)
app.include_router(anthropic_skills_router)
app.include_router(evals_router)
app.include_router(claude_code_marketplace_router)
app.include_router(google_router)
app.include_router(langfuse_router)
+59
View File
@@ -73,6 +73,19 @@ ROUTE_ENDPOINT_MAPPING = {
"aget_interaction": "/interactions/{interaction_id}",
"adelete_interaction": "/interactions/{interaction_id}",
"acancel_interaction": "/interactions/{interaction_id}/cancel",
# OpenAI Evals API routes
"acreate_eval": "/evals",
"alist_evals": "/evals",
"aget_eval": "/evals/{eval_id}",
"aupdate_eval": "/evals/{eval_id}",
"adelete_eval": "/evals/{eval_id}",
"acancel_eval": "/evals/{eval_id}/cancel",
# OpenAI Evals Runs API routes
"acreate_run": "/evals/{eval_id}/runs",
"alist_runs": "/evals/{eval_id}/runs",
"aget_run": "/evals/{eval_id}/runs/{run_id}",
"acancel_run": "/evals/{eval_id}/runs/{run_id}/cancel",
"adelete_run": "/evals/{eval_id}/runs/{run_id}",
}
@@ -190,6 +203,17 @@ async def route_request(
"acancel_interaction",
"acancel_batch",
"afile_delete",
"acreate_eval",
"alist_evals",
"aget_eval",
"aupdate_eval",
"adelete_eval",
"acancel_eval",
"acreate_run",
"alist_runs",
"aget_run",
"acancel_run",
"adelete_run",
],
):
"""
@@ -256,6 +280,41 @@ async def route_request(
else:
return getattr(litellm, f"{route_type}")(**data)
elif llm_router is not None:
# Evals API: always route to litellm directly (not through router)
# But extract model credentials if a model is provided
if route_type in [
"acreate_eval",
"alist_evals",
"aget_eval",
"aupdate_eval",
"adelete_eval",
"acancel_eval",
"acreate_run",
"alist_runs",
"aget_run",
"acancel_run",
"adelete_run",
]:
# If a model is provided, get its credentials from the router
model = data.get("model")
if model and llm_router:
try:
# Try to get deployment credentials for this model
deployment_creds = llm_router.get_deployment_credentials(model_id=model)
if not deployment_creds:
# Try by model group name
deployment = llm_router.get_deployment_by_model_group_name(model_group_name=model)
if deployment and deployment.litellm_params:
deployment_creds = deployment.litellm_params.model_dump(exclude_none=True)
# If we found credentials, merge them into data (but don't override user-provided values)
if deployment_creds:
data.update(deployment_creds)
except Exception:
# If we can't get deployment creds, continue without them
pass
return getattr(litellm, f"{route_type}")(**data)
# Skip model-based routing for container operations
if route_type in [
"acreate_container",
+417
View File
@@ -0,0 +1,417 @@
"""
Type definitions for OpenAI Evals API
"""
from typing import Any, Dict, List, Literal, Optional, Union
from pydantic import BaseModel, Field
from typing_extensions import Required, TypedDict
# Evals API Request Types
class DataSourceConfigCustom(TypedDict, total=False):
"""Data source configuration for custom data sources"""
type: Required[Literal["custom"]]
"""Data source type - custom"""
item_schema: Required[Dict[str, Any]]
"""JSON schema describing the structure of each row in the dataset"""
include_sample_schema: Optional[bool]
"""Whether eval expects sample schema population"""
class DataSourceConfigLogs(TypedDict, total=False):
"""Data source configuration for logs-based evals"""
type: Required[Literal["logs"]]
"""Data source type - logs"""
metadata: Optional[Dict[str, Any]]
"""Optional metadata for filtering logs"""
class DataSourceConfigStoredCompletions(TypedDict, total=False):
"""Data source configuration for stored completions (deprecated)"""
type: Required[Literal["stored_completions"]]
"""Data source type - stored_completions (deprecated)"""
metadata: Optional[Dict[str, Any]]
"""Optional metadata for filtering stored completions"""
DataSourceConfig = Union[DataSourceConfigCustom, DataSourceConfigLogs, DataSourceConfigStoredCompletions]
class LLMAsJudgeGraderConfig(TypedDict, total=False):
"""Configuration for LLM as judge grading"""
type: Required[Literal["llm_as_judge"]]
"""Grader type - LLM as judge"""
model: Optional[str]
"""Model to use as judge (e.g., 'gpt-4')"""
prompt: Optional[str]
"""Custom prompt for the judge model"""
class GroundTruthGraderConfig(TypedDict, total=False):
"""Configuration for ground truth grading"""
type: Required[Literal["ground_truth"]]
"""Grader type - ground truth comparison"""
metric: Optional[Literal["exact_match", "f1_score", "bleu"]]
"""Metric to use for comparison"""
class CustomGraderConfig(TypedDict, total=False):
"""Configuration for custom grading function"""
type: Required[Literal["custom"]]
"""Grader type - custom"""
function_id: Required[str]
"""ID of the custom grading function"""
GraderConfig = Union[LLMAsJudgeGraderConfig, GroundTruthGraderConfig, CustomGraderConfig]
class CreateEvalRequest(TypedDict, total=False):
"""Request parameters for creating an evaluation"""
name: Optional[str]
"""The name of the evaluation"""
data_source_config: Required[DataSourceConfig]
"""Configuration for the data source"""
testing_criteria: Required[List[GraderConfig]]
"""List of graders for all eval runs"""
metadata: Optional[Dict[str, Any]]
"""Set of 16 key-value pairs that can be attached to an object (max 64 char keys, 512 char values)"""
class UpdateEvalRequest(TypedDict, total=False):
"""Request parameters for updating an evaluation"""
name: Optional[str]
"""Updated name"""
metadata: Optional[Dict[str, Any]]
"""Updated metadata"""
class ListEvalsParams(TypedDict, total=False):
"""Query parameters for listing evaluations"""
limit: Optional[int]
"""Number of results to return per page. Maximum value is 100. Defaults to 20."""
after: Optional[str]
"""Cursor for pagination - returns evals after this ID"""
before: Optional[str]
"""Cursor for pagination - returns evals before this ID"""
order: Optional[Literal["asc", "desc"]]
"""Sort order for results. Defaults to 'desc'."""
order_by: Optional[Literal["created_at", "updated_at"]]
"""Field to sort by. Defaults to 'created_at'."""
# Evals API Response Types
class Eval(BaseModel):
"""Represents an evaluation from the OpenAI Evals API"""
id: str
"""Unique identifier for the evaluation"""
object: str = "eval"
"""Object type, always 'eval'"""
created_at: int
"""Unix timestamp of when the evaluation was created"""
updated_at: Optional[int] = None
"""Unix timestamp of when the evaluation was last updated"""
name: Optional[str] = None
"""The name of the evaluation"""
data_source_config: Dict[str, Any]
"""Configuration for the data source"""
testing_criteria: List[Dict[str, Any]]
"""List of graders for the evaluation"""
metadata: Optional[Dict[str, Any]] = None
"""Additional metadata"""
class ListEvalsResponse(BaseModel):
"""Response from listing evaluations"""
object: str = "list"
"""Object type, always 'list'"""
data: List[Eval]
"""List of evaluations"""
first_id: Optional[str] = None
"""ID of the first evaluation in the list"""
last_id: Optional[str] = None
"""ID of the last evaluation in the list"""
has_more: bool = False
"""Whether there are more evaluations available"""
class DeleteEvalResponse(BaseModel):
"""Response from deleting an evaluation"""
eval_id: str
"""The ID of the deleted evaluation"""
object: str = "eval.deleted"
"""Object type, always 'eval.deleted'"""
deleted: bool
"""Whether the evaluation was successfully deleted"""
class CancelEvalResponse(BaseModel):
"""Response from cancelling an evaluation"""
id: str
"""The ID of the cancelled evaluation"""
object: str = "eval"
"""Object type, always 'eval'"""
status: Literal["cancelled"]
"""Status after cancellation, always 'cancelled'"""
# Run API Request Types
class DataSourceDatasetConfig(TypedDict, total=False):
"""Data source configuration for dataset-based runs"""
type: Required[Literal["dataset"]]
"""Data source type - dataset"""
dataset_id: Required[str]
"""ID of the dataset to use for the run"""
class DataSourceSampleSetConfig(TypedDict, total=False):
"""Data source configuration for sample set-based runs"""
type: Required[Literal["sample_set"]]
"""Data source type - sample_set"""
sample_set_id: Required[str]
"""ID of the sample set to use for the run"""
class DataSourceInlineConfig(TypedDict, total=False):
"""Data source configuration for inline samples"""
type: Required[Literal["inline"]]
"""Data source type - inline"""
samples: Required[List[Dict[str, Any]]]
"""List of inline samples to use for the run"""
RunDataSourceConfig = Union[
DataSourceDatasetConfig, DataSourceSampleSetConfig, DataSourceInlineConfig
]
class CompletionConfig(TypedDict, total=False):
"""Configuration for model completions in a run"""
model: Required[str]
"""Model to use for completions"""
temperature: Optional[float]
"""Sampling temperature (0-2)"""
max_tokens: Optional[int]
"""Maximum tokens to generate"""
top_p: Optional[float]
"""Nucleus sampling parameter"""
frequency_penalty: Optional[float]
"""Frequency penalty (-2.0 to 2.0)"""
presence_penalty: Optional[float]
"""Presence penalty (-2.0 to 2.0)"""
class CreateRunRequest(TypedDict, total=False):
"""Request parameters for creating a run"""
data_source: Required[Dict[str, Any]]
"""Data source configuration for the run (can be jsonl, completions, or responses type)"""
name: Optional[str]
"""Optional name for the run"""
metadata: Optional[Dict[str, Any]]
"""Optional metadata for the run"""
class ListRunsParams(TypedDict, total=False):
"""Query parameters for listing runs"""
limit: Optional[int]
"""Number of results to return per page. Maximum value is 100. Defaults to 20."""
after: Optional[str]
"""Cursor for pagination - returns runs after this ID"""
before: Optional[str]
"""Cursor for pagination - returns runs before this ID"""
order: Optional[Literal["asc", "desc"]]
"""Sort order for results. Defaults to 'desc'."""
# Run API Response Types
class ResultCounts(BaseModel):
"""Result counts for a run"""
total: int
"""Total number of results"""
passed: int = 0
"""Number of passed results"""
failed: int = 0
"""Number of failed results"""
error: int = 0
"""Number of error results"""
class PerTestingCriteriaResult(BaseModel):
"""Results for a specific testing criteria"""
testing_criteria_index: int
"""Index of the testing criteria"""
result_counts: ResultCounts
"""Result counts for this criteria"""
average_score: Optional[float] = None
"""Average score for this criteria"""
class Run(BaseModel):
"""Represents a run from the OpenAI Evals API"""
id: str
"""Unique identifier for the run"""
object: str = "eval.run"
"""Object type, always 'eval.run'"""
created_at: int
"""Unix timestamp of when the run was created"""
status: Literal["queued", "running", "completed", "failed", "cancelled"]
"""Current status of the run"""
data_source: Dict[str, Any]
"""Data source configuration used for the run"""
eval_id: str
"""ID of the evaluation this run belongs to"""
name: Optional[str] = None
"""Name of the run"""
started_at: Optional[int] = None
"""Unix timestamp of when the run started"""
completed_at: Optional[int] = None
"""Unix timestamp of when the run completed"""
model: Optional[str] = None
"""Model used for the run, if any"""
per_model_usage: Optional[Any] = None
"""Model usage details per model, if available"""
per_testing_criteria_results: Optional[List[PerTestingCriteriaResult]] = None
"""Per-criteria results"""
report_url: Optional[str] = None
"""URL for the evaluation report"""
result_counts: Optional[Dict[str, int]] = None
"""Aggregate result counts (e.g., {"passed": 0, "failed": 0, "errored": 0, "total": 0})"""
shared_with_openai: Optional[bool] = None
"""Whether run is shared with OpenAI"""
metadata: Optional[Dict[str, Any]] = None
"""Additional metadata"""
error: Optional[Dict[str, Any]] = None
"""Error details if the run failed"""
class ListRunsResponse(BaseModel):
"""Response from listing runs"""
object: str = "list"
"""Object type, always 'list'"""
data: List[Run]
"""List of runs"""
first_id: Optional[str] = None
"""ID of the first run in the list"""
last_id: Optional[str] = None
"""ID of the last run in the list"""
has_more: bool = False
"""Whether there are more runs available"""
class CancelRunResponse(BaseModel):
"""Response from cancelling a run"""
id: str
"""The ID of the cancelled run"""
object: str = "eval.run"
"""Object type, always 'eval.run'"""
status: Literal["cancelled"]
"""Status after cancellation, always 'cancelled'"""
class RunDeleteResponse(BaseModel):
"""Response from deleting a run"""
run_id: str
"""The ID of the deleted run"""
object: Optional[str] = "eval.run.deleted"
"""Object type, always 'eval.run.deleted'"""
deleted: Optional[bool] = True
"""Whether the run was successfully deleted"""
+20
View File
@@ -380,6 +380,7 @@ if TYPE_CHECKING:
from litellm.llms.base_llm.chat.transformation import BaseConfig
from litellm.llms.base_llm.completion.transformation import BaseTextCompletionConfig
from litellm.llms.base_llm.evals.transformation import BaseEvalsAPIConfig
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
from litellm.llms.base_llm.skills.transformation import BaseSkillsAPIConfig
@@ -8282,6 +8283,25 @@ class ProviderConfigManager:
return litellm.AnthropicSkillsConfig()
return None
@staticmethod
def get_provider_evals_api_config(
provider: LlmProviders,
) -> Optional["BaseEvalsAPIConfig"]:
"""
Get provider-specific Evals API configuration
Args:
provider: The LLM provider
Returns:
Provider-specific Evals API config or None
"""
if litellm.LlmProviders.OPENAI == provider:
from litellm.llms.openai.evals.transformation import OpenAIEvalsConfig
return OpenAIEvalsConfig()
return None
@staticmethod
def get_provider_text_completion_config(
model: str,
+237
View File
@@ -0,0 +1,237 @@
"""
Tests for Evals API operations across providers
"""
import os
import sys
from abc import ABC, abstractmethod
from typing import Optional
import pytest
sys.path.insert(0, os.path.abspath("../.."))
import litellm
from litellm.types.llms.openai_evals import (
CancelEvalResponse,
DeleteEvalResponse,
Eval,
ListEvalsResponse,
)
class BaseEvalsAPITest(ABC):
"""
Base test class for Evals API operations.
Tests create, list, get, update, delete, and cancel operations.
"""
@abstractmethod
def get_custom_llm_provider(self) -> str:
"""Return the provider name (e.g., 'openai')"""
pass
@abstractmethod
def get_api_key(self) -> Optional[str]:
"""Return the API key for the provider"""
pass
@abstractmethod
def get_api_base(self) -> Optional[str]:
"""Return the API base URL for the provider"""
pass
def test_create_eval(self):
"""
Test creating an evaluation.
"""
import time
custom_llm_provider = self.get_custom_llm_provider()
api_key = self.get_api_key()
api_base = self.get_api_base()
if not api_key:
pytest.skip(f"No API key provided for {custom_llm_provider}")
litellm.set_verbose = True
# Create eval with stored_completions data source
unique_name = f"Test Eval {int(time.time())}"
response = litellm.create_eval(
name=unique_name,
data_source_config={
"type": "stored_completions",
"metadata": {"usecase": "chatbot"},
},
testing_criteria=[
{
"type": "label_model",
"model": "gpt-4o",
"input": [
{
"role": "developer",
"content": "Classify the sentiment as 'positive' or 'negative'",
},
{"role": "user", "content": "Statement: {{item.input}}"},
],
"passing_labels": ["positive"],
"labels": ["positive", "negative"],
"name": "Sentiment grader",
}
],
custom_llm_provider=custom_llm_provider,
api_key=api_key,
api_base=api_base,
)
assert response is not None
assert isinstance(response, Eval)
assert response.id is not None
assert response.name == unique_name
print(f"Created eval: {response}")
print(f"Eval ID: {response.id}")
def test_list_evals(self):
"""
Test listing evaluations.
"""
custom_llm_provider = self.get_custom_llm_provider()
api_key = self.get_api_key()
api_base = self.get_api_base()
if not api_key:
pytest.skip(f"No API key provided for {custom_llm_provider}")
litellm.set_verbose = True
response = litellm.list_evals(
limit=10,
custom_llm_provider=custom_llm_provider,
api_key=api_key,
api_base=api_base,
)
assert response is not None
assert isinstance(response, ListEvalsResponse)
assert hasattr(response, "data")
assert hasattr(response, "has_more")
print(f"Listed evals: {len(response.data)} evaluations")
def test_get_eval(self):
"""
Test getting a specific evaluation by ID.
"""
custom_llm_provider = self.get_custom_llm_provider()
api_key = self.get_api_key()
api_base = self.get_api_base()
if not api_key:
pytest.skip(f"No API key provided for {custom_llm_provider}")
litellm.set_verbose = True
# First list existing evals to get an ID
list_response = litellm.list_evals(
limit=1,
custom_llm_provider=custom_llm_provider,
api_key=api_key,
api_base=api_base,
)
assert isinstance(list_response, ListEvalsResponse)
if list_response.data and len(list_response.data) > 0:
eval_id = list_response.data[0].id
print(f"Testing with eval ID: {eval_id}")
# Get the eval
response = litellm.get_eval(
eval_id=eval_id,
custom_llm_provider=custom_llm_provider,
api_key=api_key,
api_base=api_base,
)
assert response is not None
assert isinstance(response, Eval)
assert response.id == eval_id
print(f"Retrieved eval: {response}")
else:
pytest.skip("No existing evals to test with")
def test_update_eval(self):
"""
Test updating an evaluation.
"""
import time
custom_llm_provider = self.get_custom_llm_provider()
api_key = self.get_api_key()
api_base = self.get_api_base()
if not api_key:
pytest.skip(f"No API key provided for {custom_llm_provider}")
litellm.set_verbose = True
# First list existing evals
list_response = litellm.list_evals(
limit=1,
custom_llm_provider=custom_llm_provider,
api_key=api_key,
api_base=api_base,
)
assert isinstance(list_response, ListEvalsResponse)
if list_response.data and len(list_response.data) > 0:
eval_id = list_response.data[0].id
updated_name = f"Updated Eval {int(time.time())}"
# Update the eval
response = litellm.update_eval(
eval_id=eval_id,
name=updated_name,
custom_llm_provider=custom_llm_provider,
api_key=api_key,
api_base=api_base,
)
assert response is not None
assert isinstance(response, Eval)
assert response.id == eval_id
assert response.name == updated_name
print(f"Updated eval: {response}")
else:
pytest.skip("No existing evals to test with")
def test_delete_eval(self):
"""
Test deleting an evaluation.
"""
custom_llm_provider = self.get_custom_llm_provider()
api_key = self.get_api_key()
api_base = self.get_api_base()
if not api_key:
pytest.skip(f"No API key provided for {custom_llm_provider}")
# Skip this test to avoid deleting production evals
pytest.skip("Skipping delete test to preserve existing evals")
class TestOpenAIEvalsAPI(BaseEvalsAPITest):
"""
Test OpenAI Evals API implementation.
"""
def get_custom_llm_provider(self) -> str:
return "openai"
def get_api_key(self) -> Optional[str]:
return os.environ.get("OPENAI_API_KEY")
def get_api_base(self) -> Optional[str]:
return os.environ.get("OPENAI_API_BASE")
@@ -0,0 +1 @@
"""OpenAI Evals API tests"""
@@ -0,0 +1,257 @@
"""
Unit tests for OpenAI Evals API transformation
"""
import httpx
import pytest
from litellm.llms.openai.evals.transformation import OpenAIEvalsConfig
from litellm.types.router import GenericLiteLLMParams
@pytest.fixture()
def config() -> OpenAIEvalsConfig:
return OpenAIEvalsConfig()
def test_validate_environment_sets_headers(config: OpenAIEvalsConfig):
"""Test that validate_environment correctly sets authorization headers"""
headers: dict = {}
params = GenericLiteLLMParams(api_key="sk-test-12345")
result = config.validate_environment(headers=headers, litellm_params=params)
assert result["Authorization"] == "Bearer sk-test-12345"
assert result["Content-Type"] == "application/json"
def test_validate_environment_requires_api_key(config: OpenAIEvalsConfig, monkeypatch):
"""Test that validate_environment raises error when no API key is provided"""
import os
# Ensure OPENAI_API_KEY environment variable is None before validation
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
headers: dict = {}
params = GenericLiteLLMParams()
with pytest.raises(ValueError, match="OPENAI_API_KEY is required"):
config.validate_environment(headers=headers, litellm_params=params)
def test_get_complete_url_with_eval_id(config: OpenAIEvalsConfig):
"""Test URL construction with eval_id"""
url = config.get_complete_url(
api_base="https://api.openai.com",
endpoint="evals",
eval_id="eval_123",
)
assert url == "https://api.openai.com/v1/evals/eval_123"
def test_get_complete_url_without_eval_id(config: OpenAIEvalsConfig):
"""Test URL construction without eval_id"""
url = config.get_complete_url(
api_base="https://api.openai.com",
endpoint="evals",
)
assert url == "https://api.openai.com/v1/evals"
def test_transform_create_eval_request(config: OpenAIEvalsConfig):
"""Test transformation of create eval request"""
create_request = {
"name": "Test Eval",
"data_source_config": {
"type": "stored_completions",
"metadata": {"usecase": "chatbot"}
},
"testing_criteria": [
{
"type": "label_model",
"model": "gpt-4o",
"input": [{"role": "user", "content": "Test"}],
"passing_labels": ["positive"],
"labels": ["positive", "negative"],
"name": "Test Grader"
}
],
}
result = config.transform_create_eval_request(
create_request=create_request,
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert result["name"] == "Test Eval"
assert result["data_source_config"]["type"] == "stored_completions"
assert len(result["testing_criteria"]) == 1
assert result["testing_criteria"][0]["type"] == "label_model"
def test_transform_create_eval_response(config: OpenAIEvalsConfig):
"""Test transformation of create eval response"""
response = httpx.Response(
status_code=200,
json={
"id": "eval_123",
"object": "eval",
"created_at": 1234567890,
"name": "Test Eval",
"data_source_config": {"type": "stored_completions"},
"testing_criteria": [],
},
request=httpx.Request("POST", "https://api.openai.com/v1/evals"),
)
result = config.transform_create_eval_response(
raw_response=response,
logging_obj=None, # type: ignore
)
assert result.id == "eval_123"
assert result.object == "eval"
assert result.name == "Test Eval"
def test_transform_list_evals_request(config: OpenAIEvalsConfig):
"""Test transformation of list evals request"""
list_params = {
"limit": 10,
"after": "eval_123",
"order": "desc",
}
url, query_params = config.transform_list_evals_request(
list_params=list_params,
litellm_params=GenericLiteLLMParams(api_base="https://api.openai.com"),
headers={},
)
assert url == "https://api.openai.com/v1/evals"
assert query_params["limit"] == 10
assert query_params["after"] == "eval_123"
assert query_params["order"] == "desc"
def test_transform_list_evals_response(config: OpenAIEvalsConfig):
"""Test transformation of list evals response"""
response = httpx.Response(
status_code=200,
json={
"object": "list",
"data": [
{
"id": "eval_123",
"object": "eval",
"created_at": 1234567890,
"name": "Test Eval",
"data_source_config": {"type": "stored_completions"},
"testing_criteria": [],
}
],
"first_id": "eval_123",
"last_id": "eval_123",
"has_more": False,
},
request=httpx.Request("GET", "https://api.openai.com/v1/evals"),
)
result = config.transform_list_evals_response(
raw_response=response,
logging_obj=None, # type: ignore
)
assert result.object == "list"
assert len(result.data) == 1
assert result.data[0].id == "eval_123"
assert result.has_more is False
def test_transform_update_eval_request(config: OpenAIEvalsConfig):
"""Test transformation of update eval request"""
update_request = {
"name": "Updated Eval Name",
"metadata": {"key": "value"},
}
url, headers, request_body = config.transform_update_eval_request(
eval_id="eval_123",
update_request=update_request,
api_base="https://api.openai.com",
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert url == "https://api.openai.com/v1/evals/eval_123"
assert request_body["name"] == "Updated Eval Name"
assert request_body["metadata"]["key"] == "value"
def test_transform_delete_eval_request(config: OpenAIEvalsConfig):
"""Test transformation of delete eval request"""
url, headers = config.transform_delete_eval_request(
eval_id="eval_123",
api_base="https://api.openai.com",
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert url == "https://api.openai.com/v1/evals/eval_123"
def test_transform_delete_eval_response(config: OpenAIEvalsConfig):
"""Test transformation of delete eval response"""
response = httpx.Response(
status_code=200,
json={
"object": "eval.deleted",
"deleted": True,
"eval_id": "eval_abc123"
},
request=httpx.Request("DELETE", "https://api.openai.com/v1/evals/eval_123"),
)
result = config.transform_delete_eval_response(
raw_response=response,
logging_obj=None, # type: ignore
)
assert result.eval_id == "eval_abc123"
assert result.object == "eval.deleted"
assert result.deleted is True
def test_transform_cancel_eval_request(config: OpenAIEvalsConfig):
"""Test transformation of cancel eval request"""
url, headers, request_body = config.transform_cancel_eval_request(
eval_id="eval_123",
api_base="https://api.openai.com",
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert url == "https://api.openai.com/v1/evals/eval_123/cancel"
assert request_body == {}
def test_transform_cancel_eval_response(config: OpenAIEvalsConfig):
"""Test transformation of cancel eval response"""
response = httpx.Response(
status_code=200,
json={
"id": "eval_123",
"object": "eval",
"status": "cancelled",
},
request=httpx.Request("POST", "https://api.openai.com/v1/evals/eval_123/cancel"),
)
result = config.transform_cancel_eval_response(
raw_response=response,
logging_obj=None, # type: ignore
)
assert result.id == "eval_123"
assert result.object == "eval"