Add end to end integration tests for batches

This commit is contained in:
Ephrim Stanley 2025-12-23 08:31:21 -05:00
parent fa14a9931f
commit b84aafbab6
21 changed files with 3041 additions and 4 deletions

365
BATCH_FIXES_README.md Normal file
View file

@ -0,0 +1,365 @@
# LiteLLM Batch API Fixes
This document describes bugs found in LiteLLM's managed batch/files functionality and the patches applied to fix them. It also provides step-by-step instructions to reproduce the tests from a clean slate.
## Table of Contents
1. [Bug 1: File Deletion Fails for Batch Output Files](#bug-1-file-deletion-fails-for-batch-output-files)
2. [Bug 2: File Deletion Returns Wrong Response](#bug-2-file-deletion-returns-wrong-response)
3. [Bug 3: Batch Listing Fails with Duplicate Argument](#bug-3-batch-listing-fails-with-duplicate-argument)
4. [Bug 4: File Retrieve Returns None for Batch Output Files](#bug-4-file-retrieve-returns-none-for-batch-output-files)
5. [Mock Server: Azure-like Credential Validation](#mock-server-azure-like-credential-validation)
6. [Test Setup Instructions](#test-setup-instructions)
---
## Bug 1: File Deletion Fails for Batch Output Files
### Description
**Broken Feature:** `DELETE /files/{file_id}` - Deleting batch output files fails with a Pydantic validation error.
**Error Message:**
```
openai.InternalServerError: Error code: 500 - {
'error': {
'message': '1 validation error for LiteLLM_ManagedFileTable\nfile_object\n Input should be a valid dictionary or instance of OpenAIFileObject [type=model_type, input_value=None, input_type=NoneType]'
}
}
```
**Root Cause:** When LiteLLM stores batch output files in `LiteLLM_ManagedFileTable`, it sets `file_object=None`. However, the Pydantic model requires this field to be a valid `OpenAIFileObject`.
### Patch
**File:** `litellm/proxy/_types.py`, line ~3759
```python
# Before
class LiteLLM_ManagedFileTable(LiteLLMPydanticObjectBase):
file_object: OpenAIFileObject
# After
class LiteLLM_ManagedFileTable(LiteLLMPydanticObjectBase):
file_object: Optional[OpenAIFileObject] = None # PATCHED
```
---
## Bug 2: File Deletion Returns Wrong Response
### Description
**Broken Feature:** `DELETE /files/{file_id}` - Even after fixing Bug #1, the method returns `None` instead of the delete confirmation.
**Error Message:**
```
Exception: LiteLLM Managed File object with id=... not found
```
**Root Cause:** `afile_delete` in `managed_files.py` calls `llm_router.afile_delete()` (which deletes the file at the provider) but discards the response.
### Patch
**File:** `enterprise/litellm_enterprise/proxy/hooks/managed_files.py`, line ~879
```python
# Before
async def afile_delete(self, file_id, ...):
for model_id, model_file_id in mapping.items():
await llm_router.afile_delete(model=model_id, file_id=model_file_id, **data)
# Returns None when stored_file_object is None
# After
async def afile_delete(self, file_id, ...):
delete_response = None # PATCHED: Capture response
for model_id, model_file_id in mapping.items():
delete_response = await llm_router.afile_delete(model=model_id, file_id=model_file_id, **data)
stored_file_object = await self.delete_unified_file_id(file_id, ...)
if stored_file_object:
return stored_file_object
elif delete_response: # PATCHED: Return provider response
delete_response.id = file_id # Replace with unified ID
return delete_response
else:
raise Exception(...)
```
---
## Bug 3: Batch Listing Fails with Duplicate Argument
### Description
**Broken Feature:** `GET /batches?target_model_names=...` - Listing batches fails when using `target_model_names` query parameter.
**Error Message:**
```
openai.InternalServerError: Error code: 500 - {
'error': {
'message': "alist_batches() got multiple values for keyword argument 'model'"
}
}
```
**Root Cause:** The code passes `model` explicitly AND includes it in `**data`:
```python
model = target_model_names.split(",")[0]
response = await llm_router.alist_batches(
model=model, # Passed explicitly
**data, # Also contains 'model' and 'target_model_names' keys
)
```
### Patch
**File:** `litellm/proxy/batches_endpoints/endpoints.py`, line ~576-577
```python
# Before
model = target_model_names.split(",")[0]
response = await llm_router.alist_batches(model=model, **data)
# After
model = target_model_names.split(",")[0]
data.pop("model", None) # PATCHED: Remove duplicate
data.pop("target_model_names", None) # PATCHED: Remove to avoid passing to downstream
response = await llm_router.alist_batches(model=model, **data)
```
---
## Bug 4: File Retrieve Returns None for Batch Output Files
### Description
**Broken Feature:** `GET /files/{file_id}` - Retrieving batch output file metadata returns `None`.
**Error Message:**
```
AttributeError: 'NoneType' object has no attribute 'id'
```
**Root Cause:** `afile_retrieve` returns `stored_file_object.file_object` which is `None` for batch output files. It should fetch the file metadata from the provider instead.
### Patch (Part A)
**File:** `enterprise/litellm_enterprise/proxy/hooks/managed_files.py`, line ~839-868
Add `import litellm` at the top of the file, then modify `afile_retrieve`:
```python
# Before
async def afile_retrieve(self, file_id, litellm_parent_otel_span):
stored = await self.get_unified_file_id(file_id, ...)
return stored.file_object # Returns None for batch output files!
# After
import litellm # Added at top of file
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router=None): # PATCHED: Added llm_router
stored = await self.get_unified_file_id(file_id, ...)
if stored:
if stored.file_object:
return stored.file_object
# PATCHED: Fetch from provider when file_object is None
elif stored.model_mappings and llm_router:
for model_id, model_file_id in stored.model_mappings.items():
deployment = llm_router.get_deployment(model_id=model_id)
if deployment:
credentials = llm_router.get_deployment_credentials(model_id=model_id) or {}
# Extract custom_llm_provider - afile_retrieve needs it as explicit param
custom_llm_provider = credentials.pop("custom_llm_provider", None)
if not custom_llm_provider:
# Infer from model name (e.g., "azure/gpt-5" -> "azure")
model_name = deployment.litellm_params.model or ""
if "/" in model_name:
custom_llm_provider = model_name.split("/")[0]
else:
custom_llm_provider = "openai"
response = await litellm.afile_retrieve(
file_id=model_file_id,
custom_llm_provider=custom_llm_provider, # Explicit param for Azure
**credentials
)
response.id = file_id # Replace with unified ID
return response
```
### Patch (Part B)
**File:** `litellm/proxy/openai_files_endpoints/files_endpoints.py`, line ~888
```python
# Before
response = await managed_files_obj.afile_retrieve(
file_id=file_id,
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
)
# After
response = await managed_files_obj.afile_retrieve(
file_id=file_id,
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
llm_router=llm_router, # PATCHED: Pass router to fetch from provider
)
```
---
## Test Setup Instructions
### Prerequisites
- Python 3.11+
- Docker and Docker Compose
- Poetry (Python package manager)
### Step 1: Clone and Setup Environment
```bash
# Install dependencies
poetry install --extras "proxy extra_proxy"
# Install enterprise package in editable mode (required for patches to work)
poetry run pip install -e enterprise
```
### Step 2: Terminal 1 - Start Database and Mock Server
```bash
cd tests/batches_tests/local-litellm
# Build and start PostgreSQL and Mock Azure Server
docker compose -f docker-compose.dev.yml up --build
```
Wait until you see both services are healthy:
- `litellm_dev_db` - PostgreSQL database
- `mock-server` - Mock Azure OpenAI server (with credential validation enabled by default)
**Note:** The mock server now validates credentials like real Azure. Use `--build` to ensure you have the latest mock server with credential validation.
### Step 3: Terminal 2 - Start LiteLLM Proxy
```bash
cd /path/to/litellm
# Set environment variables
export DATABASE_URL="postgresql://llmproxy:dbpassword9090@localhost:5432/litellm"
export LITELLM_MASTER_KEY="sk-1234"
export LITELLM_SALT_KEY="mock-salt-key-12345"
# For real Azure testing (optional):
# export OPENAI_API_KEY="your-azure-api-key"
# export OPENAI_API_BASE=https://your azure endpoint"
# Generate Prisma client (first time only)
poetry run python -m prisma generate
# Start the proxy server
poetry run litellm --config tests/batches_tests/local-litellm/litellm-config.yaml --detailed_debug --port 4000
```
Wait until you see:
```
INFO: Uvicorn running on http://0.0.0.0:4000
```
### Step 4: Terminal 3 - Run Tests
```bash
cd /path/to/litellm
# Run the end-to-end managed files test with mock server
USE_MOCK_SERVER=true poetry run pytest tests/batches_tests/test_managed_files_endtoend.py -s -vvv
```
### Expected Output
The test should pass with output similar to:
```
tests/batches_tests/test_managed_files_endtoend.py::TestManagedFilesAPI::test_e2e_managed_batch[gpt]
Creating batch input file...
Created batch input file: bGl0ZWxs...
Creating batch...
Created batch: bGl0ZWxs...
Waiting for batch to reach completed state...
Batch status: completed
Retrieving batch output file metadata...
Output file metadata: ...
Fetching batch output file content...
Output file content: ...
Deleting input file...
Deleting output file...
PASSED
```
---
## Configuration Files
### `tests/batches_tests/local-litellm/litellm-config-local.yaml`
This config file sets up models for local testing:
- Mock OpenAI models pointing to `http://localhost:8090`
- Mock Azure batch model pointing to `http://localhost:8090`
- (Optional) Real Azure batch model with API key from environment
### `tests/batches_tests/local-litellm/docker-compose.dev.yml`
Docker Compose file that runs:
- PostgreSQL 16 database on port 5432
- Mock Azure OpenAI server on port 8090
---
## Troubleshooting
### "No module named prisma"
```bash
poetry run pip install prisma==0.11.0
poetry run python -m prisma generate
```
### Database connection error
Ensure PostgreSQL is running and the DATABASE_URL is correct:
```bash
docker ps | grep postgres
# Should show litellm_dev_db running on port 5432
```
### Patches not being picked up/
1. Clear Python cache:
```bash
find enterprise -name "__pycache__" -type d -exec rm -rf {} +
find litellm -name "__pycache__" -type d -exec rm -rf {} +
```
2. Verify editable install:
```bash
poetry run pip show litellm-enterprise | grep "Editable"
# Should show: Editable project location: /path/to/litellm/enterprise
```
3. Restart the proxy server
### Azure credentials error when testing with real Azure
Set the environment variable before starting the proxy:
```bash
export OPENAI_API_KEY="your-actual-azure-api-key"
```
---

View file

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cas
from fastapi import HTTPException
import litellm
from litellm import Router, verbose_logger
from litellm._uuid import uuid
from litellm.caching.caching import DualCache
@ -836,13 +837,41 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
return response
async def afile_retrieve(
self, file_id: str, litellm_parent_otel_span: Optional[Span]
self, file_id: str, litellm_parent_otel_span: Optional[Span], llm_router=None
) -> OpenAIFileObject:
stored_file_object = await self.get_unified_file_id(
file_id, litellm_parent_otel_span
)
if stored_file_object:
return stored_file_object.file_object
# PATCHED: If file_object is None (batch output files), fetch from provider
if stored_file_object.file_object:
return stored_file_object.file_object
elif stored_file_object.model_mappings and llm_router:
for model_id, model_file_id in stored_file_object.model_mappings.items():
# PATCHED: Get deployment info and credentials from router
deployment = llm_router.get_deployment(model_id=model_id)
if deployment:
credentials = llm_router.get_deployment_credentials(model_id=model_id) or {}
# Extract custom_llm_provider - afile_retrieve needs it as explicit param
custom_llm_provider = credentials.pop("custom_llm_provider", None)
if not custom_llm_provider:
# Infer from model name (e.g., "azure/gpt-5" -> "azure")
model_name = deployment.litellm_params.model or ""
if "/" in model_name:
custom_llm_provider = model_name.split("/")[0]
else:
custom_llm_provider = "openai"
response = await litellm.afile_retrieve(
file_id=model_file_id,
custom_llm_provider=custom_llm_provider,
**credentials
)
response.id = file_id # Replace with unified ID
return response
else:
raise Exception(f"No deployment found for model_id={model_id}")
else:
raise Exception(f"LiteLLM Managed File object with id={file_id} has no file_object, or no model_mappings/llm_router to fetch from provider")
else:
raise Exception(f"LiteLLM Managed File object with id={file_id} not found")
@ -868,10 +897,12 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
[file_id], litellm_parent_otel_span
)
# PATCHED: Capture delete response from provider
delete_response = None
specific_model_file_id_mapping = model_file_id_mapping.get(file_id)
if specific_model_file_id_mapping:
for model_id, model_file_id in specific_model_file_id_mapping.items():
await llm_router.afile_delete(model=model_id, file_id=model_file_id, **data) # type: ignore
delete_response = await llm_router.afile_delete(model=model_id, file_id=model_file_id, **data) # type: ignore
stored_file_object = await self.delete_unified_file_id(
file_id, litellm_parent_otel_span
@ -879,6 +910,10 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
if stored_file_object:
return stored_file_object
# PATCHED: Return provider response with unified ID when stored_file_object is None
elif delete_response:
delete_response.id = file_id
return delete_response
else:
raise Exception(f"LiteLLM Managed File object with id={file_id} not found")

View file

@ -3756,7 +3756,7 @@ class SpendUpdateQueueItem(TypedDict, total=False):
class LiteLLM_ManagedFileTable(LiteLLMPydanticObjectBase):
unified_file_id: str
file_object: OpenAIFileObject
file_object: Optional[OpenAIFileObject] = None # PATCHED: Allow None for batch output files
model_mappings: Dict[str, str]
flat_model_file_ids: List[str]
created_by: Optional[str]

View file

@ -574,6 +574,7 @@ async def list_batches(
raise ValueError("target_model_names is required for this routing scenario")
model = target_model_names.split(",")[0]
data.pop("model", None)
data.pop("target_model_names", None) # PATCHED: Remove to avoid passing to downstream
response = await llm_router.alist_batches(
model=model,
after=after,

View file

@ -885,6 +885,7 @@ async def get_file(
response = await managed_files_obj.afile_retrieve(
file_id=file_id,
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
llm_router=llm_router, # PATCHED: Pass router to fetch from provider if file_object is None
)
else:
response = await litellm.afile_retrieve(

View file

@ -0,0 +1,15 @@
# Local LiteLLM
Local LiteLLM proxy with a mock LLM server for testing.
## Start
```bash
docker compose up --build
```
## Stop
```bash
docker compose down
```

View file

@ -0,0 +1,38 @@
# Docker Compose for local development
# Runs db and mock-server only - proxy runs locally via poetry
services:
db:
image: postgres:16
container_name: litellm_dev_db
restart: always
environment:
POSTGRES_DB: litellm
POSTGRES_USER: llmproxy
POSTGRES_PASSWORD: dbpassword9090
healthcheck:
test: ["CMD-SHELL", "pg_isready -d litellm -U llmproxy"]
interval: 1s
timeout: 5s
retries: 10
volumes:
- postgres_data_dev:/var/lib/postgresql/data
ports:
- "5432:5432"
mock-server:
build:
context: ./mock-server
dockerfile: Dockerfile
ports:
- "8090:8090"
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 http://localhost:8090/health || exit 1"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
volumes:
postgres_data_dev:

View file

@ -0,0 +1,109 @@
services:
# Default LiteLLM without patches
litellm:
image: ghcr.io/berriai/litellm:main-latest
profiles: ["default", "unpatched"]
ports:
- "4000:4000"
volumes:
- ./litellm-config.yaml:/app/config.yaml
command:
- "--config=/app/config.yaml"
- "--detailed_debug"
environment:
DATABASE_URL: "postgresql://llmproxy:dbpassword9090@db:5432/litellm"
STORE_MODEL_IN_DB: "True"
LITELLM_MASTER_KEY: "sk-1234"
LITELLM_SALT_KEY: "mock-salt-key-12345"
LITELLM_LOG: "DEBUG"
real_azure_api_key: "${real_azure_api_key:-}"
depends_on:
db:
condition: service_healthy
mock-server:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 http://localhost:4000/health/liveliness || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# LiteLLM with patches enabled
litellm-patched:
image: ghcr.io/berriai/litellm:main-latest
profiles: ["patched"]
ports:
- "4000:4000"
volumes:
- ./litellm-config.yaml:/app/config.yaml
# patch1
- ./patches/managed_files.py:/usr/lib/python3.13/site-packages/litellm_enterprise/proxy/hooks/managed_files.py
- ./patches/managed_files.py:/app/enterprise/litellm_enterprise/proxy/hooks/managed_files.py
# patch2
- ./patches/_types.py:/usr/lib/python3.13/site-packages/litellm/proxy/_types.py
- ./patches/_types.py:/app/litellm/proxy/_types.py
# patch3
- ./patches/batches_endpoints.py:/usr/lib/python3.13/site-packages/litellm/proxy/batches_endpoints/endpoints.py
- ./patches/batches_endpoints.py:/app/litellm/proxy/batches_endpoints/endpoints.py
# patch4
- ./patches/files_endpoints.py:/usr/lib/python3.13/site-packages/litellm/proxy/openai_files_endpoints/files_endpoints.py
- ./patches/files_endpoints.py:/app/litellm/proxy/openai_files_endpoints/files_endpoints.py
command:
- "--config=/app/config.yaml"
- "--detailed_debug"
environment:
DATABASE_URL: "postgresql://llmproxy:dbpassword9090@db:5432/litellm"
STORE_MODEL_IN_DB: "True"
LITELLM_MASTER_KEY: "sk-1234"
LITELLM_SALT_KEY: "mock-salt-key-12345"
LITELLM_LOG: "DEBUG"
real_azure_api_key: "${real_azure_api_key:-}"
depends_on:
db:
condition: service_healthy
mock-server:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 http://localhost:4000/health/liveliness || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres:16
restart: always
container_name: litellm_local_db
environment:
POSTGRES_DB: litellm
POSTGRES_USER: llmproxy
POSTGRES_PASSWORD: dbpassword9090
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -d litellm -U llmproxy"]
interval: 1s
timeout: 5s
retries: 10
mock-server:
build:
context: ./mock-server
dockerfile: Dockerfile
ports:
- "8090:8090"
environment:
TIME_TO_SLEEP: "0"
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8090/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
volumes:
postgres_data:
name: litellm_local_postgres_data

View file

@ -0,0 +1,57 @@
model_list:
- model_name: openai-fake-gpt-3.5-turbo
litellm_params:
model: openai/openai-fake-gpt-3.5-turbo
api_base: http://localhost:8090/v1
api_key: fake-key
- model_name: openai-fake-gpt-4
litellm_params:
model: openai/openai-fake-gpt-4
api_base: http://localhost:8090/v1
api_key: fake-key
- model_name: openai-fake-gpt-4o
litellm_params:
model: openai/openai-fake-gpt-4o
api_base: http://localhost:8090/v1
api_key: fake-key
- model_name: fake-text-embedding-3-small
litellm_params:
model: openai/fake-text-embedding-3-small
api_base: http://localhost:8090/v1
api_key: fake-key
- model_name: o3-mini-batch-2025-01-31
litellm_params:
model: openai/o3-mini-batch-2025-01-31
api_base: http://localhost:8090/openai/v1
api_key: fake-key
model_info:
mode: batch
- model_name: azure-fake-gpt-5-batch-2025-08-07
litellm_params:
api_base: http://localhost:8090
api_key: fake-key
api_version: 2025-03-01-preview
base_model: azure/gpt-5
model: azure/gpt-5-batch-2025-08-07
custom_llm_provider: azure
model_info:
mode: batch
- model_name: gpt-5-batch-2025-08-07
litellm_params:
api_base: os.environ/OPENAI_API_BASE
api_key: os.environ/OPENAI_API_KEY
api_version: 2025-03-01-preview
base_model: azure/gpt-5
model: azure/gpt-5-batch-2025-08-07
custom_llm_provider: azure
model_info:
mode: batch
general_settings:
master_key: sk-1234
database_url: "postgresql://llmproxy:dbpassword9090@localhost:5432/litellm"
litellm_settings:
drop_params: true
set_verbose: true
json_logs: true

View file

@ -0,0 +1,18 @@
FROM python:3.11-slim
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY pyproject.toml .
COPY main.py .
COPY mock_azure_batch.py .
COPY mock_chat.py .
COPY mock_responses.py .
COPY mock_embeddings.py .
RUN pip install --no-cache-dir $(python -c "import tomllib; print(' '.join(tomllib.load(open('pyproject.toml', 'rb'))['project']['dependencies']))")
EXPOSE 8090
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8090"]

View file

@ -0,0 +1,47 @@
from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from mock_azure_batch import setup_batch_routes
from mock_chat import setup_chat_routes
from mock_embeddings import setup_embeddings_routes
from mock_responses import setup_responses_routes
def get_request_url(request: Request):
return str(request.url)
limiter = Limiter(key_func=get_request_url)
load_dotenv()
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def health():
return {"status": "ok"}
setup_chat_routes(app)
setup_responses_routes(app)
setup_embeddings_routes(app)
setup_batch_routes(app)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8090)

View file

@ -0,0 +1,582 @@
import asyncio
import io
import json
import logging
import os
import time
import uuid
from typing import Dict, List, Optional
from fastapi import FastAPI, HTTPException, Query, Request, UploadFile, Depends
from fastapi.responses import StreamingResponse
from fastapi.security import APIKeyHeader
from pydantic import BaseModel
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Azure-like credential validation
# Set MOCK_REQUIRE_CREDENTIALS=true to enforce credential checks (like real Azure)
REQUIRE_CREDENTIALS = os.environ.get("MOCK_REQUIRE_CREDENTIALS", "true").lower() == "true"
VALID_API_KEYS = {"fake-key", "sk-1234", "test-key"} # Accept these API keys
api_key_header = APIKeyHeader(name="api-key", auto_error=False)
auth_header = APIKeyHeader(name="Authorization", auto_error=False)
def validate_credentials(
api_key: Optional[str] = Depends(api_key_header),
authorization: Optional[str] = Depends(auth_header),
):
"""
Validate Azure-style credentials.
Azure accepts either:
- api-key header
- Authorization: Bearer <token> header
"""
if not REQUIRE_CREDENTIALS:
return True
# Check api-key header
if api_key:
if api_key in VALID_API_KEYS:
return True
logger.warning(f"Invalid api-key provided: {api_key[:8]}...")
raise HTTPException(
status_code=401,
detail={
"error": {
"code": "401",
"message": "Access denied due to invalid subscription key or wrong API endpoint. "
"Make sure to provide a valid key for an active subscription and use a "
"correct regional API endpoint for your resource."
}
}
)
# Check Authorization header (Bearer token)
if authorization:
if authorization.startswith("Bearer "):
# Accept any bearer token for mock purposes
return True
logger.warning(f"Invalid Authorization header format")
# No credentials provided
logger.warning("No credentials provided in request")
raise HTTPException(
status_code=401,
detail={
"error": {
"code": "401",
"message": "Missing credentials. Please pass one of `api_key`, `azure_ad_token`, "
"`azure_ad_token_provider`, or the `AZURE_OPENAI_API_KEY` or "
"`AZURE_OPENAI_AD_TOKEN` environment variables."
}
}
)
class FileObject(BaseModel):
id: str
object: str = "file"
bytes: int
created_at: int
filename: str
purpose: str
status: str = "processed"
status_details: Optional[str] = None
expires_at: Optional[int] = None
class BatchObject(BaseModel):
id: str
object: str = "batch"
endpoint: str
errors: Optional[Dict] = None
input_file_id: str
completion_window: str
status: str
output_file_id: Optional[str] = None
error_file_id: Optional[str] = None
created_at: int
in_progress_at: Optional[int] = None
expires_at: Optional[int] = None
finalizing_at: Optional[int] = None
completed_at: Optional[int] = None
failed_at: Optional[int] = None
expired_at: Optional[int] = None
cancelling_at: Optional[int] = None
cancelled_at: Optional[int] = None
request_counts: Optional[Dict[str, int]] = None
metadata: Optional[Dict] = None
class BatchListResponse(BaseModel):
object: str = "list"
data: List[Dict]
first_id: Optional[str] = None
last_id: Optional[str] = None
has_more: bool = False
file_storage: Dict[str, Dict] = {}
batch_storage: Dict[str, BatchObject] = {}
batch_results: Dict[str, List[Dict]] = {}
PROCESSING_DELAY_SECONDS = float(1)
VALIDATING_DELAY_SECONDS = float(3)
async def process_batch(batch_id: str):
logger.info(f"Starting batch processing for {batch_id}")
try:
batch = batch_storage[batch_id]
await asyncio.sleep(VALIDATING_DELAY_SECONDS)
batch.status = "in_progress"
batch.in_progress_at = int(time.time())
logger.info(f"Batch {batch_id} status: in_progress")
await process_batch_requests(batch_id)
await asyncio.sleep(PROCESSING_DELAY_SECONDS)
batch.status = "finalizing"
batch.finalizing_at = int(time.time())
logger.info(f"Batch {batch_id} status: finalizing")
await asyncio.sleep(PROCESSING_DELAY_SECONDS)
await create_output_file(batch_id)
batch.status = "completed"
batch.completed_at = int(time.time())
logger.info(f"Batch {batch_id} status: completed")
except Exception as e:
logger.error(f"Batch {batch_id} failed: {e}")
batch = batch_storage[batch_id]
batch.status = "failed"
batch.failed_at = int(time.time())
batch.errors = {
"object": "list",
"data": [{"code": "processing_error", "message": str(e)}],
}
async def process_batch_requests(batch_id: str):
batch = batch_storage[batch_id]
input_file = file_storage[batch.input_file_id]
requests = []
for line in input_file["content"].split("\n"):
if line.strip():
try:
requests.append(json.loads(line))
except json.JSONDecodeError as e:
logger.warning(f"Invalid JSON line in batch {batch_id}: {e}")
logger.info(f"Batch {batch_id} has {len(requests)} requests")
results = []
failed_count = 0
for req in requests:
result = await process_single_request(req)
if result.get("error"):
failed_count += 1
results.append(result)
batch_results[batch_id] = results
batch.request_counts = {
"total": len(requests),
"completed": len(results) - failed_count,
"failed": failed_count,
}
async def process_single_request(request_data: Dict) -> Dict:
custom_id = request_data.get("custom_id")
url = request_data.get("url", "/v1/chat/completions")
body = request_data.get("body", {})
if "/chat/completions" in url:
response_body = {
"id": f"chatcmpl-{uuid.uuid4().hex}",
"object": "chat.completion",
"created": int(time.time()),
"model": body.get("model", "gpt-4o"),
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Mock batch response."},
"finish_reason": "stop",
},
],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
}
status_code = 200
else:
response_body = {"error": {"message": f"Unsupported endpoint: {url}"}}
status_code = 400
return {
"id": f"batch_req_{uuid.uuid4().hex[:12]}",
"custom_id": custom_id,
"response": {
"status_code": status_code,
"request_id": f"req_{uuid.uuid4().hex[:12]}",
"body": response_body,
},
"error": None,
}
async def create_output_file(batch_id: str):
results = batch_results.get(batch_id, [])
output_lines = [json.dumps(result) for result in results]
output_content = "\n".join(output_lines)
output_file_id = f"file-batch-output-{uuid.uuid4().hex[:12]}"
file_storage[output_file_id] = {
"content": output_content,
"filename": f"batch_output_{batch_id}.jsonl",
"purpose": "batch_output",
"bytes": len(output_content.encode()),
"created_at": int(time.time()),
}
batch = batch_storage[batch_id]
batch.output_file_id = output_file_id
logger.info(f"Created output file {output_file_id} for batch {batch_id}")
def validate_batch_input(content: str) -> tuple[bool, str, List[Dict]]:
requests = []
custom_ids = set()
lines = content.strip().split("\n")
if not lines or all(not line.strip() for line in lines):
return False, "empty_batch", []
for line_num, line in enumerate(lines, 1):
if not line.strip():
continue
try:
req = json.loads(line)
except json.JSONDecodeError:
return False, "invalid_json_line", []
for field in ["custom_id", "method", "url", "body"]:
if field not in req:
return False, "invalid_request", []
if req["custom_id"] in custom_ids:
return False, "duplicate_custom_id", []
custom_ids.add(req["custom_id"])
requests.append(req)
if len(requests) > 100000:
return False, "too_many_tasks", []
return True, "", requests
def setup_batch_routes(app: FastAPI):
# Files endpoints (OpenAI and Azure paths)
# All endpoints require credentials (like real Azure)
@app.post("/openai/v1/files")
@app.post("/openai/files")
@app.post("/v1/files")
@app.post("/files")
async def create_file(request: Request, _=Depends(validate_credentials)):
form = await request.form()
logger.info(f"File upload form fields: {list(form.keys())}")
file: UploadFile = form.get("file")
purpose: str = form.get("purpose", "batch")
if not file:
raise HTTPException(status_code=400, detail="No file provided")
logger.info(f"Uploading file: {file.filename}, purpose: {purpose}")
content = await file.read()
content_str = content.decode("utf-8")
file_id = f"file-{uuid.uuid4().hex[:24]}"
created_at = int(time.time())
expires_at = None
expires_after_seconds = form.get("expires_after[seconds]")
if expires_after_seconds:
try:
seconds = int(expires_after_seconds)
logger.info(f"expires_after[seconds] = {seconds}")
if seconds < 259200 or seconds > 2592000:
raise HTTPException(
status_code=400,
detail={
"error": {
"code": "invalidPayload",
"message": "Value for Seconds must be between 259200 and 2592000.",
},
},
)
expires_at = created_at + seconds
logger.info(f"Calculated expires_at: {expires_at}")
except ValueError as e:
logger.warning(f"Failed to parse expires_after[seconds]: {e}")
file_storage[file_id] = {
"content": content_str,
"filename": file.filename or "batch_input.jsonl",
"purpose": purpose,
"bytes": len(content),
"created_at": created_at,
"expires_at": expires_at,
}
logger.info(f"Created file {file_id}, expires_at={expires_at}")
return FileObject(
id=file_id,
bytes=len(content),
created_at=created_at,
filename=file.filename or "batch_input.jsonl",
purpose=purpose,
expires_at=expires_at,
).model_dump()
@app.get("/openai/v1/files/{file_id}")
@app.get("/openai/files/{file_id}")
@app.get("/v1/files/{file_id}")
@app.get("/files/{file_id}")
async def get_file(file_id: str, _=Depends(validate_credentials)):
logger.info(f"Getting file: {file_id}")
if file_id not in file_storage:
raise HTTPException(status_code=404, detail="File not found")
file_data = file_storage[file_id]
return FileObject(
id=file_id,
bytes=file_data["bytes"],
created_at=file_data["created_at"],
filename=file_data["filename"],
purpose=file_data["purpose"],
expires_at=file_data.get("expires_at"),
).model_dump()
@app.get("/openai/v1/files/{file_id}/content")
@app.get("/openai/files/{file_id}/content")
@app.get("/v1/files/{file_id}/content")
@app.get("/files/{file_id}/content")
async def get_file_content(file_id: str, _=Depends(validate_credentials)):
logger.info(f"Getting file content: {file_id}")
if file_id not in file_storage:
raise HTTPException(status_code=404, detail="File not found")
file_data = file_storage[file_id]
content = file_data["content"]
return StreamingResponse(
io.StringIO(content),
media_type="application/octet-stream",
headers={
"Content-Disposition": f"attachment; filename={file_data['filename']}",
},
)
@app.delete("/openai/v1/files/{file_id}")
@app.delete("/openai/files/{file_id}")
@app.delete("/v1/files/{file_id}")
@app.delete("/files/{file_id}")
async def delete_file(file_id: str, _=Depends(validate_credentials)):
logger.info(f"Deleting file: {file_id}")
if file_id not in file_storage:
raise HTTPException(status_code=404, detail="File not found")
del file_storage[file_id]
return {"id": file_id, "object": "file", "deleted": True}
@app.get("/openai/v1/files")
@app.get("/openai/files")
@app.get("/v1/files")
@app.get("/files")
async def list_files(
purpose: Optional[str] = None,
limit: int = Query(10000, le=10000),
_=Depends(validate_credentials),
):
logger.info(f"Listing files, purpose: {purpose}, limit: {limit}")
files = []
for file_id, file_data in file_storage.items():
if purpose is None or file_data.get("purpose") == purpose:
files.append(
FileObject(
id=file_id,
bytes=file_data["bytes"],
created_at=file_data["created_at"],
filename=file_data["filename"],
purpose=file_data["purpose"],
expires_at=file_data.get("expires_at"),
).model_dump(),
)
return {"object": "list", "data": files[:limit]}
# Batches endpoints (OpenAI and Azure paths)
@app.post("/openai/v1/batches")
@app.post("/openai/batches")
@app.post("/v1/batches")
@app.post("/batches")
async def create_batch(request_data: dict, _=Depends(validate_credentials)):
input_file_id = request_data.get("input_file_id")
endpoint = request_data.get("endpoint", "/v1/chat/completions")
completion_window = request_data.get("completion_window", "24h")
metadata = request_data.get("metadata", {})
output_expires_after = request_data.get("output_expires_after")
logger.info(
f"Creating batch with input_file: {input_file_id}, endpoint: {endpoint}, output_expires_after: {output_expires_after}",
)
if not input_file_id or input_file_id not in file_storage:
raise HTTPException(status_code=400, detail="Input file not found")
input_file = file_storage[input_file_id]
is_valid, error_code, _ = validate_batch_input(input_file["content"])
if not is_valid:
raise HTTPException(
status_code=400,
detail={
"error": {
"code": error_code,
"message": f"Validation failed: {error_code}",
},
},
)
batch_id = f"batch_{uuid.uuid4()}"
created_at = int(time.time())
if output_expires_after:
seconds = (
output_expires_after.get("seconds", 0)
if isinstance(output_expires_after, dict)
else 0
)
expires_at = created_at + seconds
logger.info(
f"Using output_expires_after: {seconds}s, expires_at: {expires_at}",
)
elif completion_window == "24h":
expires_at = created_at + (24 * 60 * 60)
else:
expires_at = created_at + (24 * 60 * 60)
batch = BatchObject(
id=batch_id,
endpoint=endpoint,
input_file_id=input_file_id,
completion_window=completion_window,
status="validating",
created_at=created_at,
expires_at=expires_at,
request_counts={"total": 0, "completed": 0, "failed": 0},
metadata=metadata,
)
batch_storage[batch_id] = batch
logger.info(f"Created batch {batch_id}")
asyncio.create_task(process_batch(batch_id))
return batch.model_dump()
@app.get("/openai/v1/batches/{batch_id}")
@app.get("/openai/batches/{batch_id}")
@app.get("/v1/batches/{batch_id}")
@app.get("/batches/{batch_id}")
async def get_batch(batch_id: str, _=Depends(validate_credentials)):
logger.info(f"Getting batch: {batch_id}")
if batch_id not in batch_storage:
raise HTTPException(status_code=404, detail="Batch not found")
return batch_storage[batch_id].model_dump()
@app.get("/openai/v1/batches")
@app.get("/openai/batches")
@app.get("/v1/batches")
@app.get("/batches")
async def list_batches(
after: Optional[str] = Query(None),
limit: int = Query(20, le=100),
_=Depends(validate_credentials),
):
logger.info(f"Listing batches, after: {after}, limit: {limit}")
batches = list(batch_storage.values())
batches.sort(key=lambda x: x.created_at, reverse=True)
if after:
after_index = next((i for i, b in enumerate(batches) if b.id == after), -1)
if after_index >= 0:
batches = batches[after_index + 1 :]
batches = batches[:limit]
return BatchListResponse(
data=[batch.model_dump() for batch in batches],
first_id=batches[0].id if batches else None,
last_id=batches[-1].id if batches else None,
has_more=len(batches) == limit,
).model_dump()
@app.post("/openai/v1/batches/{batch_id}/cancel")
@app.post("/openai/batches/{batch_id}/cancel")
@app.post("/v1/batches/{batch_id}/cancel")
@app.post("/batches/{batch_id}/cancel")
async def cancel_batch(batch_id: str, _=Depends(validate_credentials)):
logger.info(f"Cancelling batch: {batch_id}")
if batch_id not in batch_storage:
raise HTTPException(status_code=404, detail="Batch not found")
batch = batch_storage[batch_id]
if batch.status in ["completed", "failed", "cancelled", "expired"]:
raise HTTPException(
status_code=400,
detail=f"Cannot cancel batch in {batch.status} status",
)
batch.status = "cancelled"
batch.cancelled_at = int(time.time())
logger.info(f"Batch {batch_id} cancelled")
return batch.model_dump()
# Debug endpoints
@app.get("/debug/batches")
async def debug_list_batches():
return {
"batches": {
batch_id: batch.model_dump()
for batch_id, batch in batch_storage.items()
},
"files": {
file_id: {k: v for k, v in data.items() if k != "content"}
for file_id, data in file_storage.items()
},
}
@app.post("/reset")
@app.post("/debug/clear")
async def reset_all():
file_storage.clear()
batch_storage.clear()
batch_results.clear()
logger.info("All data cleared")
return {"message": "All data cleared"}
@app.get("/debug/status")
async def debug_status():
return {
"files_count": len(file_storage),
"batches_count": len(batch_storage),
"batch_statuses": {bid: b.status for bid, b in batch_storage.items()},
}

View file

@ -0,0 +1,124 @@
import json
import time
import uuid
from datetime import datetime
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
def get_request_details(request: Request, body: dict = None) -> str:
details = {
"method": request.method,
"url": str(request.url),
"path": request.url.path,
"headers": dict(request.headers),
"query_params": dict(request.query_params),
}
return json.dumps(details, indent=2)
def data_generator(response_details: str, model: str):
response_id = uuid.uuid4().hex
content = response_details
chunk_size = 50
for i in range(0, len(content), chunk_size):
text_chunk = content[i : i + chunk_size]
chunk = {
"id": f"chatcmpl-{response_id}",
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": model,
"choices": [{"index": 0, "delta": {"content": text_chunk}}],
}
yield f"data: {json.dumps(chunk)}\n\n"
final_chunk = {
"id": f"chatcmpl-{response_id}",
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": model,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
}
yield f"data: {json.dumps(final_chunk)}\n\n"
yield "data: [DONE]\n\n"
def setup_chat_routes(app: FastAPI):
@app.post("/chat/completions")
@app.post("/v1/chat/completions")
@app.post("/openai/deployments/{model:path}/chat/completions")
async def completion(request: Request):
data = await request.json()
model = data.get("model", "unknown")
request_details = get_request_details(request, data)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
response_details = f"Request:{request_details}, Canned Response:{timestamp}"
if data.get("stream"):
return StreamingResponse(
content=data_generator(response_details, model),
media_type="text/event-stream",
)
else:
response_id = uuid.uuid4().hex
response = {
"id": f"chatcmpl-{response_id}",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"system_fingerprint": "fp_mock_server",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": response_details,
},
"logprobs": None,
"finish_reason": "stop",
},
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
},
}
return response
@app.post("/completions")
@app.post("/v1/completions")
async def text_completion(request: Request):
data = await request.json()
model = data.get("model", "unknown")
request_details = get_request_details(request, data)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
response_details = f"Request:{request_details}, Canned Response:{timestamp}"
if data.get("stream"):
return StreamingResponse(
content=data_generator(response_details, model),
media_type="text/event-stream",
)
else:
response = {
"id": f"cmpl-{uuid.uuid4().hex}",
"choices": [
{
"finish_reason": "stop",
"index": 0,
"logprobs": None,
"text": response_details,
},
],
"created": int(time.time()),
"model": model,
"object": "text_completion",
"system_fingerprint": None,
"usage": {
"completion_tokens": 16,
"prompt_tokens": 10,
"total_tokens": 26,
},
}
return response

View file

@ -0,0 +1,26 @@
from fastapi import FastAPI, Request
def setup_embeddings_routes(app: FastAPI):
@app.post("/embeddings")
@app.post("/v1/embeddings")
@app.post("/openai/deployments/{model:path}/embeddings")
async def embeddings(request: Request):
data = await request.json()
model = data.get("model", "unknown")
_small_embedding = [
-0.006929283495992422,
-0.005336422007530928,
-4.547132266452536e-05,
-0.024047505110502243,
]
big_embedding = _small_embedding * 100
return {
"object": "list",
"data": [{"object": "embedding", "index": 0, "embedding": big_embedding}],
"model": model,
"usage": {"prompt_tokens": 5, "total_tokens": 5},
}

View file

@ -0,0 +1,92 @@
import json
import time
import uuid
from datetime import datetime
from fastapi import FastAPI, Request
def get_request_details(request: Request, body: dict = None) -> str:
details = {
"method": request.method,
"url": str(request.url),
"path": request.url.path,
"headers": dict(request.headers),
"query_params": dict(request.query_params),
}
return json.dumps(details, indent=2)
def setup_responses_routes(app: FastAPI):
@app.post("/responses")
@app.post("/v1/responses")
@app.post("/openai/responses")
async def responses_api(request: Request):
data = await request.json()
model = data.get("model", "unknown")
request_details = get_request_details(request, data)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
response_details = f"Request:{request_details}, Canned Response:{timestamp}"
response_id = uuid.uuid4().hex
message_id = f"msg_{uuid.uuid4().hex[:34]}"
return {
"id": f"resp_{response_id}",
"created_at": int(time.time()),
"error": None,
"incomplete_details": None,
"instructions": None,
"metadata": {},
"model": model,
"object": "response",
"output": [
{
"id": message_id,
"content": [
{
"annotations": [],
"text": response_details,
"type": "output_text",
"logprobs": [],
},
],
"role": "assistant",
"status": "completed",
"type": "message",
},
],
"parallel_tool_calls": True,
"temperature": data.get("temperature", 1.0),
"tool_choice": data.get("tool_choice", "auto"),
"tools": data.get("tools", []),
"top_p": data.get("top_p", 1.0),
"max_output_tokens": data.get("max_output_tokens"),
"previous_response_id": None,
"reasoning": {"effort": None, "summary": None},
"status": "completed",
"text": {"format": {"type": "text"}, "verbosity": "medium"},
"truncation": "disabled",
"usage": {
"input_tokens": 11,
"input_tokens_details": {
"audio_tokens": None,
"cached_tokens": 0,
"text_tokens": None,
},
"output_tokens": 19,
"output_tokens_details": {"reasoning_tokens": 0, "text_tokens": None},
"total_tokens": 30,
"cost": None,
},
"user": None,
"store": True,
"background": False,
"content_filters": None,
"max_tool_calls": None,
"prompt_cache_key": None,
"safety_identifier": None,
"service_tier": "default",
"top_logprobs": 0,
}

View file

@ -0,0 +1,20 @@
[project]
name = "mock-server"
version = "0.1.0"
description = "Mock LLM server for testing LiteLLM"
requires-python = ">=3.11"
dependencies = [
"fastapi",
"uvicorn",
"slowapi",
"python-dotenv>=0.2.0",
"python-multipart",
"pydantic",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["."]

View file

@ -0,0 +1,416 @@
version = 1
revision = 1
requires-python = ">=3.11"
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" },
]
[[package]]
name = "anyio"
version = "4.12.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
]
[[package]]
name = "deprecated"
version = "1.3.1"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f" },
]
[[package]]
name = "fastapi"
version = "0.125.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/17/71/2df15009fb4bdd522a069d2fbca6007c6c5487fce5cb965be00fc335f1d1/fastapi-0.125.0.tar.gz", hash = "sha256:16b532691a33e2c5dee1dac32feb31dc6eb41a3dd4ff29a95f9487cb21c054c0" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/34/2f/ff2fcc98f500713368d8b650e1bbc4a0b3ebcdd3e050dcdaad5f5a13fd7e/fastapi-0.125.0-py3-none-any.whl", hash = "sha256:2570ec4f3aecf5cca8f0428aed2398b774fcdfee6c2116f86e80513f2f86a7a1" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea" },
]
[[package]]
name = "limits"
version = "5.6.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "packaging" },
{ name = "typing-extensions" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/bb/e5/c968d43a65128cd54fb685f257aafb90cd5e4e1c67d084a58f0e4cbed557/limits-5.6.0.tar.gz", hash = "sha256:807fac75755e73912e894fdd61e2838de574c5721876a19f7ab454ae1fffb4b5" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/40/96/4fcd44aed47b8fcc457653b12915fcad192cd646510ef3f29fd216f4b0ab/limits-5.6.0-py3-none-any.whl", hash = "sha256:b585c2104274528536a5b68864ec3835602b3c4a802cd6aa0b07419798394021" },
]
[[package]]
name = "mock-server"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "fastapi" },
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "python-multipart" },
{ name = "slowapi" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi" },
{ name = "pydantic" },
{ name = "python-dotenv", specifier = ">=0.2.0" },
{ name = "python-multipart" },
{ name = "slowapi" },
{ name = "uvicorn" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61" },
]
[[package]]
name = "python-multipart"
version = "0.0.21"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090" },
]
[[package]]
name = "slowapi"
version = "0.1.9"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "limits" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36" },
]
[[package]]
name = "starlette"
version = "0.50.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" },
]
[[package]]
name = "uvicorn"
version = "0.38.0"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02" },
]
[[package]]
name = "wrapt"
version = "2.0.1"
source = { registry = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/simple" }
sdist = { url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f" }
wheels = [
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad" },
{ url = "https://artifacts.prod.devops.point72.com/artifactory/api/pypi/pypi-remote/packages/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca" },
]

View file

@ -0,0 +1,231 @@
# LiteLLM Patches
Patches for LiteLLM `main-latest` (as of 2025-12-19).
---
## 1. File Deletion Fails for Batch Output Files
### Broken Feature
`DELETE /files/{file_id}` - Deleting batch output files fails with a Pydantic validation error.
### Error Message
```
openai.InternalServerError: Error code: 500 - {
'error': {
'message': '1 validation error for LiteLLM_ManagedFileTable\nfile_object\n Input should be a valid dictionary or instance of OpenAIFileObject [type=model_type, input_value=None, input_type=NoneType]'
}
}
```
### Root Cause
When LiteLLM stores batch output files in `LiteLLM_ManagedFileTable`, it sets `file_object=None`. However, the Pydantic model requires this field to be a valid `OpenAIFileObject`.
### Code Change (`_types.py`)
```python
# Before
class LiteLLM_ManagedFileTable(LiteLLMPydanticObjectBase):
file_object: OpenAIFileObject
# After
class LiteLLM_ManagedFileTable(LiteLLMPydanticObjectBase):
file_object: Optional[OpenAIFileObject] = None # PATCHED
```
---
## 2. File Deletion Returns Wrong Response
### Broken Feature
`DELETE /files/{file_id}` - Even after fixing patch #1, the method returns `None` instead of the delete confirmation.
### Error Message
```
Exception: LiteLLM Managed File object with id=... not found
```
### Root Cause
`afile_delete` in `managed_files.py` calls `llm_router.afile_delete` (which deletes the file at the provider) but discards the response.
### Code Change (`managed_files.py`)
```python
# Before
async def afile_delete(self, file_id, ...):
for model_id, model_file_id in mapping.items():
await llm_router.afile_delete(model=model_id, file_id=model_file_id, **data)
# Returns None
# After
async def afile_delete(self, file_id, ...):
delete_response = None
for model_id, model_file_id in mapping.items():
delete_response = await llm_router.afile_delete(...) # PATCHED: Capture response
if delete_response:
delete_response.id = file_id # PATCHED: Replace with unified ID
return delete_response
```
---
## 3. Batch Listing Fails with Duplicate Argument
### Broken Feature
`GET /batches?target_model_names=...` - Listing batches fails when using `target_model_names` query parameter.
### Error Message
```
openai.InternalServerError: Error code: 500 - {
'error': {
'message': "alist_batches() got multiple values for keyword argument 'model'"
}
}
```
### Root Cause
The code passes `model` explicitly AND includes it in `**data`:
```python
model = target_model_names.split(",")[0]
response = await llm_router.alist_batches(
model=model, # Passed explicitly
**data, # Also contains 'model' key
)
```
### Code Change (`batches_endpoints.py`)
```python
# Before
model = target_model_names.split(",")[0]
response = await llm_router.alist_batches(model=model, **data)
# After
model = target_model_names.split(",")[0]
data.pop("model", None) # PATCHED: Remove duplicate
data.pop("target_model_names", None) # PATCHED: Remove to avoid passing to downstream
response = await llm_router.alist_batches(model=model, **data)
```
---
## 4. File Retrieve Returns None for Batch Output Files
### Broken Feature
`GET /files/{file_id}` - Retrieving batch output file metadata returns `None`.
### Error Message
```
AttributeError: 'NoneType' object has no attribute 'id'
```
### Root Cause
`afile_retrieve` returns `stored_file_object.file_object` which is `None` for batch output files. It should fetch from the provider.
### Code Change (`managed_files.py` + `files_endpoints.py`)
```python
# managed_files.py - After
async def afile_retrieve(self, file_id, litellm_parent_otel_span, llm_router=None):
stored = await self.get_unified_file_id(file_id, ...)
if stored.file_object:
return stored.file_object
# PATCHED: Fetch from provider when file_object is None
for model_id, model_file_id in stored.model_mappings.items():
response = await llm_router.afile_retrieve(model=model_id, file_id=model_file_id)
response.id = file_id
return response
```
```python
# files_endpoints.py - After
response = await managed_files_obj.afile_retrieve(
file_id=file_id,
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
llm_router=llm_router, # PATCHED: Pass router
)
```
---
## Known Issues (Not Bugs)
### Azure Batch Creation Response Missing `endpoint`
**Behavior:** Azure's batch creation response returns `endpoint=''` (empty string).
**Expected:** When you call `batches.retrieve()` or `batches.list()`, Azure returns `endpoint='/v1/chat/completions'` correctly.
**Workaround:** If you need the endpoint immediately after creation, retrieve the batch to get the correct value.
### Azure Batch Listing Returns Raw IDs
**Behavior:** `batches.list()` returns raw Azure batch IDs (e.g., `batch_abc123`) instead of LiteLLM unified IDs.
**Root Cause:** LiteLLM routes `batches.list()` directly to Azure instead of querying its internal managed batches database.
**Workaround:** Use `batches.retrieve(unified_batch_id)` instead of relying on list.
### Config Fix: Azure Batch Listing 404
**Behavior:** `batches.list()` returns empty results because Azure returns 404.
**Root Cause:** LiteLLM defaults to OpenAI handler instead of Azure handler for batch operations.
**Fix (config):** Add `custom_llm_provider: azure` to your model's `litellm_params`:
```yaml
litellm_params:
model: azure/gpt-5-batch
custom_llm_provider: azure # Required for Azure batch operations
```
---
## Patch Files
| Patch File | Container Path |
|------------|----------------|
| `_types.py` | `/usr/lib/python3.13/site-packages/litellm/proxy/_types.py` |
| `managed_files.py` | `/usr/lib/python3.13/site-packages/litellm_enterprise/proxy/hooks/managed_files.py` |
| `batches_endpoints.py` | `/usr/lib/python3.13/site-packages/litellm/proxy/batches_endpoints/endpoints.py` |
| `files_endpoints.py` | `/usr/lib/python3.13/site-packages/litellm/proxy/openai_files_endpoints/files_endpoints.py` |
---
## Usage
### With Patches
```bash
./start-patched.sh
```
### Without Patches
```bash
./start-unpatched.sh
```
### Docker Compose Volumes
```yaml
volumes:
- ./patches/_types.py:/usr/lib/python3.13/site-packages/litellm/proxy/_types.py
- ./patches/managed_files.py:/usr/lib/python3.13/site-packages/litellm_enterprise/proxy/hooks/managed_files.py
- ./patches/batches_endpoints.py:/usr/lib/python3.13/site-packages/litellm/proxy/batches_endpoints/endpoints.py
- ./patches/files_endpoints.py:/usr/lib/python3.13/site-packages/litellm/proxy/openai_files_endpoints/files_endpoints.py
```

View file

@ -0,0 +1,220 @@
"""Base class for managed files and batch API tests."""
import json
import os
import time
import uuid
import httpx
import openai
import pytest
from tenacity import Retrying, stop_after_delay, wait_fixed
LOCAL_LITELLM_BASE_URL = "http://localhost:4000"
LOCAL_AZURE_BASE_URL = "http://localhost:8090"
USE_LITELLM = os.environ.get("USE_LITELLM", "true").lower() == "true"
if USE_LITELLM:
base_url = LOCAL_LITELLM_BASE_URL
api_key = "sk-1234"
else:
base_url = LOCAL_AZURE_BASE_URL
api_key = "sk-1234"
USE_MOCK_SERVER = os.environ.get("USE_MOCK_SERVER", "false").lower() == "true"
if USE_MOCK_SERVER:
model_name = "azure-fake-gpt-5-batch-2025-08-07"
MODEL_NAMES = [
"azure-fake-gpt-5-batch-2025-08-07",
# "anthropic-fake-claude-sonnet-4-batch-2025-08-07",
# "vertex-fake-gemini-2.5-pro-batch-2025-08-07",
]
else:
model_name = "gpt-5-batch-2025-08-07"
MODEL_NAMES = [
"gpt-5-batch-2025-08-07",
# "claude-sonnet-4-batch-2025-08-07",
# "gemini-2.5-pro-batch-2025-08-07",
]
def _extract_model_id(model_name: str) -> str:
if "gpt" in model_name:
return "gpt"
elif "claude" in model_name or "anthropic" in model_name:
return "anthropic"
elif "gemini" in model_name or "vertex" in model_name:
return "gemini"
return model_name.split("-")[0]
MODEL_IDS = [_extract_model_id(m) for m in MODEL_NAMES]
MIN_EXPIRY_SECONDS = 259200
class ManagedFilesBase:
"""Base class with shared helpers for managed files and batch tests."""
base_url = base_url
api_key = api_key
@pytest.fixture(autouse=True)
def setup_test(self):
print(f"Base URL: {self.base_url}, Model: {model_name}\n")
self.reset_mock_server()
@staticmethod
def generate_request_id():
return f"req-{uuid.uuid4().hex[:8]}"
def create_openai_client(self, api_key: str) -> openai.OpenAI:
return openai.OpenAI(
base_url=self.base_url,
api_key=api_key,
http_client=httpx.Client(verify=False),
)
def create_batch_request_file_on_disk(self, tmpdir, model: str):
request_id = self.generate_request_id()
batch_request = {
"custom_id": request_id,
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": model,
"messages": [
{"role": "user", "content": "What is 2+2?"},
],
},
}
request_file = os.path.join(tmpdir, f"request-{request_id}.jsonl")
with open(request_file, "w") as f:
f.write(json.dumps(batch_request))
return request_file
def create_batch_input_file(
self,
client: openai.OpenAI,
request_file: str,
expiry_seconds: int = MIN_EXPIRY_SECONDS,
):
batch_input_file = client.files.create(
file=open(request_file, "rb"),
purpose="batch",
extra_body={
"target_model_names": model_name,
"expires_after": {
"seconds": expiry_seconds,
"anchor": "created_at",
},
},
)
return batch_input_file
def create_batch(
self,
client: openai.OpenAI,
input_file_id: str,
expiry_seconds: int = MIN_EXPIRY_SECONDS,
):
batch = client.batches.create(
input_file_id=input_file_id,
endpoint="/v1/chat/completions",
completion_window="24h",
extra_body={
"output_expires_after": {
"seconds": expiry_seconds,
"anchor": "created_at",
},
},
)
return batch
def wait_for_batch_state(
self,
client: openai.OpenAI,
batch_id: str,
expected_status: str,
max_seconds: int = 60,
wait_seconds: int = 5,
):
for attempt in Retrying(
stop=stop_after_delay(max_seconds),
wait=wait_fixed(wait_seconds),
):
with attempt:
batch_response = client.batches.retrieve(batch_id=batch_id)
print(
f"[{time.strftime('%H:%M:%S')}] Batch status: {batch_response.status}, expected: {expected_status}",
)
if batch_response.status == expected_status:
return batch_response
if batch_response.status in ["failed", "expired", "cancelled"]:
raise Exception(
f"Batch failed with status: {batch_response.status}",
)
raise Exception(f"Batch not in {expected_status} state yet")
return None
def wait_for_batch_completed(
self,
client: openai.OpenAI,
batch_id: str,
max_seconds: int = 120,
wait_seconds: int = 5,
):
return self.wait_for_batch_state(
client,
batch_id,
"completed",
max_seconds,
wait_seconds,
)
def shorten_id(self, id_str: str) -> str:
if id_str is None:
return "None"
if len(id_str) <= 20:
return id_str
return id_str[:8] + "..." + id_str[-8:]
def reset_mock_server(self):
if not USE_MOCK_SERVER:
return
print("Resetting mock server state...")
reset_response = httpx.post(f"{LOCAL_AZURE_BASE_URL}/reset")
assert reset_response.status_code == 200, f"Reset failed: {reset_response.text}"
def print_file_metadata(self, file_obj, label="File"):
print(f"{label} metadata:")
print(f"\tid={self.shorten_id(file_obj.id)}")
print(f"\tobject={file_obj.object}")
print(f"\tbytes={file_obj.bytes}")
print(f"\tfilename={file_obj.filename}")
print(f"\tpurpose={file_obj.purpose}")
print(f"\tstatus={file_obj.status}")
print(f"\tcreated_at={file_obj.created_at}")
print(f"\texpires_at={file_obj.expires_at}")
if file_obj.status_details:
print(f"\tstatus_details={file_obj.status_details}")
def print_batch_metadata(self, batch):
print("Batch metadata:")
print(f"\tid={self.shorten_id(batch.id)}")
print(f"\tstatus={batch.status}")
print(f"\tendpoint={batch.endpoint}")
print(f"\tcompletion_window={batch.completion_window}")
print(f"\tinput_file_id={self.shorten_id(batch.input_file_id)}")
print(f"\tcreated_at={batch.created_at}")
print(f"\texpires_at={batch.expires_at}")
print(f"\tin_progress_at={batch.in_progress_at}")
print(f"\tcompleted_at={batch.completed_at}")
print(f"\toutput_file_id={self.shorten_id(batch.output_file_id)}")
print(f"\trequest_counts={batch.request_counts}")

View file

@ -0,0 +1,204 @@
import warnings
import openai
import pytest
from tenacity import RetryError, Retrying, stop_after_delay, wait_fixed
from test_managed_files_base import (
MODEL_IDS,
MODEL_NAMES,
ManagedFilesBase,
MIN_EXPIRY_SECONDS,
)
class TestManagedFilesAPI(ManagedFilesBase):
"""Test cases for managed files and batch API.
Configuration via environment variables:
USE_LITELLM=true - Run against LiteLLM proxy
USE_LITELLM=false - Run against mock server directly (default)
"""
@classmethod
def setup_class(cls):
cls.openai_client = cls.create_openai_client(cls, cls.api_key)
def wait_for_batch_list(self, model_name, max_seconds=90, wait_seconds=10):
for attempt in Retrying(
stop=stop_after_delay(max_seconds),
wait=wait_fixed(wait_seconds),
):
with attempt:
batches_list = self.openai_client.batches.list(
limit=10,
extra_query={"target_model_names": model_name},
)
print(
f"Batches in list: {len(batches_list.data)}",
)
if len(batches_list.data) == 0:
raise Exception("No batches found in list yet")
return batches_list
return None
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_e2e_managed_batch(self, tmp_path, model_name):
print(
f"\n\nStarting test with base_url={self.base_url} and model_name={model_name}\n",
)
self.reset_mock_server()
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
print("Creating batch input file...")
batch_input_file = self.create_batch_input_file(
self.openai_client,
request_file,
MIN_EXPIRY_SECONDS,
)
print(f"Created batch input file: {self.shorten_id(batch_input_file.id)}\n")
print(
f"Retrieving batch input file metadata for file id: {self.shorten_id(batch_input_file.id)}",
)
input_file_metadata = self.openai_client.files.retrieve(batch_input_file.id)
assert input_file_metadata.id == batch_input_file.id, "File ID mismatch"
assert input_file_metadata.object == "file", "object should be 'file'"
assert input_file_metadata.bytes > 0, "bytes not set"
assert input_file_metadata.filename == "modified_file.jsonl", (
"filename mismatch"
)
assert input_file_metadata.purpose == "batch", "purpose mismatch"
assert input_file_metadata.status in ["uploaded", "processed", "error"], (
"invalid status"
)
assert input_file_metadata.created_at > 0, "created_at not set"
if not input_file_metadata.expires_at:
warnings.warn("batch input file expires_at not set")
self.print_file_metadata(input_file_metadata, "Input file")
print("\nCreating batch...")
batch = self.create_batch(
self.openai_client,
batch_input_file.id,
MIN_EXPIRY_SECONDS,
)
print(f"Created batch: {self.shorten_id(batch.id)}")
assert batch.id, "No batch ID returned"
assert batch.input_file_id == batch_input_file.id, "File ID mismatch"
assert batch.status in [
"validating",
"in_progress",
"finalizing",
"completed",
], "Status mismatch"
if not batch.expires_at:
warnings.warn("batch expires_at not set")
else:
assert batch.expires_at > 0, "batch expires_at not set"
if not batch.endpoint:
warnings.warn(
"batch.endpoint empty in creation response - Azure API quirk, not a bug",
)
else:
assert batch.endpoint == "/v1/chat/completions", "endpoint mismatch"
assert batch.completion_window == "24h", "completion_window mismatch"
assert batch.created_at > 0, "created_at not set"
self.print_batch_metadata(batch)
print("\nListing batches...")
try:
batches_list = self.wait_for_batch_list(
model_name,
max_seconds=30,
wait_seconds=5,
)
batches = batches_list.data if batches_list else []
batch_ids = [b.id for b in batches]
if batch.id not in batch_ids:
warnings.warn(
f"Batch {batch.id} not found in list. batches.list returns raw IDs and not the encoded IDs. raw IDs: {batch_ids}",
)
except RetryError:
warnings.warn(
"batches.list() returned empty list after retries - known LiteLLM issue with managed batches",
)
except openai.APIError as e:
pytest.fail(f"batches.list() failed: {e}")
print(
f"\nWaiting for batch {self.shorten_id(batch.id)} to reach completed state...",
)
try:
batch_response = self.wait_for_batch_state(
self.openai_client,
batch.id,
"completed",
max_seconds=30 * 60,
wait_seconds=5,
)
except RetryError:
raise TimeoutError("Timed out waiting for batch to be in state: completed")
print("\nRetrieving batch output file metadata...")
output_file_metadata = self.openai_client.files.retrieve(
batch_response.output_file_id,
)
assert output_file_metadata.id == batch_response.output_file_id, (
"Output file ID mismatch"
)
assert output_file_metadata.object == "file", "object should be 'file'"
assert output_file_metadata.bytes > 0, "bytes not set"
assert output_file_metadata.filename, "filename not set"
assert output_file_metadata.purpose in ["batch_output", "batch"], (
"purpose should be batch_output"
)
assert output_file_metadata.created_at > 0, "created_at not set"
self.print_file_metadata(output_file_metadata, "Output file")
print("\nFetching batch output file content...")
batch_file_content = self.openai_client.files.content(
batch_response.output_file_id,
)
assert batch_file_content.text, "No batch file content returned"
assert len(batch_file_content.text) > 0, "Batch file content is empty"
print(f"Output file content ({len(batch_file_content.text)} bytes):")
for line in batch_file_content.text.strip().split("\n")[:3]:
print(f"\t{line}")
print(f"\nDeleting input file: {self.shorten_id(batch_input_file.id)}")
try:
self.openai_client.files.delete(batch_input_file.id)
except openai.APIError as e:
pytest.fail(f"files.delete() failed: {e}")
print(
f"\nDeleting output file: {self.shorten_id(batch_response.output_file_id)}",
)
try:
self.openai_client.files.delete(batch_response.output_file_id)
except openai.APIError as e:
pytest.fail(f"files.delete() failed: {e}")
print("\nVerifying input file is deleted...")
try:
self.openai_client.files.content(batch_input_file.id)
assert False, f"Input file {batch_input_file.id} exists after deletion"
except (openai.NotFoundError, openai.PermissionDeniedError):
print("Input file correctly not accessible after deletion")
print("\nVerifying output file is deleted...")
try:
self.openai_client.files.content(batch_response.output_file_id)
assert False, (
f"Output file {batch_response.output_file_id} exists after deletion"
)
except (openai.NotFoundError, openai.PermissionDeniedError):
print("Output file correctly not accessible after deletion")

View file

@ -0,0 +1,436 @@
"""
Test cross-user batch access permissions.
This test verifies that a batch and related files created by one API key
cannot be accessed, modified, or cancelled by a different API key.
Reference: https://github.com/BerriAI/litellm/pull/17401/files
"""
import time
import httpx
import openai
import pytest
from test_managed_files_base import (
ManagedFilesBase,
MODEL_NAMES,
MODEL_IDS,
)
BATCH_ROUTES = [
"/v1/files",
"/files",
"/v1/files/*",
"/files/*",
"/v1/batches",
"/batches",
"/v1/batches/*",
"/batches/*",
]
class TestManagedFilesPermissions(ManagedFilesBase):
"""Test cases for cross-user batch access permissions.
Verifies that:
- User A can create and access their own batches and files
- User B cannot access, retrieve, cancel, or delete User A's batches/files
"""
master_api_key = "sk-1234"
@classmethod
def setup_class(cls):
cls.admin_client = httpx.Client(base_url=cls.base_url, verify=False)
@classmethod
def teardown_class(cls):
cls.admin_client.close()
def user_suffix(self) -> str:
return f"{time.strftime('%Y%m%d%H%M%S')}{int(time.time() * 1000) % 1000:03d}"
def create_user_and_key(self, user_suffix: str) -> tuple[str, str]:
user_email = f"test-user-{user_suffix}-{self.user_suffix()}@test.com"
user_response = self.admin_client.post(
"/user/new",
json={
"user_email": user_email,
"user_alias": user_email,
"user_role": "internal_user",
"auto_create_key": "false",
},
headers={
"Authorization": f"Bearer {self.master_api_key}",
"Content-Type": "application/json",
},
timeout=30,
)
assert user_response.status_code == 200, (
f"Failed to create user: {user_response.status_code} - {user_response.text}"
)
user_data = user_response.json()
user_id = user_data.get("user_id")
key_response = self.admin_client.post(
"/key/generate",
json={
"user_id": user_id,
"key_alias": user_email,
"allowed_routes": BATCH_ROUTES,
},
headers={
"Authorization": f"Bearer {self.master_api_key}",
"Content-Type": "application/json",
},
timeout=30,
)
assert key_response.status_code == 200, (
f"Failed to create key: {key_response.status_code} - {key_response.text}"
)
key_data = key_response.json()
api_key = key_data.get("key")
print(f"Created user {user_email} with key {key_data.get('key_alias')}")
return user_id, api_key
def create_user_key_and_client(
self,
user_suffix: str,
) -> tuple[str, str, openai.OpenAI]:
user_id, api_key = self.create_user_and_key(user_suffix)
return user_id, api_key, self.create_openai_client(api_key)
def create_key_and_client(self, user_id: str, key_suffix: str) -> str:
key_alias = f"additional-key-{key_suffix}-{self.user_suffix()}"
key_response = self.admin_client.post(
"/key/generate",
json={
"user_id": user_id,
"key_alias": key_alias,
"allowed_routes": BATCH_ROUTES,
},
headers={
"Authorization": f"Bearer {self.master_api_key}",
"Content-Type": "application/json",
},
timeout=30,
)
assert key_response.status_code == 200, (
f"Failed to create additional key: {key_response.status_code} - {key_response.text}"
)
api_key = key_response.json().get("key")
print(f"Created additional key {api_key[:20]}... for user {user_id}")
return api_key, self.create_openai_client(api_key)
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_retrieve_user_a_batch(self, tmp_path, model_name):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
batch = self.create_batch(client_A, batch_input_file.id)
# User A retrieves their own batch
batch_a = client_A.batches.retrieve(batch_id=batch.id)
assert batch_a.id == batch.id, (
"User A should be able to retrieve their own batch"
)
# User B cannot retrieve User A's batch
try:
client_B.batches.retrieve(batch_id=batch.id)
pytest.fail("User B should NOT be able to retrieve User A's batch")
except openai.PermissionDeniedError as e:
assert e.status_code == 403
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_cancel_user_a_batch(self, tmp_path, model_name):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
batch = self.create_batch(client_A, batch_input_file.id)
# User B cannot cancel User A's batch
try:
client_B.batches.cancel(batch_id=batch.id)
pytest.fail("User B should NOT be able to cancel User A's batch")
except openai.PermissionDeniedError as e:
assert e.status_code == 403
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_retrieve_user_a_batch_input_file(self, tmp_path, model_name):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
# User A retrieves their own file
file_a = client_A.files.retrieve(file_id=batch_input_file.id)
assert file_a.id == batch_input_file.id, (
"User A should be able to retrieve their own file"
)
# User B cannot retrieve User A's file
try:
client_B.files.retrieve(file_id=batch_input_file.id)
pytest.fail("User B should NOT be able to retrieve User A's file")
except openai.PermissionDeniedError as e:
assert e.status_code == 403
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_download_user_a_batch_input_file_content(
self,
tmp_path,
model_name,
):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
# User A can download their own file content
content_a = client_A.files.content(file_id=batch_input_file.id)
assert content_a.text, (
"User A should be able to download their own file content"
)
# User B cannot download User A's file content
try:
client_B.files.content(file_id=batch_input_file.id)
pytest.fail("User B should NOT be able to download User A's file content")
except openai.PermissionDeniedError as e:
assert e.status_code == 403
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_delete_user_a_batch_input_file(self, tmp_path, model_name):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
# User B cannot delete User A's file
try:
client_B.files.delete(file_id=batch_input_file.id)
pytest.fail("User B should NOT be able to delete User A's file")
except openai.PermissionDeniedError as e:
assert e.status_code == 403
# User A can still retrieve their own file
file_a = client_A.files.retrieve(file_id=batch_input_file.id)
assert file_a.id == batch_input_file.id, "File should still exist"
# User A can delete their own file
try:
client_A.files.delete(file_id=batch_input_file.id)
except openai.APIError as e:
pytest.fail(f"User A should be able to delete their own file: {e}")
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_retrieve_user_a_batch_output_file(
self,
tmp_path,
model_name,
):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
batch = self.create_batch(client_A, batch_input_file.id)
# Wait for batch to complete
completed_batch = self.wait_for_batch_completed(client_A, batch.id)
assert completed_batch.output_file_id, "Batch should have an output file"
# User A retrieves their own output file
file_a = client_A.files.retrieve(file_id=completed_batch.output_file_id)
assert file_a.id == completed_batch.output_file_id, (
"User A should be able to retrieve their own output file"
)
# User B cannot retrieve User A's output file
try:
client_B.files.retrieve(file_id=completed_batch.output_file_id)
pytest.fail("User B should NOT be able to retrieve User A's output file")
except openai.PermissionDeniedError as e:
assert e.status_code == 403
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_download_user_a_batch_output_file_content(
self,
tmp_path,
model_name,
):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
batch = self.create_batch(client_A, batch_input_file.id)
# Wait for batch to complete
completed_batch = self.wait_for_batch_completed(client_A, batch.id)
assert completed_batch.output_file_id, "Batch should have an output file"
# User A can download their own output file content
content_a = client_A.files.content(file_id=completed_batch.output_file_id)
assert content_a.text, (
"User A should be able to download their own output file content"
)
# User B cannot download User A's output file content
try:
client_B.files.content(file_id=completed_batch.output_file_id)
pytest.fail(
"User B should NOT be able to download User A's output file content",
)
except openai.PermissionDeniedError as e:
assert e.status_code == 403
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_delete_user_a_batch_output_file(self, tmp_path, model_name):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
batch = self.create_batch(client_A, batch_input_file.id)
# Wait for batch to complete
completed_batch = self.wait_for_batch_completed(client_A, batch.id)
assert completed_batch.output_file_id, "Batch should have an output file"
# User B cannot delete User A's output file
try:
client_B.files.delete(file_id=completed_batch.output_file_id)
pytest.fail("User B should NOT be able to delete User A's output file")
except openai.PermissionDeniedError as e:
assert e.status_code == 403
# User A can still retrieve their own output file
file_a = client_A.files.retrieve(file_id=completed_batch.output_file_id)
assert file_a.id == completed_batch.output_file_id, (
"Output file should still exist"
)
# User A can delete their own output file
try:
client_A.files.delete(file_id=completed_batch.output_file_id)
except openai.APIError as e:
pytest.fail(f"User A should be able to delete their own output file: {e}")
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_b_cannot_list_user_a_batches(self, tmp_path, model_name):
user_a_id, user_a_key, client_A = self.create_user_key_and_client("a")
user_b_id, user_b_key, client_B = self.create_user_key_and_client("b")
# User A creates a batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_A, request_file)
batch = self.create_batch(client_A, batch_input_file.id)
# User A can see their own batch in the list
batches_a = client_A.batches.list(
limit=10,
extra_query={"target_model_names": model_name},
)
batch_ids_a = [b.id for b in batches_a.data]
assert batch.id in batch_ids_a, "User A should see their own batch in the list"
# User B's batch list should NOT contain User A's batch
batches_b = client_B.batches.list(
limit=10,
extra_query={"target_model_names": model_name},
)
batch_ids_b = [b.id for b in batches_b.data]
assert batch.id not in batch_ids_b, (
"User B should NOT see User A's batch in the list"
)
@pytest.mark.parametrize("model_name", MODEL_NAMES, ids=MODEL_IDS)
def test_user_api_keys_are_interchangeable(self, tmp_path, model_name):
# Create user with 3 keys
user_id, key1, client_Key1 = self.create_user_key_and_client("a")
key2, client_Key2 = self.create_key_and_client(user_id, "a2")
key3, client_Key3 = self.create_key_and_client(user_id, "a3")
# Key1: Create batch input file and batch
request_file = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file = self.create_batch_input_file(client_Key1, request_file)
batch = self.create_batch(client_Key1, batch_input_file.id)
# Key1: Retrieve batch
batch_retrieved = client_Key1.batches.retrieve(batch_id=batch.id)
assert batch_retrieved.id == batch.id, "Key1 should retrieve its own batch"
# Key2: Wait for batch completion and retrieve output
completed_batch = self.wait_for_batch_completed(client_Key2, batch.id)
assert completed_batch.output_file_id, "Batch should have an output file"
# Key2: Retrieve output file metadata
output_file = client_Key2.files.retrieve(file_id=completed_batch.output_file_id)
assert output_file.id == completed_batch.output_file_id, (
"Key2 should retrieve output file"
)
# Key2: Download output file content
output_content = client_Key2.files.content(
file_id=completed_batch.output_file_id,
)
assert output_content.text, "Key2 should download output file content"
# Key3: List batches and verify batch is visible
batches = client_Key3.batches.list(
limit=10,
extra_query={"target_model_names": model_name},
)
batch_ids = [b.id for b in batches.data]
assert batch.id in batch_ids, "Key3 should see batch in list"
# Key3: Delete input file
try:
client_Key3.files.delete(file_id=batch_input_file.id)
except openai.APIError as e:
pytest.fail(f"Key3 should delete input file: {e}")
# Key3: Delete output file
try:
client_Key3.files.delete(file_id=completed_batch.output_file_id)
except openai.APIError as e:
pytest.fail(f"Key3 should delete output file: {e}")
# Key1: Create another batch for cancellation test
request_file2 = self.create_batch_request_file_on_disk(tmp_path, model_name)
batch_input_file2 = self.create_batch_input_file(client_Key1, request_file2)
batch2 = self.create_batch(client_Key1, batch_input_file2.id)
# Key3: Cancel the batch created by Key1
try:
cancelled_batch = client_Key3.batches.cancel(batch_id=batch2.id)
assert cancelled_batch.id == batch2.id, (
"Key3 should cancel batch created by Key1"
)
except openai.BadRequestError:
pass # Batch may have already completed