From bbf2255e10c21ef87941a9eb21db51bd6457698d Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Fri, 27 Feb 2026 10:56:49 +0100 Subject: [PATCH 1/3] refactor: make _parse_content a pure parser, add Chunk type param - Remove **parameters from _parse_content across ~28 provider clients, base abstract method, docstrings, templates, and tests - Add 5th type parameter (Chunk) to ModalityClient and all 5 modality base clients for correct mypy typing of _stream_class overrides - Eliminate 4 duplicated _transform_output overrides in audio providers by implementing parse_output() on their OutputFormatMapper mappers - Remove if-value-is-not-None guard in base _transform_output so parse_output() always runs (safe: default is no-op) Closes #202 --- src/celeste/client.py | 21 +++++++++------- src/celeste/modalities/audio/client.py | 4 ++-- .../audio/providers/elevenlabs/client.py | 6 +---- .../audio/providers/google/client.py | 10 ++------ .../audio/providers/gradium/client.py | 6 +---- .../audio/providers/openai/client.py | 7 +----- src/celeste/modalities/embeddings/client.py | 7 +++++- .../embeddings/providers/google/client.py | 4 +--- src/celeste/modalities/images/client.py | 4 ++-- .../modalities/images/providers/bfl/client.py | 1 - .../images/providers/byteplus/client.py | 1 - .../images/providers/google/client.py | 3 +-- .../images/providers/google/gemini.py | 1 - .../images/providers/google/imagen.py | 24 ++++++++++++++----- .../images/providers/ollama/client.py | 1 - .../images/providers/openai/client.py | 1 - .../modalities/images/providers/xai/client.py | 1 - src/celeste/modalities/text/client.py | 6 +++-- .../text/providers/anthropic/client.py | 1 - .../text/providers/cohere/client.py | 1 - .../text/providers/deepseek/client.py | 1 - .../text/providers/google/client.py | 1 - .../modalities/text/providers/groq/client.py | 1 - .../text/providers/huggingface/client.py | 1 - .../text/providers/mistral/client.py | 1 - .../text/providers/moonshot/client.py | 1 - .../text/providers/openai/client.py | 1 - .../text/providers/openresponses/client.py | 1 - .../modalities/text/providers/xai/client.py | 1 - src/celeste/modalities/videos/client.py | 4 ++-- .../videos/providers/byteplus/client.py | 1 - .../videos/providers/google/client.py | 1 - .../videos/providers/openai/client.py | 1 - .../modalities/videos/providers/xai/client.py | 1 - .../providers/anthropic/messages/client.py | 2 +- src/celeste/providers/bfl/images/client.py | 2 +- .../providers/byteplus/images/client.py | 2 +- .../providers/byteplus/videos/client.py | 2 +- src/celeste/providers/cohere/chat/client.py | 2 +- .../elevenlabs/text_to_speech/parameters.py | 16 +++++++++++++ .../providers/google/cloud_tts/client.py | 2 +- .../providers/google/cloud_tts/parameters.py | 8 +++++++ .../providers/google/embeddings/client.py | 2 +- .../google/generate_content/client.py | 2 +- src/celeste/providers/google/imagen/client.py | 2 +- .../providers/google/interactions/client.py | 2 +- src/celeste/providers/google/veo/client.py | 2 +- .../gradium/text_to_speech/parameters.py | 15 ++++++++++++ .../providers/openai/audio/parameters.py | 19 ++++++++++++++- .../{modality_slug}/client.py.template | 4 ++-- .../{provider_slug}/client.py.template | 1 - tests/unit_tests/test_client.py | 8 +++---- 52 files changed, 127 insertions(+), 93 deletions(-) diff --git a/src/celeste/client.py b/src/celeste/client.py index 236e17d..ed92449 100644 --- a/src/celeste/client.py +++ b/src/celeste/client.py @@ -12,7 +12,8 @@ from celeste.core import Modality, Provider from celeste.exceptions import StreamingNotSupportedError from celeste.http import HTTPClient, get_http_client -from celeste.io import Chunk, FinishReason, Input, Output, Usage +from celeste.io import Chunk as ChunkBase +from celeste.io import FinishReason, Input, Output, Usage from celeste.mime_types import ApplicationMimeType from celeste.models import Model from celeste.parameters import ParameterMapper, Parameters @@ -130,15 +131,19 @@ def _handle_error_response(self, response: httpx.Response) -> None: super()._handle_error_response(response) # type: ignore[misc] -class ModalityClient[In: Input, Out: Output, Params: Parameters, Content]( - APIMixin, BaseModel -): +class ModalityClient[ + In: Input, + Out: Output, + Params: Parameters, + Content, + Chunk: ChunkBase, +](APIMixin, BaseModel): """Base class for unified modality clients. Operation methods in subclasses delegate to _predict(). Example: - class ImagesClient(ModalityClient[ImagesInput, ImagesOutput, ImagesParameters, ImageContent]): + class ImagesClient(ModalityClient[ImagesInput, ImagesOutput, ImagesParameters, ImageContent, ImageChunk]): modality = Modality.IMAGES async def generate(self, prompt: str, **parameters) -> ImageGenerationOutput: @@ -198,7 +203,7 @@ async def _predict( response_data = await self._make_request( request_body, endpoint=endpoint, extra_headers=extra_headers, **parameters ) - content = self._parse_content(response_data, **parameters) + content = self._parse_content(response_data) content = self._transform_output(content, **parameters) return self._output_class()( content=content, @@ -277,7 +282,6 @@ def _parse_usage(self, response_data: dict[str, Any]) -> RawUsage: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[Params], # type: ignore[misc] ) -> Content: """Parse content from provider response.""" ... @@ -384,8 +388,7 @@ def _transform_output( """Transform content using parameter mapper output transformations.""" for mapper in self.parameter_mappers(): value = parameters.get(mapper.name) - if value is not None: - content = mapper.parse_output(content, value) + content = mapper.parse_output(content, value) return content @abstractmethod diff --git a/src/celeste/modalities/audio/client.py b/src/celeste/modalities/audio/client.py index 6e0e281..8fc95ef 100644 --- a/src/celeste/modalities/audio/client.py +++ b/src/celeste/modalities/audio/client.py @@ -8,13 +8,13 @@ from celeste.core import Modality from celeste.types import AudioContent -from .io import AudioFinishReason, AudioInput, AudioOutput, AudioUsage +from .io import AudioChunk, AudioFinishReason, AudioInput, AudioOutput, AudioUsage from .parameters import AudioParameters from .streaming import AudioStream class AudioClient( - ModalityClient[AudioInput, AudioOutput, AudioParameters, AudioContent] + ModalityClient[AudioInput, AudioOutput, AudioParameters, AudioContent, AudioChunk] ): """Base audio client. Providers implement speak() method.""" diff --git a/src/celeste/modalities/audio/providers/elevenlabs/client.py b/src/celeste/modalities/audio/providers/elevenlabs/client.py index cc230ed..9087f0e 100644 --- a/src/celeste/modalities/audio/providers/elevenlabs/client.py +++ b/src/celeste/modalities/audio/providers/elevenlabs/client.py @@ -64,7 +64,6 @@ def _init_request(self, inputs: AudioInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[AudioParameters], ) -> AudioArtifact: """Extract audio bytes from response.""" audio_bytes = response_data.get("audio_bytes") @@ -72,10 +71,7 @@ def _parse_content( msg = "No audio data in response" raise ValueError(msg) - output_format = parameters.get("output_format") - mime_type = self._map_output_format_to_mime_type(output_format) - - return AudioArtifact(data=audio_bytes, mime_type=mime_type) + return AudioArtifact(data=audio_bytes) def _stream_class(self) -> type[AudioStream]: """Return the Stream class for this provider.""" diff --git a/src/celeste/modalities/audio/providers/google/client.py b/src/celeste/modalities/audio/providers/google/client.py index 96ea1d2..7343580 100644 --- a/src/celeste/modalities/audio/providers/google/client.py +++ b/src/celeste/modalities/audio/providers/google/client.py @@ -3,7 +3,6 @@ from typing import Any, Unpack from celeste.artifacts import AudioArtifact -from celeste.mime_types import AudioMimeType from celeste.parameters import ParameterMapper from celeste.providers.google.cloud_tts import config from celeste.providers.google.cloud_tts.client import ( @@ -16,7 +15,7 @@ AudioInput, AudioOutput, ) -from ...parameters import AudioParameter, AudioParameters +from ...parameters import AudioParameters from .parameters import GOOGLE_PARAMETER_MAPPERS @@ -51,15 +50,10 @@ def _init_request(self, inputs: AudioInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[AudioParameters], ) -> AudioArtifact: """Extract audio bytes from response.""" audio_b64 = super()._parse_content(response_data) - - output_format = parameters.get(AudioParameter.OUTPUT_FORMAT) - mime_type = AudioMimeType(output_format) if output_format else AudioMimeType.MP3 - - return AudioArtifact(data=audio_b64, mime_type=mime_type) + return AudioArtifact(data=audio_b64) __all__ = ["GoogleAudioClient"] diff --git a/src/celeste/modalities/audio/providers/gradium/client.py b/src/celeste/modalities/audio/providers/gradium/client.py index 7ed7c06..61769aa 100644 --- a/src/celeste/modalities/audio/providers/gradium/client.py +++ b/src/celeste/modalities/audio/providers/gradium/client.py @@ -64,7 +64,6 @@ def _init_request(self, inputs: AudioInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[AudioParameters], ) -> AudioArtifact: """Extract audio bytes from response.""" audio_bytes = response_data.get("audio_bytes") @@ -72,10 +71,7 @@ def _parse_content( msg = "No audio data in response" raise ValueError(msg) - output_format = parameters.get("output_format") - mime_type = self._map_output_format_to_mime_type(output_format) - - return AudioArtifact(data=audio_bytes, mime_type=mime_type) + return AudioArtifact(data=audio_bytes) def _stream_class(self) -> type[AudioStream]: """Return the Stream class for this provider.""" diff --git a/src/celeste/modalities/audio/providers/openai/client.py b/src/celeste/modalities/audio/providers/openai/client.py index bd7daee..9053fc0 100644 --- a/src/celeste/modalities/audio/providers/openai/client.py +++ b/src/celeste/modalities/audio/providers/openai/client.py @@ -41,7 +41,6 @@ def _init_request(self, inputs: AudioInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[AudioParameters], ) -> AudioArtifact: """Extract audio bytes from response.""" audio_bytes = response_data.get("audio_bytes") @@ -49,11 +48,7 @@ def _parse_content( msg = "No audio data in response" raise ValueError(msg) - # Use mixin helper to determine MIME type from output_format - output_format = parameters.get("output_format") - mime_type = self._map_response_format_to_mime_type(output_format) - - return AudioArtifact(data=audio_bytes, mime_type=mime_type) + return AudioArtifact(data=audio_bytes) def _parse_finish_reason(self, response_data: dict[str, Any]) -> AudioFinishReason: """OpenAI TTS doesn't provide finish reasons.""" diff --git a/src/celeste/modalities/embeddings/client.py b/src/celeste/modalities/embeddings/client.py index d5ee4d9..a98c1da 100644 --- a/src/celeste/modalities/embeddings/client.py +++ b/src/celeste/modalities/embeddings/client.py @@ -9,6 +9,7 @@ from celeste.types import EmbeddingsContent from .io import ( + EmbeddingsChunk, EmbeddingsFinishReason, EmbeddingsInput, EmbeddingsOutput, @@ -19,7 +20,11 @@ class EmbeddingsClient( ModalityClient[ - EmbeddingsInput, EmbeddingsOutput, EmbeddingsParameters, EmbeddingsContent + EmbeddingsInput, + EmbeddingsOutput, + EmbeddingsParameters, + EmbeddingsContent, + EmbeddingsChunk, ] ): """Base embeddings client. Providers implement operation methods.""" diff --git a/src/celeste/modalities/embeddings/providers/google/client.py b/src/celeste/modalities/embeddings/providers/google/client.py index aa857ca..683f10b 100644 --- a/src/celeste/modalities/embeddings/providers/google/client.py +++ b/src/celeste/modalities/embeddings/providers/google/client.py @@ -1,6 +1,6 @@ """Google embeddings client.""" -from typing import Any, Unpack +from typing import Any from celeste.parameters import ParameterMapper from celeste.providers.google.embeddings.client import ( @@ -10,7 +10,6 @@ from ...client import EmbeddingsClient from ...io import EmbeddingsInput -from ...parameters import EmbeddingsParameters from .parameters import GOOGLE_PARAMETER_MAPPERS @@ -42,7 +41,6 @@ def _init_request(self, inputs: EmbeddingsInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[EmbeddingsParameters], ) -> EmbeddingsContent: """Parse embedding vectors from response.""" return super()._parse_content(response_data) diff --git a/src/celeste/modalities/images/client.py b/src/celeste/modalities/images/client.py index ed471e8..eca8bc9 100644 --- a/src/celeste/modalities/images/client.py +++ b/src/celeste/modalities/images/client.py @@ -9,13 +9,13 @@ from celeste.core import Modality from celeste.types import ImageContent -from .io import ImageFinishReason, ImageInput, ImageOutput, ImageUsage +from .io import ImageChunk, ImageFinishReason, ImageInput, ImageOutput, ImageUsage from .parameters import ImageParameters from .streaming import ImagesStream class ImagesClient( - ModalityClient[ImageInput, ImageOutput, ImageParameters, ImageContent] + ModalityClient[ImageInput, ImageOutput, ImageParameters, ImageContent, ImageChunk] ): """Base images client. Providers implement generate/edit methods.""" diff --git a/src/celeste/modalities/images/providers/bfl/client.py b/src/celeste/modalities/images/providers/bfl/client.py index 73a9d94..8ce1245 100644 --- a/src/celeste/modalities/images/providers/bfl/client.py +++ b/src/celeste/modalities/images/providers/bfl/client.py @@ -59,7 +59,6 @@ def _init_request(self, inputs: ImageInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[ImageParameters], ) -> ImageArtifact: """Parse content from response.""" result = super()._parse_content(response_data) diff --git a/src/celeste/modalities/images/providers/byteplus/client.py b/src/celeste/modalities/images/providers/byteplus/client.py index b5829fc..1660b03 100644 --- a/src/celeste/modalities/images/providers/byteplus/client.py +++ b/src/celeste/modalities/images/providers/byteplus/client.py @@ -121,7 +121,6 @@ def _init_request(self, inputs: ImageInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[ImageParameters], ) -> ImageArtifact: """Parse content from response.""" content = super()._parse_content(response_data) diff --git a/src/celeste/modalities/images/providers/google/client.py b/src/celeste/modalities/images/providers/google/client.py index 265c8dd..f88c794 100644 --- a/src/celeste/modalities/images/providers/google/client.py +++ b/src/celeste/modalities/images/providers/google/client.py @@ -89,9 +89,8 @@ def _parse_usage( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[ImageParameters], ) -> ImageContent: - return self._strategy._parse_content(response_data, **parameters) # type: ignore[union-attr] + return self._strategy._parse_content(response_data) # type: ignore[union-attr] def _parse_finish_reason(self, response_data: dict[str, Any]) -> ImageFinishReason: return self._strategy._parse_finish_reason(response_data) # type: ignore[union-attr] diff --git a/src/celeste/modalities/images/providers/google/gemini.py b/src/celeste/modalities/images/providers/google/gemini.py index 530342b..472c87d 100644 --- a/src/celeste/modalities/images/providers/google/gemini.py +++ b/src/celeste/modalities/images/providers/google/gemini.py @@ -101,7 +101,6 @@ def _parse_usage( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[ImageParameters], ) -> ImageContent: """Parse image artifacts from Gemini candidates.""" candidates = super()._parse_content(response_data) diff --git a/src/celeste/modalities/images/providers/google/imagen.py b/src/celeste/modalities/images/providers/google/imagen.py index d2faa1a..ec33486 100644 --- a/src/celeste/modalities/images/providers/google/imagen.py +++ b/src/celeste/modalities/images/providers/google/imagen.py @@ -44,7 +44,6 @@ def _init_request(self, inputs: ImageInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[ImageParameters], ) -> ImageContent: """Parse image artifacts from Imagen predictions.""" predictions = super()._parse_content(response_data) @@ -57,14 +56,27 @@ def _parse_content( mime_type = ImageMimeType(prediction.get("mimeType", "image/png")) images.append(ImageArtifact(data=base64_data, mime_type=mime_type)) - num_images_requested = parameters.get("num_images") - if num_images_requested == 1: - return images[0] if images else ImageArtifact() - if num_images_requested is not None and num_images_requested > 1: - return images if images else [] if len(images) == 1: return images[0] return images if images else ImageArtifact() + def _transform_output( + self, + content: ImageContent, + **parameters: Unpack[ImageParameters], + ) -> ImageContent: + """Singularize/pluralize based on num_images parameter.""" + content = super()._transform_output(content, **parameters) + num_images_requested = parameters.get("num_images") + if num_images_requested == 1 and isinstance(content, list): + return content[0] if content else ImageArtifact() + if ( + num_images_requested is not None + and num_images_requested > 1 + and not isinstance(content, list) + ): + return [content] + return content + __all__ = ["ImagenImagesClient"] diff --git a/src/celeste/modalities/images/providers/ollama/client.py b/src/celeste/modalities/images/providers/ollama/client.py index c600c7c..539d5c9 100644 --- a/src/celeste/modalities/images/providers/ollama/client.py +++ b/src/celeste/modalities/images/providers/ollama/client.py @@ -87,7 +87,6 @@ def _parse_usage( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[ImageParameters], ) -> ImageArtifact: """Parse content from response. diff --git a/src/celeste/modalities/images/providers/openai/client.py b/src/celeste/modalities/images/providers/openai/client.py index 325e04c..b08b87c 100644 --- a/src/celeste/modalities/images/providers/openai/client.py +++ b/src/celeste/modalities/images/providers/openai/client.py @@ -77,7 +77,6 @@ async def edit( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[ImageParameters], ) -> ImageArtifact: """Parse content from response.""" data = super()._parse_content(response_data) diff --git a/src/celeste/modalities/images/providers/xai/client.py b/src/celeste/modalities/images/providers/xai/client.py index 1b10e2a..c6aa4bb 100644 --- a/src/celeste/modalities/images/providers/xai/client.py +++ b/src/celeste/modalities/images/providers/xai/client.py @@ -67,7 +67,6 @@ async def edit( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[ImageParameters], ) -> ImageArtifact: """Parse content from response.""" data = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/client.py b/src/celeste/modalities/text/client.py index 50bfc02..f46ee61 100644 --- a/src/celeste/modalities/text/client.py +++ b/src/celeste/modalities/text/client.py @@ -8,12 +8,14 @@ from celeste.core import InputType, Modality from celeste.types import AudioContent, ImageContent, Message, TextContent, VideoContent -from .io import TextFinishReason, TextInput, TextOutput, TextUsage +from .io import TextChunk, TextFinishReason, TextInput, TextOutput, TextUsage from .parameters import TextParameters from .streaming import TextStream -class TextClient(ModalityClient[TextInput, TextOutput, TextParameters, TextContent]): +class TextClient( + ModalityClient[TextInput, TextOutput, TextParameters, TextContent, TextChunk] +): """Base text client. Providers implement operation methods (generate, analyze). diff --git a/src/celeste/modalities/text/providers/anthropic/client.py b/src/celeste/modalities/text/providers/anthropic/client.py index 05be71b..8d0ae8c 100644 --- a/src/celeste/modalities/text/providers/anthropic/client.py +++ b/src/celeste/modalities/text/providers/anthropic/client.py @@ -153,7 +153,6 @@ def _build_image_source(self, img: ImageArtifact) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" content = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/cohere/client.py b/src/celeste/modalities/text/providers/cohere/client.py index 8f3fd25..31621f8 100644 --- a/src/celeste/modalities/text/providers/cohere/client.py +++ b/src/celeste/modalities/text/providers/cohere/client.py @@ -80,7 +80,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" content_array = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/deepseek/client.py b/src/celeste/modalities/text/providers/deepseek/client.py index 356c100..100e481 100644 --- a/src/celeste/modalities/text/providers/deepseek/client.py +++ b/src/celeste/modalities/text/providers/deepseek/client.py @@ -60,7 +60,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" choices = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/google/client.py b/src/celeste/modalities/text/providers/google/client.py index d418be5..d026745 100644 --- a/src/celeste/modalities/text/providers/google/client.py +++ b/src/celeste/modalities/text/providers/google/client.py @@ -172,7 +172,6 @@ def _build_audio_part(self, audio: AudioArtifact) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" candidates = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/groq/client.py b/src/celeste/modalities/text/providers/groq/client.py index 97b6d4d..aa180d7 100644 --- a/src/celeste/modalities/text/providers/groq/client.py +++ b/src/celeste/modalities/text/providers/groq/client.py @@ -78,7 +78,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" choices = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/huggingface/client.py b/src/celeste/modalities/text/providers/huggingface/client.py index d586198..4d292f5 100644 --- a/src/celeste/modalities/text/providers/huggingface/client.py +++ b/src/celeste/modalities/text/providers/huggingface/client.py @@ -80,7 +80,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" choices = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/mistral/client.py b/src/celeste/modalities/text/providers/mistral/client.py index cf621e8..896cdf5 100644 --- a/src/celeste/modalities/text/providers/mistral/client.py +++ b/src/celeste/modalities/text/providers/mistral/client.py @@ -77,7 +77,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" choices = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/moonshot/client.py b/src/celeste/modalities/text/providers/moonshot/client.py index 9640d6e..e0fa905 100644 --- a/src/celeste/modalities/text/providers/moonshot/client.py +++ b/src/celeste/modalities/text/providers/moonshot/client.py @@ -77,7 +77,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" choices = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/openai/client.py b/src/celeste/modalities/text/providers/openai/client.py index 175c2cf..14de021 100644 --- a/src/celeste/modalities/text/providers/openai/client.py +++ b/src/celeste/modalities/text/providers/openai/client.py @@ -79,7 +79,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse text content from response.""" output = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/openresponses/client.py b/src/celeste/modalities/text/providers/openresponses/client.py index 6fc23c3..59ca587 100644 --- a/src/celeste/modalities/text/providers/openresponses/client.py +++ b/src/celeste/modalities/text/providers/openresponses/client.py @@ -106,7 +106,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse text content from response.""" output = super()._parse_content(response_data) diff --git a/src/celeste/modalities/text/providers/xai/client.py b/src/celeste/modalities/text/providers/xai/client.py index 71dfc4c..e67c40d 100644 --- a/src/celeste/modalities/text/providers/xai/client.py +++ b/src/celeste/modalities/text/providers/xai/client.py @@ -78,7 +78,6 @@ def _init_request(self, inputs: TextInput) -> dict[str, Any]: def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[TextParameters], ) -> TextContent: """Parse content from response.""" output = super()._parse_content(response_data) diff --git a/src/celeste/modalities/videos/client.py b/src/celeste/modalities/videos/client.py index 1522058..481460a 100644 --- a/src/celeste/modalities/videos/client.py +++ b/src/celeste/modalities/videos/client.py @@ -8,12 +8,12 @@ from celeste.core import Modality from celeste.types import VideoContent -from .io import VideoFinishReason, VideoInput, VideoOutput, VideoUsage +from .io import VideoChunk, VideoFinishReason, VideoInput, VideoOutput, VideoUsage from .parameters import VideoParameters class VideosClient( - ModalityClient[VideoInput, VideoOutput, VideoParameters, VideoContent] + ModalityClient[VideoInput, VideoOutput, VideoParameters, VideoContent, VideoChunk] ): """Base videos client. Providers implement generate method.""" diff --git a/src/celeste/modalities/videos/providers/byteplus/client.py b/src/celeste/modalities/videos/providers/byteplus/client.py index f467207..9c4388f 100644 --- a/src/celeste/modalities/videos/providers/byteplus/client.py +++ b/src/celeste/modalities/videos/providers/byteplus/client.py @@ -45,7 +45,6 @@ async def generate( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[VideoParameters], ) -> VideoArtifact: """Parse content from response.""" content = super()._parse_content(response_data) diff --git a/src/celeste/modalities/videos/providers/google/client.py b/src/celeste/modalities/videos/providers/google/client.py index fdbbaaa..8ef293a 100644 --- a/src/celeste/modalities/videos/providers/google/client.py +++ b/src/celeste/modalities/videos/providers/google/client.py @@ -44,7 +44,6 @@ async def generate( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[VideoParameters], ) -> VideoArtifact: """Parse content from response.""" video_data = super()._parse_content(response_data) diff --git a/src/celeste/modalities/videos/providers/openai/client.py b/src/celeste/modalities/videos/providers/openai/client.py index 4509616..0b566ec 100644 --- a/src/celeste/modalities/videos/providers/openai/client.py +++ b/src/celeste/modalities/videos/providers/openai/client.py @@ -46,7 +46,6 @@ async def generate( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[VideoParameters], ) -> VideoArtifact: """Parse content from response.""" video_data_b64 = super()._parse_content(response_data) diff --git a/src/celeste/modalities/videos/providers/xai/client.py b/src/celeste/modalities/videos/providers/xai/client.py index fc3c8b5..02a234f 100644 --- a/src/celeste/modalities/videos/providers/xai/client.py +++ b/src/celeste/modalities/videos/providers/xai/client.py @@ -58,7 +58,6 @@ async def edit( def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[VideoParameters], ) -> VideoArtifact: """Parse content from response.""" # xAI returns video URL directly diff --git a/src/celeste/providers/anthropic/messages/client.py b/src/celeste/providers/anthropic/messages/client.py index bad241a..d87f8a6 100644 --- a/src/celeste/providers/anthropic/messages/client.py +++ b/src/celeste/providers/anthropic/messages/client.py @@ -28,7 +28,7 @@ class AnthropicMessagesClient(APIMixin): Usage: class AnthropicTextGenerationClient(AnthropicMessagesClient, TextGenerationClient): - def _parse_content(self, response_data, **parameters): + def _parse_content(self, response_data): content = super()._parse_content(response_data) # Raw content array for block in content: if block.get("type") == "text": diff --git a/src/celeste/providers/bfl/images/client.py b/src/celeste/providers/bfl/images/client.py index 1b16565..b35992f 100644 --- a/src/celeste/providers/bfl/images/client.py +++ b/src/celeste/providers/bfl/images/client.py @@ -28,7 +28,7 @@ class BFLImagesClient(APIMixin): Usage: class BFLImageGenerationClient(BFLImagesClient, ImageGenerationClient): - def _parse_content(self, response_data, **parameters): + def _parse_content(self, response_data): result = response_data.get("result", {}) # Extract image from result["sample"]... """ diff --git a/src/celeste/providers/byteplus/images/client.py b/src/celeste/providers/byteplus/images/client.py index 611595b..d573181 100644 --- a/src/celeste/providers/byteplus/images/client.py +++ b/src/celeste/providers/byteplus/images/client.py @@ -23,7 +23,7 @@ class BytePlusImagesClient(APIMixin): Usage: class BytePlusImageGenerationClient(BytePlusImagesClient, ImageGenerationClient): - def _parse_content(self, response_data, **parameters): + def _parse_content(self, response_data): images = response_data.get("images", []) # Extract image from images[0] or data[0]... """ diff --git a/src/celeste/providers/byteplus/videos/client.py b/src/celeste/providers/byteplus/videos/client.py index 14200d7..4f18899 100644 --- a/src/celeste/providers/byteplus/videos/client.py +++ b/src/celeste/providers/byteplus/videos/client.py @@ -29,7 +29,7 @@ class BytePlusVideosClient(APIMixin): Usage: class BytePlusVideoGenerationClient(BytePlusVideosClient, VideoGenerationClient): - def _parse_content(self, response_data, **parameters): + def _parse_content(self, response_data): content = response_data.get("content", {}) # Extract video from content["video_url"]... """ diff --git a/src/celeste/providers/cohere/chat/client.py b/src/celeste/providers/cohere/chat/client.py index a5891fa..2cf5e91 100644 --- a/src/celeste/providers/cohere/chat/client.py +++ b/src/celeste/providers/cohere/chat/client.py @@ -23,7 +23,7 @@ class CohereChatClient(APIMixin): Usage: class CohereTextGenerationClient(CohereChatClient, TextGenerationClient): - def _parse_content(self, response_data, **parameters): + def _parse_content(self, response_data): content_array = super()._parse_content(response_data) text = content_array[0].get("text") or "" return text diff --git a/src/celeste/providers/elevenlabs/text_to_speech/parameters.py b/src/celeste/providers/elevenlabs/text_to_speech/parameters.py index a90f9ec..d55b858 100644 --- a/src/celeste/providers/elevenlabs/text_to_speech/parameters.py +++ b/src/celeste/providers/elevenlabs/text_to_speech/parameters.py @@ -2,6 +2,7 @@ from typing import Any +from celeste.artifacts import AudioArtifact from celeste.models import Model from celeste.parameters import FieldMapper, ParameterMapper from celeste.types import AudioContent @@ -36,6 +37,21 @@ def map( request["output_format"] = validated_value return request + def parse_output(self, content: AudioContent, value: object | None) -> AudioContent: + """Apply output_format → MIME type mapping to parsed content.""" + if not isinstance(content, AudioArtifact): + return content + + # Lazy import to avoid circular dependency + from celeste.providers.elevenlabs.text_to_speech.client import ( + ElevenLabsTextToSpeechClient, + ) + + mime_type = ElevenLabsTextToSpeechClient._map_output_format_to_mime_type( + str(value) if value else None + ) + return AudioArtifact(data=content.data, mime_type=mime_type) + class SpeedMapper(ParameterMapper[AudioContent]): """Map speed parameter to ElevenLabs voice_settings.speed field.""" diff --git a/src/celeste/providers/google/cloud_tts/client.py b/src/celeste/providers/google/cloud_tts/client.py index c61414b..24aa31c 100644 --- a/src/celeste/providers/google/cloud_tts/client.py +++ b/src/celeste/providers/google/cloud_tts/client.py @@ -23,7 +23,7 @@ class GoogleCloudTTSClient(APIMixin): Capability clients extend via super() to wrap results in artifacts: class GoogleSpeechGenerationClient(GoogleCloudTTSClient, SpeechGenerationClient): - def _parse_content(self, response_data, **params): + def _parse_content(self, response_data): audio_b64 = super()._parse_content(response_data) # Get base64 string audio_bytes = base64.b64decode(audio_b64) return AudioArtifact(data=audio_bytes, mime_type=..., ...) diff --git a/src/celeste/providers/google/cloud_tts/parameters.py b/src/celeste/providers/google/cloud_tts/parameters.py index 2e12c18..8e74495 100644 --- a/src/celeste/providers/google/cloud_tts/parameters.py +++ b/src/celeste/providers/google/cloud_tts/parameters.py @@ -2,6 +2,7 @@ from typing import Any, ClassVar +from celeste.artifacts import AudioArtifact from celeste.mime_types import AudioMimeType from celeste.models import Model from celeste.parameters import ParameterMapper @@ -97,6 +98,13 @@ def map( request.setdefault("audioConfig", {})["audioEncoding"] = encoding return request + def parse_output(self, content: AudioContent, value: object | None) -> AudioContent: + """Apply output_format → MIME type mapping to parsed content.""" + if not isinstance(content, AudioArtifact): + return content + mime_type = AudioMimeType(value) if value else AudioMimeType.MP3 + return AudioArtifact(data=content.data, mime_type=mime_type) + __all__ = [ "AudioEncodingMapper", diff --git a/src/celeste/providers/google/embeddings/client.py b/src/celeste/providers/google/embeddings/client.py index 235ac1b..a3cb7a5 100644 --- a/src/celeste/providers/google/embeddings/client.py +++ b/src/celeste/providers/google/embeddings/client.py @@ -24,7 +24,7 @@ class GoogleEmbeddingsClient(APIMixin): Capability clients extend via super(): class GoogleEmbeddingsClient(GoogleEmbeddingsClient, EmbeddingsClient): - def _parse_content(self, response_data, **params): + def _parse_content(self, response_data): return super()._parse_content(response_data) # No transformation needed """ diff --git a/src/celeste/providers/google/generate_content/client.py b/src/celeste/providers/google/generate_content/client.py index c1402dc..54ba62f 100644 --- a/src/celeste/providers/google/generate_content/client.py +++ b/src/celeste/providers/google/generate_content/client.py @@ -30,7 +30,7 @@ class GoogleGenerateContentClient(APIMixin): Usage: class GoogleTextGenerationClient(GoogleGenerateContentClient, TextGenerationClient): - def _parse_content(self, response_data, **parameters): + def _parse_content(self, response_data): parts = super()._parse_content(response_data) text = parts[0].get("text") or "" return text diff --git a/src/celeste/providers/google/imagen/client.py b/src/celeste/providers/google/imagen/client.py index e91fe6c..7ca62ff 100644 --- a/src/celeste/providers/google/imagen/client.py +++ b/src/celeste/providers/google/imagen/client.py @@ -30,7 +30,7 @@ class GoogleImagenClient(APIMixin): Usage: class GoogleImageGenerationClient(GoogleImagenClient, ImageGenerationClient): - def _parse_content(self, response_data, **parameters): + def _parse_content(self, response_data): predictions = super()._parse_content(response_data) # Extract image from predictions[0]... """ diff --git a/src/celeste/providers/google/interactions/client.py b/src/celeste/providers/google/interactions/client.py index dc9d9d7..9083dec 100644 --- a/src/celeste/providers/google/interactions/client.py +++ b/src/celeste/providers/google/interactions/client.py @@ -30,7 +30,7 @@ class GoogleInteractionsClient(APIMixin): class GoogleInteractionsTextGenerationClient( GoogleInteractionsClient, TextGenerationClient ): - def _parse_content(self, response_data, **parameters): + def _parse_content(self, response_data): outputs = super()._parse_content(response_data) text = "".join(o.get("text", "") for o in outputs if o.get("type") == "text") return text diff --git a/src/celeste/providers/google/veo/client.py b/src/celeste/providers/google/veo/client.py index f456b5b..b3db236 100644 --- a/src/celeste/providers/google/veo/client.py +++ b/src/celeste/providers/google/veo/client.py @@ -25,7 +25,7 @@ class GoogleVeoClient(APIMixin): Capability clients extend via super() to wrap results in artifacts: class GoogleVideoGenerationClient(GoogleVeoClient, VideoGenerationClient): - def _parse_content(self, response_data, **params): + def _parse_content(self, response_data): video_data = super()._parse_content(response_data) # Get generic dict return VideoArtifact(url=video_data["uri"]) # Capability-specific diff --git a/src/celeste/providers/gradium/text_to_speech/parameters.py b/src/celeste/providers/gradium/text_to_speech/parameters.py index 1ff78ff..285ee26 100644 --- a/src/celeste/providers/gradium/text_to_speech/parameters.py +++ b/src/celeste/providers/gradium/text_to_speech/parameters.py @@ -2,6 +2,7 @@ from typing import Any +from celeste.artifacts import AudioArtifact from celeste.models import Model from celeste.parameters import FieldMapper, ParameterMapper from celeste.types import AudioContent @@ -36,6 +37,20 @@ class OutputFormatMapper(FieldMapper[AudioContent]): field = "output_format" + def parse_output(self, content: AudioContent, value: object | None) -> AudioContent: + """Apply output_format → MIME type mapping to parsed content.""" + if not isinstance(content, AudioArtifact): + return content + + from celeste.providers.gradium.text_to_speech.client import ( + GradiumTextToSpeechClient, + ) + + mime_type = GradiumTextToSpeechClient._map_output_format_to_mime_type( + str(value) if value else None + ) + return AudioArtifact(data=content.data, mime_type=mime_type) + __all__ = [ "OutputFormatMapper", diff --git a/src/celeste/providers/openai/audio/parameters.py b/src/celeste/providers/openai/audio/parameters.py index aaf0242..3637f2a 100644 --- a/src/celeste/providers/openai/audio/parameters.py +++ b/src/celeste/providers/openai/audio/parameters.py @@ -1,7 +1,8 @@ """OpenAI Audio API parameter mappers.""" -from typing import Any +from typing import Any, ClassVar +from celeste.artifacts import AudioArtifact from celeste.mime_types import AudioMimeType from celeste.models import Model from celeste.parameters import FieldMapper, ParameterMapper @@ -23,6 +24,15 @@ class SpeedMapper(FieldMapper[AudioContent]): class ResponseFormatMapper(ParameterMapper[AudioContent]): """Map response_format to OpenAI response_format field.""" + _mime_map: ClassVar[dict[str, AudioMimeType]] = { + "mp3": AudioMimeType.MP3, + "opus": AudioMimeType.OGG, + "aac": AudioMimeType.AAC, + "flac": AudioMimeType.FLAC, + "wav": AudioMimeType.WAV, + "pcm": AudioMimeType.WAV, + } + def map( self, request: dict[str, Any], @@ -56,6 +66,13 @@ def map( request["response_format"] = response_format return request + def parse_output(self, content: AudioContent, value: object | None) -> AudioContent: + """Apply response_format → MIME type mapping to parsed content.""" + if not isinstance(content, AudioArtifact): + return content + mime_type = self._mime_map.get(str(value) if value else "", AudioMimeType.MP3) + return AudioArtifact(data=content.data, mime_type=mime_type) + class InstructionsMapper(FieldMapper[AudioContent]): """Map instructions to OpenAI instructions field.""" diff --git a/templates/modalities/{modality_slug}/client.py.template b/templates/modalities/{modality_slug}/client.py.template index 134fe61..13971cc 100644 --- a/templates/modalities/{modality_slug}/client.py.template +++ b/templates/modalities/{modality_slug}/client.py.template @@ -8,12 +8,12 @@ from celeste.client import ModalityClient from celeste.core import InputType, Modality from celeste.types import AudioContent, ImageContent, {Content}, VideoContent -from .io import {Modality}Input, {Modality}Output +from .io import {Modality}Chunk, {Modality}Input, {Modality}Output from .parameters import {Modality}Parameters from .streaming import {Modality}Stream -class {Modality}Client(ModalityClient[{Modality}Input, {Modality}Output, {Modality}Parameters, {Content}]): +class {Modality}Client(ModalityClient[{Modality}Input, {Modality}Output, {Modality}Parameters, {Content}, {Modality}Chunk]): """Base {modality} client. Providers implement operation methods (generate, analyze). diff --git a/templates/modalities/{modality_slug}/providers/{provider_slug}/client.py.template b/templates/modalities/{modality_slug}/providers/{provider_slug}/client.py.template index b184507..8d83d03 100644 --- a/templates/modalities/{modality_slug}/providers/{provider_slug}/client.py.template +++ b/templates/modalities/{modality_slug}/providers/{provider_slug}/client.py.template @@ -63,7 +63,6 @@ class {Provider}{Modality}Client({Provider}{Api}Mixin, {Modality}Client): def _parse_content( self, response_data: dict[str, Any], - **parameters: Unpack[{Modality}Parameters], ) -> {Content}: """Parse content from response.""" data = super()._parse_content(response_data) diff --git a/tests/unit_tests/test_client.py b/tests/unit_tests/test_client.py index 0c39046..c1ee894 100644 --- a/tests/unit_tests/test_client.py +++ b/tests/unit_tests/test_client.py @@ -112,7 +112,9 @@ def api_key() -> str: return "sk-test123456789" -class ConcreteModalityClient(ModalityClient[_TestInput, Output, Parameters, str]): +class ConcreteModalityClient( + ModalityClient[_TestInput, Output, Parameters, str, Chunk] +): """Concrete ModalityClient implementation for testing.""" @classmethod @@ -127,9 +129,7 @@ def _parse_usage( ) -> dict[str, int | float | None]: return {} - def _parse_content( # type: ignore[override] - self, response_data: dict[str, Any], **parameters: Unpack[Parameters] - ) -> str: + def _parse_content(self, response_data: dict[str, Any]) -> str: content = response_data.get("content", "test content") return content if isinstance(content, str) else "test content" From 7d77f9debcd84b3404ab5f8c6983dce24fbd6a36 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Fri, 27 Feb 2026 11:36:57 +0100 Subject: [PATCH 2/3] fix: imagen _parse_content regression for empty images When predictions exist but none contain valid image data, _parse_content now returns [] instead of ImageArtifact() sentinel, preventing _transform_output from wrapping it into [ImageArtifact()]. --- src/celeste/modalities/images/providers/google/imagen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/celeste/modalities/images/providers/google/imagen.py b/src/celeste/modalities/images/providers/google/imagen.py index ec33486..f60727c 100644 --- a/src/celeste/modalities/images/providers/google/imagen.py +++ b/src/celeste/modalities/images/providers/google/imagen.py @@ -58,7 +58,7 @@ def _parse_content( if len(images) == 1: return images[0] - return images if images else ImageArtifact() + return images def _transform_output( self, From 8f20ff5831bc1535cdd232e7b8bdfa8064f5fb0f Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Fri, 27 Feb 2026 12:06:18 +0100 Subject: [PATCH 3/3] fix: use dict.get pattern for audio MIME type mapping - Google Cloud TTS: replace try/except + AudioMimeType(value) with _mime_map dict.get, consistent with all other audio providers - OpenAI: remove "wav"/"pcm" from _mime_map since map() cannot send these formats to the API --- src/celeste/providers/google/cloud_tts/parameters.py | 9 ++++++++- src/celeste/providers/openai/audio/parameters.py | 2 -- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/celeste/providers/google/cloud_tts/parameters.py b/src/celeste/providers/google/cloud_tts/parameters.py index 8e74495..187082c 100644 --- a/src/celeste/providers/google/cloud_tts/parameters.py +++ b/src/celeste/providers/google/cloud_tts/parameters.py @@ -98,11 +98,18 @@ def map( request.setdefault("audioConfig", {})["audioEncoding"] = encoding return request + _mime_map: ClassVar[dict[str, AudioMimeType]] = { + AudioMimeType.MP3: AudioMimeType.MP3, + AudioMimeType.WAV: AudioMimeType.WAV, + AudioMimeType.OGG: AudioMimeType.OGG, + AudioMimeType.PCM: AudioMimeType.PCM, + } + def parse_output(self, content: AudioContent, value: object | None) -> AudioContent: """Apply output_format → MIME type mapping to parsed content.""" if not isinstance(content, AudioArtifact): return content - mime_type = AudioMimeType(value) if value else AudioMimeType.MP3 + mime_type = self._mime_map.get(str(value) if value else "", AudioMimeType.MP3) return AudioArtifact(data=content.data, mime_type=mime_type) diff --git a/src/celeste/providers/openai/audio/parameters.py b/src/celeste/providers/openai/audio/parameters.py index 3637f2a..bbe9f5d 100644 --- a/src/celeste/providers/openai/audio/parameters.py +++ b/src/celeste/providers/openai/audio/parameters.py @@ -29,8 +29,6 @@ class ResponseFormatMapper(ParameterMapper[AudioContent]): "opus": AudioMimeType.OGG, "aac": AudioMimeType.AAC, "flac": AudioMimeType.FLAC, - "wav": AudioMimeType.WAV, - "pcm": AudioMimeType.WAV, } def map(