Files
lijiaoqiao/llm-gateway-competitors/litellm-wheel-src/litellm/llms/anthropic/files/transformation.py
2026-03-26 20:06:14 +08:00

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,
)