diff --git a/app/api/schemas/openai.py b/app/api/schemas/openai.py index e494ab5..eef3b8e 100644 --- a/app/api/schemas/openai.py +++ b/app/api/schemas/openai.py @@ -17,15 +17,21 @@ class OpenAIAudioModel(BaseModel): format: str +class OpenAIContentFileModel(BaseModel): + filename: str | None = None + file_data: str | None = None + file_id: str | None = None + class OpenAIContentModel(BaseModel): - type: str # One of: "text", "image_url", "input_audio" + type: str # One of: "text", "image_url", "input_audio", "file" text: str | None = None image_url: OpenAIContentImageUrlModel | None = None input_audio: OpenAIAudioModel | None = None + file: OpenAIContentFileModel | None = None def __init__(self, **data: Any): super().__init__(**data) - if self.type not in ["text", "image_url", "input_audio"]: + if self.type not in ["text", "image_url", "input_audio", "file"]: error_message = f"Invalid type: {self.type}. Must be one of: text, image_url, input_audio" logger.error(error_message) raise InvalidCompletionRequestException( @@ -55,6 +61,13 @@ def __init__(self, **data: Any): provider_name="openai", error=ValueError(error_message) ) + if self.type == "file" and self.file is None: + error_message = "file field must be set when type is 'file'" + logger.error(error_message) + raise InvalidCompletionRequestException( + provider_name="openai", + error=ValueError(error_message) + ) # --------------------------------------------------------------------------- diff --git a/app/exceptions/exceptions.py b/app/exceptions/exceptions.py index ca2e516..5113251 100644 --- a/app/exceptions/exceptions.py +++ b/app/exceptions/exceptions.py @@ -76,7 +76,7 @@ class InvalidCompletionRequestException(BaseInvalidRequestException): def __init__(self, provider_name: str, error: Exception): self.provider_name = provider_name self.error = error - super().__init__(f"Provider {provider_name} completion request is invalid: {error}") + super().__init__(self.provider_name, self.error) class InvalidEmbeddingsRequestException(BaseInvalidRequestException): """Exception raised when a embeddings request is invalid.""" @@ -84,7 +84,7 @@ class InvalidEmbeddingsRequestException(BaseInvalidRequestException): def __init__(self, provider_name: str, error: Exception): self.provider_name = provider_name self.error = error - super().__init__(f"Provider {provider_name} embeddings request is invalid: {error}") + super().__init__(self.provider_name, self.error) class BaseInvalidForgeKeyException(BaseForgeException): """Exception raised when a Forge key is invalid.""" diff --git a/app/services/providers/anthropic_adapter.py b/app/services/providers/anthropic_adapter.py index 6577e3f..55deba6 100644 --- a/app/services/providers/anthropic_adapter.py +++ b/app/services/providers/anthropic_adapter.py @@ -58,6 +58,48 @@ def format_anthropic_usage(usage_data: dict[str, Any], token_usage: dict[str, in }, } + @staticmethod + async def convert_openai_file_content_to_anthropic( + msg: dict[str, Any], allow_url_download: bool = False + ) -> dict[str, Any]: + """Convert OpenAI file content to Anthropic file content""" + # Only support pdf & plain text files for now + file = msg["file"] + file_data = file.get("file_data") + if not file_data: + raise InvalidCompletionRequestException( + provider_name="anthropic", error=ValueError("file_data is required for file content in anthropic") + ) + + if file_data.startswith("data:"): + # Extract media type and base64 data, assume it's a pdf file and return it as a base64 document + parts = file_data.split(",", 1) + media_type = parts[0].split(":")[1].split(";")[0] # e.g., "application/pdf" + base64_data = parts[1] # The actual base64 string without prefix + if not media_type == "application/pdf": + raise InvalidCompletionRequestException( + provider_name="anthropic", error=ValueError("Only application/pdf files are supported for base64 file content in anthropic") + ) + return { + "type": "document", + "source": { + "data": base64_data, + "media_type": media_type, + "type": "base64", + } + } + else: + # Treat it as a plain text file + return { + "type": "document", + "source": { + "data": file_data, + "media_type": "text/plain", + "type": "text", + } + } + + @staticmethod async def convert_openai_image_content_to_anthropic( msg: dict[str, Any], allow_url_download: bool = False @@ -154,6 +196,10 @@ async def convert_openai_content_to_anthropic( result.append( await AnthropicAdapter.convert_openai_image_content_to_anthropic(msg, allow_url_download=allow_url_download) ) + elif _type == "file": + result.append( + await AnthropicAdapter.convert_openai_file_content_to_anthropic(msg, allow_url_download=allow_url_download) + ) else: error_message = f"{_type} is not supported" logger.error(error_message) diff --git a/app/utils/anthropic_converter.py b/app/utils/anthropic_converter.py index 7ea3209..05c3480 100644 --- a/app/utils/anthropic_converter.py +++ b/app/utils/anthropic_converter.py @@ -23,7 +23,6 @@ ToolChoice, Usage, ) -from app.api.schemas.openai import ChatMessage, OpenAIContentModel from app.core.logger import get_logger logger = get_logger(name="anthropic_converter")