308 lines
9.4 KiB
Python
308 lines
9.4 KiB
Python
"""
|
|
Anthropic Files API transformation config.
|
|
|
|
Implements BaseFilesConfig for Anthropic's Files API (beta).
|
|
Reference: https://docs.anthropic.com/en/docs/build-with-claude/files
|
|
|
|
Anthropic Files API endpoints:
|
|
- POST /v1/files - Upload a file
|
|
- GET /v1/files - List files
|
|
- GET /v1/files/{file_id} - Retrieve file metadata
|
|
- DELETE /v1/files/{file_id} - Delete a file
|
|
- GET /v1/files/{file_id}/content - Download file content
|
|
"""
|
|
|
|
import calendar
|
|
import time
|
|
from typing import Any, Dict, List, Optional, Union, cast
|
|
|
|
import httpx
|
|
from openai.types.file_deleted import FileDeleted
|
|
|
|
from litellm.litellm_core_utils.prompt_templates.common_utils import extract_file_data
|
|
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
|
from litellm.llms.base_llm.files.transformation import (
|
|
BaseFilesConfig,
|
|
LiteLLMLoggingObj,
|
|
)
|
|
from litellm.types.llms.openai import (
|
|
CreateFileRequest,
|
|
FileContentRequest,
|
|
HttpxBinaryResponseContent,
|
|
OpenAICreateFileRequestOptionalParams,
|
|
OpenAIFileObject,
|
|
)
|
|
from litellm.types.utils import LlmProviders
|
|
|
|
from ..common_utils import AnthropicError, AnthropicModelInfo
|
|
|
|
ANTHROPIC_FILES_API_BASE = "https://api.anthropic.com"
|
|
ANTHROPIC_FILES_BETA_HEADER = "files-api-2025-04-14"
|
|
|
|
|
|
class AnthropicFilesConfig(BaseFilesConfig):
|
|
"""
|
|
Transformation config for Anthropic Files API.
|
|
|
|
Anthropic uses:
|
|
- x-api-key header for authentication
|
|
- anthropic-beta: files-api-2025-04-14 header
|
|
- multipart/form-data for file uploads
|
|
- purpose="messages" (Anthropic-specific, not for batches/fine-tuning)
|
|
"""
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@property
|
|
def custom_llm_provider(self) -> LlmProviders:
|
|
return LlmProviders.ANTHROPIC
|
|
|
|
def get_complete_url(
|
|
self,
|
|
api_base: Optional[str],
|
|
api_key: Optional[str],
|
|
model: str,
|
|
optional_params: dict,
|
|
litellm_params: dict,
|
|
stream: Optional[bool] = None,
|
|
) -> str:
|
|
api_base = AnthropicModelInfo.get_api_base(api_base) or ANTHROPIC_FILES_API_BASE
|
|
return f"{api_base.rstrip('/')}/v1/files"
|
|
|
|
def get_error_class(
|
|
self,
|
|
error_message: str,
|
|
status_code: int,
|
|
headers: Union[dict, httpx.Headers],
|
|
) -> BaseLLMException:
|
|
return AnthropicError(
|
|
status_code=status_code,
|
|
message=error_message,
|
|
headers=cast(httpx.Headers, headers) if isinstance(headers, dict) else headers,
|
|
)
|
|
|
|
def validate_environment(
|
|
self,
|
|
headers: dict,
|
|
model: str,
|
|
messages: list,
|
|
optional_params: dict,
|
|
litellm_params: dict,
|
|
api_key: Optional[str] = None,
|
|
api_base: Optional[str] = None,
|
|
) -> dict:
|
|
api_key = AnthropicModelInfo.get_api_key(api_key)
|
|
if not api_key:
|
|
raise ValueError(
|
|
"Anthropic API key is required. Set ANTHROPIC_API_KEY environment variable or pass api_key parameter."
|
|
)
|
|
headers.update(
|
|
{
|
|
"x-api-key": api_key,
|
|
"anthropic-version": "2023-06-01",
|
|
"anthropic-beta": ANTHROPIC_FILES_BETA_HEADER,
|
|
}
|
|
)
|
|
return headers
|
|
|
|
def get_supported_openai_params(
|
|
self, model: str
|
|
) -> List[OpenAICreateFileRequestOptionalParams]:
|
|
return ["purpose"]
|
|
|
|
def map_openai_params(
|
|
self,
|
|
non_default_params: dict,
|
|
optional_params: dict,
|
|
model: str,
|
|
drop_params: bool,
|
|
) -> dict:
|
|
return optional_params
|
|
|
|
def transform_create_file_request(
|
|
self,
|
|
model: str,
|
|
create_file_data: CreateFileRequest,
|
|
optional_params: dict,
|
|
litellm_params: dict,
|
|
) -> dict:
|
|
"""
|
|
Transform to multipart form data for Anthropic file upload.
|
|
|
|
Anthropic expects: POST /v1/files with multipart form-data
|
|
- file: the file content
|
|
- purpose: "messages" (defaults to "messages" if not provided)
|
|
"""
|
|
file_data = create_file_data.get("file")
|
|
if file_data is None:
|
|
raise ValueError("File data is required")
|
|
|
|
extracted = extract_file_data(file_data)
|
|
filename = extracted["filename"] or f"file_{int(time.time())}"
|
|
content = extracted["content"]
|
|
content_type = extracted.get("content_type", "application/octet-stream")
|
|
|
|
purpose = create_file_data.get("purpose", "messages")
|
|
|
|
return {
|
|
"file": (filename, content, content_type),
|
|
"purpose": (None, purpose),
|
|
}
|
|
|
|
def transform_create_file_response(
|
|
self,
|
|
model: Optional[str],
|
|
raw_response: httpx.Response,
|
|
logging_obj: LiteLLMLoggingObj,
|
|
litellm_params: dict,
|
|
) -> OpenAIFileObject:
|
|
"""
|
|
Transform Anthropic file response to OpenAI format.
|
|
|
|
Anthropic response:
|
|
{
|
|
"id": "file-xxx",
|
|
"type": "file",
|
|
"filename": "document.pdf",
|
|
"mime_type": "application/pdf",
|
|
"size_bytes": 12345,
|
|
"created_at": "2025-01-01T00:00:00Z"
|
|
}
|
|
"""
|
|
response_json = raw_response.json()
|
|
return self._parse_anthropic_file(response_json)
|
|
|
|
def transform_retrieve_file_request(
|
|
self,
|
|
file_id: str,
|
|
optional_params: dict,
|
|
litellm_params: dict,
|
|
) -> tuple[str, dict]:
|
|
api_base = (
|
|
AnthropicModelInfo.get_api_base(litellm_params.get("api_base"))
|
|
or ANTHROPIC_FILES_API_BASE
|
|
)
|
|
return f"{api_base.rstrip('/')}/v1/files/{file_id}", {}
|
|
|
|
def transform_retrieve_file_response(
|
|
self,
|
|
raw_response: httpx.Response,
|
|
logging_obj: LiteLLMLoggingObj,
|
|
litellm_params: dict,
|
|
) -> OpenAIFileObject:
|
|
response_json = raw_response.json()
|
|
return self._parse_anthropic_file(response_json)
|
|
|
|
def transform_delete_file_request(
|
|
self,
|
|
file_id: str,
|
|
optional_params: dict,
|
|
litellm_params: dict,
|
|
) -> tuple[str, dict]:
|
|
api_base = (
|
|
AnthropicModelInfo.get_api_base(litellm_params.get("api_base"))
|
|
or ANTHROPIC_FILES_API_BASE
|
|
)
|
|
return f"{api_base.rstrip('/')}/v1/files/{file_id}", {}
|
|
|
|
def transform_delete_file_response(
|
|
self,
|
|
raw_response: httpx.Response,
|
|
logging_obj: LiteLLMLoggingObj,
|
|
litellm_params: dict,
|
|
) -> FileDeleted:
|
|
response_json = raw_response.json()
|
|
file_id = response_json.get("id", "")
|
|
return FileDeleted(
|
|
id=file_id,
|
|
deleted=True,
|
|
object="file",
|
|
)
|
|
|
|
def transform_list_files_request(
|
|
self,
|
|
purpose: Optional[str],
|
|
optional_params: dict,
|
|
litellm_params: dict,
|
|
) -> tuple[str, dict]:
|
|
api_base = (
|
|
AnthropicModelInfo.get_api_base(litellm_params.get("api_base"))
|
|
or ANTHROPIC_FILES_API_BASE
|
|
)
|
|
url = f"{api_base.rstrip('/')}/v1/files"
|
|
params: Dict[str, Any] = {}
|
|
if purpose:
|
|
params["purpose"] = purpose
|
|
return url, params
|
|
|
|
def transform_list_files_response(
|
|
self,
|
|
raw_response: httpx.Response,
|
|
logging_obj: LiteLLMLoggingObj,
|
|
litellm_params: dict,
|
|
) -> List[OpenAIFileObject]:
|
|
"""
|
|
Anthropic list response:
|
|
{
|
|
"data": [...],
|
|
"has_more": false,
|
|
"first_id": "...",
|
|
"last_id": "..."
|
|
}
|
|
"""
|
|
response_json = raw_response.json()
|
|
files_data = response_json.get("data", [])
|
|
return [self._parse_anthropic_file(f) for f in files_data]
|
|
|
|
def transform_file_content_request(
|
|
self,
|
|
file_content_request: FileContentRequest,
|
|
optional_params: dict,
|
|
litellm_params: dict,
|
|
) -> tuple[str, dict]:
|
|
file_id = file_content_request.get("file_id")
|
|
api_base = (
|
|
AnthropicModelInfo.get_api_base(litellm_params.get("api_base"))
|
|
or ANTHROPIC_FILES_API_BASE
|
|
)
|
|
return f"{api_base.rstrip('/')}/v1/files/{file_id}/content", {}
|
|
|
|
def transform_file_content_response(
|
|
self,
|
|
raw_response: httpx.Response,
|
|
logging_obj: LiteLLMLoggingObj,
|
|
litellm_params: dict,
|
|
) -> HttpxBinaryResponseContent:
|
|
return HttpxBinaryResponseContent(response=raw_response)
|
|
|
|
@staticmethod
|
|
def _parse_anthropic_file(file_data: dict) -> OpenAIFileObject:
|
|
"""Parse Anthropic file object into OpenAI format."""
|
|
created_at_str = file_data.get("created_at", "")
|
|
if created_at_str:
|
|
try:
|
|
created_at = int(
|
|
calendar.timegm(
|
|
time.strptime(
|
|
created_at_str.replace("Z", "+00:00")[:19],
|
|
"%Y-%m-%dT%H:%M:%S",
|
|
)
|
|
)
|
|
)
|
|
except (ValueError, TypeError):
|
|
created_at = int(time.time())
|
|
else:
|
|
created_at = int(time.time())
|
|
|
|
return OpenAIFileObject(
|
|
id=file_data.get("id", ""),
|
|
bytes=file_data.get("size_bytes", file_data.get("bytes", 0)),
|
|
created_at=created_at,
|
|
filename=file_data.get("filename", ""),
|
|
object="file",
|
|
purpose=file_data.get("purpose", "messages"),
|
|
status="uploaded",
|
|
status_details=None,
|
|
)
|