diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index d3b9bedc..a620cb63 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -38,7 +38,7 @@ jobs: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' - claude_args: '--model claude-opus-4-6' + claude_args: '--model claude-sonnet-4-6' prompt: '/code-review:code-review --comment ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/src/celeste/client.py b/src/celeste/client.py index 3cb14434..095e4123 100644 --- a/src/celeste/client.py +++ b/src/celeste/client.py @@ -44,6 +44,7 @@ class OpenAITextClient(OpenAIResponsesMixin, TextClient): model: Model auth: Authentication provider: Provider + _content_fields: ClassVar[set[str]] = set() @property @abstractmethod @@ -302,6 +303,10 @@ def _validate_artifacts( def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: """Build metadata dictionary from response data.""" + if self._content_fields: + response_data = { + k: v for k, v in response_data.items() if k not in self._content_fields + } return { "model": self.model.id, "provider": self.provider, diff --git a/src/celeste/protocols/chatcompletions/client.py b/src/celeste/protocols/chatcompletions/client.py index c75e803d..71871155 100644 --- a/src/celeste/protocols/chatcompletions/client.py +++ b/src/celeste/protocols/chatcompletions/client.py @@ -23,7 +23,7 @@ class ChatCompletionsClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract choices array from response - _parse_finish_reason() - Extract finish reason from response - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Providers override ClassVars and hook methods: - _default_base_url: ClassVar[str] - Provider's API base URL @@ -39,6 +39,7 @@ class DeepSeekChatClient(ChatCompletionsClient): _default_base_url: ClassVar[str] = config.DEFAULT_BASE_URL _default_endpoint: ClassVar[str] = config.ChatCompletionsEndpoint.CREATE_CHAT + _content_fields: ClassVar[set[str]] = {"choices"} def _build_url(self, endpoint: str, streaming: bool = False) -> str: """Build full URL for request. @@ -150,13 +151,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: reason = choices[0].get("finish_reason") return FinishReason(reason=reason) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"choices"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["ChatCompletionsClient"] diff --git a/src/celeste/protocols/openresponses/client.py b/src/celeste/protocols/openresponses/client.py index 96e1854b..a0e29dd2 100644 --- a/src/celeste/protocols/openresponses/client.py +++ b/src/celeste/protocols/openresponses/client.py @@ -23,7 +23,7 @@ class OpenResponsesClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract output array from response - _parse_finish_reason() - Extract finish reason from response - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Providers override ClassVars and hook methods: - _default_base_url: ClassVar[str] - Provider's API base URL @@ -39,6 +39,7 @@ class OpenAIResponsesClient(OpenResponsesClient): _default_base_url: ClassVar[str] = config.DEFAULT_BASE_URL _default_endpoint: ClassVar[str] = config.OpenResponsesEndpoint.CREATE_RESPONSE + _content_fields: ClassVar[set[str]] = {"output"} def _build_url(self, endpoint: str, streaming: bool = False) -> str: """Build full URL for request. @@ -152,13 +153,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: return FinishReason(reason="completed") return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"output"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["OpenResponsesClient"] diff --git a/src/celeste/providers/anthropic/messages/client.py b/src/celeste/providers/anthropic/messages/client.py index 4253710e..fcb7a886 100644 --- a/src/celeste/providers/anthropic/messages/client.py +++ b/src/celeste/providers/anthropic/messages/client.py @@ -1,7 +1,7 @@ """Anthropic Messages API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -21,7 +21,7 @@ class AnthropicMessagesClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract content array from response - _parse_finish_reason() - Extract finish reason from response - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Auth-based endpoint selection: - GoogleADC auth -> Vertex AI endpoints (Claude on Google Cloud) @@ -37,6 +37,8 @@ def _parse_content(self, response_data, **parameters): return "" """ + _content_fields: ClassVar[set[str]] = {"content"} + def _get_vertex_endpoint( self, anthropic_endpoint: str, streaming: bool = False ) -> str: @@ -185,13 +187,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: stop_reason = response_data.get("stop_reason") return FinishReason(reason=stop_reason) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"content"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["AnthropicMessagesClient"] diff --git a/src/celeste/providers/bfl/images/client.py b/src/celeste/providers/bfl/images/client.py index eae6660a..3ee66c30 100644 --- a/src/celeste/providers/bfl/images/client.py +++ b/src/celeste/providers/bfl/images/client.py @@ -3,7 +3,7 @@ import asyncio import time from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -33,6 +33,8 @@ def _parse_content(self, response_data, **parameters): # Extract image from result["sample"]... """ + _content_fields: ClassVar[set[str]] = {"result"} + async def _make_request( self, request_body: dict[str, Any], @@ -152,13 +154,5 @@ def _parse_content(self, response_data: dict[str, Any]) -> Any: raise ValueError(msg) return result - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"result"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["BFLImagesClient"] diff --git a/src/celeste/providers/byteplus/images/client.py b/src/celeste/providers/byteplus/images/client.py index 944ce91a..06223a4a 100644 --- a/src/celeste/providers/byteplus/images/client.py +++ b/src/celeste/providers/byteplus/images/client.py @@ -1,7 +1,7 @@ """BytePlus Images API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -20,7 +20,7 @@ class BytePlusImagesClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract images/data array from response - _parse_finish_reason() - Extract finish reason from response - - _build_metadata() - Filter content fields and extract seed + - _content_fields: ClassVar - Content field names to exclude from metadata Usage: class BytePlusImageGenerationClient(BytePlusImagesClient, ImageGenerationClient): @@ -29,6 +29,8 @@ def _parse_content(self, response_data, **parameters): # Extract image from images[0] or data[0]... """ + _content_fields: ClassVar[set[str]] = {"images", "data"} + def _build_request( self, inputs: Any, @@ -135,18 +137,10 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: def _build_metadata(self, response_data: dict[str, Any]) -> Any: """Build metadata dictionary, extracting seed if present.""" - # Filter content fields before calling super - content_fields = {"images", "data"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - metadata = super()._build_metadata(filtered_data) - - # Add provider-specific parsed fields + metadata = super()._build_metadata(response_data) seed = response_data.get("seed") if seed is not None: metadata["seed"] = seed - return metadata diff --git a/src/celeste/providers/byteplus/videos/client.py b/src/celeste/providers/byteplus/videos/client.py index b4ea1a95..fe305e8c 100644 --- a/src/celeste/providers/byteplus/videos/client.py +++ b/src/celeste/providers/byteplus/videos/client.py @@ -4,7 +4,7 @@ import logging import time from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -35,6 +35,8 @@ def _parse_content(self, response_data, **parameters): # Extract video from content["video_url"]... """ + _content_fields: ClassVar[set[str]] = {"content"} + def _build_request( self, inputs: Any, @@ -167,13 +169,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """BytePlus provides status but not structured finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"content"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["BytePlusVideosClient"] diff --git a/src/celeste/providers/cohere/chat/client.py b/src/celeste/providers/cohere/chat/client.py index cba02b17..34780e43 100644 --- a/src/celeste/providers/cohere/chat/client.py +++ b/src/celeste/providers/cohere/chat/client.py @@ -1,7 +1,7 @@ """Cohere Chat API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -20,7 +20,7 @@ class CohereChatClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract content array from response message - _parse_finish_reason() - Extract finish reason from response - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Usage: class CohereTextGenerationClient(CohereChatClient, TextGenerationClient): @@ -30,6 +30,8 @@ def _parse_content(self, response_data, **parameters): return self._transform_output(text, **parameters) """ + _content_fields: ClassVar[set[str]] = {"message"} + def _build_request( self, inputs: Any, @@ -132,13 +134,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: reason = response_data.get("finish_reason") return FinishReason(reason=reason) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"message"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["CohereChatClient"] diff --git a/src/celeste/providers/elevenlabs/text_to_speech/client.py b/src/celeste/providers/elevenlabs/text_to_speech/client.py index e1109bcd..fa8aa8c8 100644 --- a/src/celeste/providers/elevenlabs/text_to_speech/client.py +++ b/src/celeste/providers/elevenlabs/text_to_speech/client.py @@ -165,10 +165,6 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """ElevenLabs TTS doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - return super()._build_metadata(response_data) - def _map_output_format_to_mime_type( self, output_format: str | None ) -> AudioMimeType: diff --git a/src/celeste/providers/google/cloud_tts/client.py b/src/celeste/providers/google/cloud_tts/client.py index 8d1aaa58..b1d3d8e0 100644 --- a/src/celeste/providers/google/cloud_tts/client.py +++ b/src/celeste/providers/google/cloud_tts/client.py @@ -1,7 +1,7 @@ """Google Cloud TTS API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.exceptions import StreamingNotSupportedError @@ -30,6 +30,8 @@ def _parse_content(self, response_data, **params): return AudioArtifact(data=audio_bytes, mime_type=..., ...) """ + _content_fields: ClassVar[set[str]] = {"audioContent"} + def _make_stream_request( self, request_body: dict[str, Any], @@ -100,13 +102,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """Cloud TTS API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"audioContent"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["GoogleCloudTTSClient"] diff --git a/src/celeste/providers/google/embeddings/client.py b/src/celeste/providers/google/embeddings/client.py index acc5a619..0181f0dd 100644 --- a/src/celeste/providers/google/embeddings/client.py +++ b/src/celeste/providers/google/embeddings/client.py @@ -1,7 +1,7 @@ """Google Embeddings API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.exceptions import StreamingNotSupportedError @@ -29,6 +29,8 @@ def _parse_content(self, response_data, **params): return super()._parse_content(response_data) # No transformation needed """ + _content_fields: ClassVar[set[str]] = {"embedding", "embeddings"} + def _make_stream_request( self, request_body: dict[str, Any], @@ -141,13 +143,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """Embeddings API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata, filtering out embedding content.""" - content_fields = {"embedding", "embeddings"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["GoogleEmbeddingsClient"] diff --git a/src/celeste/providers/google/generate_content/client.py b/src/celeste/providers/google/generate_content/client.py index 61d6e58d..5f7badda 100644 --- a/src/celeste/providers/google/generate_content/client.py +++ b/src/celeste/providers/google/generate_content/client.py @@ -1,7 +1,7 @@ """Google GenerateContent API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -21,7 +21,7 @@ class GoogleGenerateContentClient(APIMixin): - _parse_usage() - Extract usage dict from usageMetadata - _parse_content() - Extract parts array from first candidate - _parse_finish_reason() - Extract finish reason string from candidates - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Auth-based endpoint selection: - GoogleADC auth -> Vertex AI endpoints @@ -37,6 +37,8 @@ def _parse_content(self, response_data, **parameters): return self._transform_output(text, **parameters) """ + _content_fields: ClassVar[set[str]] = {"candidates"} + def _get_vertex_endpoint(self, gemini_endpoint: str) -> str: """Map Gemini endpoint to Vertex AI endpoint.""" mapping: dict[str, str] = { @@ -145,13 +147,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: reason = candidates[0].get("finishReason") return FinishReason(reason=reason) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"candidates"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["GoogleGenerateContentClient"] diff --git a/src/celeste/providers/google/imagen/client.py b/src/celeste/providers/google/imagen/client.py index 778b285f..a6f443d9 100644 --- a/src/celeste/providers/google/imagen/client.py +++ b/src/celeste/providers/google/imagen/client.py @@ -1,7 +1,7 @@ """Google Imagen API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -21,7 +21,7 @@ class GoogleImagenClient(APIMixin): - _parse_usage() - Returns empty dict (Imagen doesn't provide usage) - _parse_content() - Extract predictions[] array - _parse_finish_reason() - Returns None (Imagen doesn't provide finish reasons) - - _build_metadata() - Filter out predictions content + - _content_fields: ClassVar - Content field names to exclude from metadata Auth-based endpoint selection: - GoogleADC auth -> Vertex AI endpoints @@ -36,6 +36,8 @@ def _parse_content(self, response_data, **parameters): # Extract image from predictions[0]... """ + _content_fields: ClassVar[set[str]] = {"predictions"} + def _get_vertex_endpoint(self, gemini_endpoint: str) -> str: """Map Gemini Imagen endpoint to Vertex AI endpoint.""" mapping: dict[str, str] = { @@ -122,16 +124,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """Imagen API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata, filtering out predictions content. - - Keeps metadata like raiFilteredReason, safety attributes. - """ - content_fields = {"predictions"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["GoogleImagenClient"] diff --git a/src/celeste/providers/google/interactions/client.py b/src/celeste/providers/google/interactions/client.py index 4de0b3c4..e0aa1ab8 100644 --- a/src/celeste/providers/google/interactions/client.py +++ b/src/celeste/providers/google/interactions/client.py @@ -1,7 +1,7 @@ """Google Interactions API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar import httpx @@ -23,7 +23,7 @@ class GoogleInteractionsClient(APIMixin): - _parse_usage() - Extract usage dict from usage metadata - _parse_content() - Extract outputs array from response - _parse_finish_reason() - Extract finish reason string from response - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Capability clients extend parsing methods via super() to wrap/transform results. @@ -37,6 +37,8 @@ def _parse_content(self, response_data, **parameters): return self._transform_output(text, **parameters) """ + _content_fields: ClassVar[set[str]] = {"outputs"} + def _build_request( self, inputs: Any, @@ -160,13 +162,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: reason = outputs[-1].get("finish_reason") return FinishReason(reason=reason) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"outputs"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["GoogleInteractionsClient"] diff --git a/src/celeste/providers/google/veo/client.py b/src/celeste/providers/google/veo/client.py index 9348c8f0..5de40eb1 100644 --- a/src/celeste/providers/google/veo/client.py +++ b/src/celeste/providers/google/veo/client.py @@ -3,7 +3,7 @@ import asyncio import logging from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.exceptions import StreamingNotSupportedError @@ -35,6 +35,8 @@ async def download_content(self, artifact: VideoArtifact) -> VideoArtifact: return VideoArtifact(data=video_bytes, mime_type=VideoMimeType.MP4, ...) """ + _content_fields: ClassVar[set[str]] = {"response"} + def _get_vertex_endpoint(self, gemini_endpoint: str) -> str: """Map Gemini Veo endpoint to Vertex AI endpoint.""" mapping: dict[str, str] = { @@ -199,14 +201,6 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """Veo API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"response"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - async def download_content(self, url: str) -> bytes: """Download video content from GCS URL. diff --git a/src/celeste/providers/gradium/text_to_speech/client.py b/src/celeste/providers/gradium/text_to_speech/client.py index a7e4812a..3481b4f5 100644 --- a/src/celeste/providers/gradium/text_to_speech/client.py +++ b/src/celeste/providers/gradium/text_to_speech/client.py @@ -163,10 +163,6 @@ async def _make_request( "output_format": output_format, } - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary from response data.""" - return super()._build_metadata(response_data) - def _map_output_format_to_mime_type( self, output_format: str | None, diff --git a/src/celeste/providers/ollama/generate/client.py b/src/celeste/providers/ollama/generate/client.py index 8b33935d..f41becab 100644 --- a/src/celeste/providers/ollama/generate/client.py +++ b/src/celeste/providers/ollama/generate/client.py @@ -2,7 +2,7 @@ import json from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -21,9 +21,11 @@ class OllamaGenerateClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract image from response - _parse_finish_reason() - Check done field - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata """ + _content_fields: ClassVar[set[str]] = {"image", "response"} + def _build_request( self, inputs: Any, @@ -134,13 +136,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: done = response_data.get("done", False) return FinishReason(reason="completed" if done else None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"image", "response"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["OllamaGenerateClient"] diff --git a/src/celeste/providers/openai/audio/client.py b/src/celeste/providers/openai/audio/client.py index ccce48b8..13b199d8 100644 --- a/src/celeste/providers/openai/audio/client.py +++ b/src/celeste/providers/openai/audio/client.py @@ -112,10 +112,6 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """OpenAI Audio API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - return super()._build_metadata(response_data) - def _map_response_format_to_mime_type( self, response_format: str | None ) -> AudioMimeType: diff --git a/src/celeste/providers/openai/images/client.py b/src/celeste/providers/openai/images/client.py index 861eed7e..2cac5492 100644 --- a/src/celeste/providers/openai/images/client.py +++ b/src/celeste/providers/openai/images/client.py @@ -6,7 +6,7 @@ """ from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -26,9 +26,11 @@ class OpenAIImagesClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract data array from response - _parse_finish_reason() - Returns None (Images API doesn't provide finish reasons) - - _build_metadata() - Filter content fields and include revised_prompt + - _content_fields: ClassVar - Content field names to exclude from metadata """ + _content_fields: ClassVar[set[str]] = {"data"} + def _build_request( self, inputs: Any, @@ -188,16 +190,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """Images API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> Any: - """Build metadata dictionary, including revised_prompt if present.""" - content_fields = {"data"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - - metadata = super()._build_metadata(filtered_data) - - return metadata - __all__ = ["OpenAIImagesClient"] diff --git a/src/celeste/providers/openai/videos/client.py b/src/celeste/providers/openai/videos/client.py index 17f95f50..db7d8380 100644 --- a/src/celeste/providers/openai/videos/client.py +++ b/src/celeste/providers/openai/videos/client.py @@ -8,7 +8,7 @@ import base64 import logging from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -28,7 +28,7 @@ class OpenAIVideosClient(APIMixin): - _make_request() - HTTP POST with async polling pattern - _parse_usage() - Returns billing units from response - _parse_finish_reason() - Returns None (Videos API doesn't provide finish reasons) - - _build_metadata() - Filter content fields, include video metadata + - _content_fields: ClassVar - Content field names to exclude from metadata The Videos API uses async polling: 1. POST to create video job at /v1/videos @@ -41,6 +41,8 @@ async def _prepare_multipart_request(self, request_body): # Handle input_reference image uploads... """ + _content_fields: ClassVar[set[str]] = {"video_data"} + def _build_request( self, inputs: Any, @@ -204,16 +206,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """Videos API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> Any: - """Build metadata dictionary, including video-specific fields.""" - content_fields = {"video_data"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - - metadata = super()._build_metadata(filtered_data) - - return metadata - __all__ = ["OpenAIVideosClient"] diff --git a/src/celeste/providers/xai/images/client.py b/src/celeste/providers/xai/images/client.py index 9a314059..e95ae829 100644 --- a/src/celeste/providers/xai/images/client.py +++ b/src/celeste/providers/xai/images/client.py @@ -1,7 +1,7 @@ """xAI Images API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -21,12 +21,14 @@ class XAIImagesClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract data array from response - _parse_finish_reason() - Returns None (no finish reasons for images) - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Modality clients pass endpoint parameter to route operations: await self._predict(inputs, endpoint=config.XAIImagesEndpoint.CREATE_IMAGE, **parameters) """ + _content_fields: ClassVar[set[str]] = {"data"} + def _build_request( self, inputs: Any, @@ -104,13 +106,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """xAI Images API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"data"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["XAIImagesClient"] diff --git a/src/celeste/providers/xai/videos/client.py b/src/celeste/providers/xai/videos/client.py index f6dcbdbf..d76cc821 100644 --- a/src/celeste/providers/xai/videos/client.py +++ b/src/celeste/providers/xai/videos/client.py @@ -2,7 +2,7 @@ import asyncio from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar from celeste.client import APIMixin from celeste.core import UsageField @@ -21,7 +21,7 @@ class XAIVideosClient(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract video URL from response - _parse_finish_reason() - Returns None (Videos API doesn't provide finish reasons) - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata The Videos API uses async polling: 1. POST to /v1/videos/generations returns request_id @@ -29,6 +29,8 @@ class XAIVideosClient(APIMixin): 3. Response contains video URL directly """ + _content_fields: ClassVar[set[str]] = {"video"} + def _build_request( self, inputs: Any, @@ -147,13 +149,5 @@ def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: """Videos API doesn't provide finish reasons.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"video"} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["XAIVideosClient"] diff --git a/templates/protocols/{protocol_slug}/client.py.template b/templates/protocols/{protocol_slug}/client.py.template index 0c72fbc0..1d1dea8b 100644 --- a/templates/protocols/{protocol_slug}/client.py.template +++ b/templates/protocols/{protocol_slug}/client.py.template @@ -23,7 +23,7 @@ class {Protocol}Client(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract content from response - _parse_finish_reason() - Extract finish reason from response - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Providers override ClassVars and hook methods: - _default_base_url: ClassVar[str] - Provider's API base URL @@ -39,6 +39,7 @@ class {Protocol}Client(APIMixin): _default_base_url: ClassVar[str] = config.DEFAULT_BASE_URL _default_endpoint: ClassVar[str] = "" # Set to config.{Protocol}Endpoint.CREATE_... + _content_fields: ClassVar[set[str]] = {"..."} def _build_url(self, endpoint: str, streaming: bool = False) -> str: """Build full URL for request. @@ -145,13 +146,5 @@ class {Protocol}Client(APIMixin): """Extract finish reason from {Protocol} API response.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"..."} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["{Protocol}Client"] diff --git a/templates/providers/{provider_slug}/{api_slug}/client.py.template b/templates/providers/{provider_slug}/{api_slug}/client.py.template index b828b76b..093695dd 100644 --- a/templates/providers/{provider_slug}/{api_slug}/client.py.template +++ b/templates/providers/{provider_slug}/{api_slug}/client.py.template @@ -1,7 +1,7 @@ """{Provider} {Api} API client mixin.""" from collections.abc import AsyncIterator -from typing import Any +from typing import Any, ClassVar import httpx @@ -27,7 +27,7 @@ class {Provider}{Api}Client(APIMixin): - _parse_usage() - Extract usage dict from response - _parse_content() - Extract content from response - _parse_finish_reason() - Extract finish reason (if provided) - - _build_metadata() - Filter content fields + - _content_fields: ClassVar - Content field names to exclude from metadata Auth-based endpoint selection (optional, for Google providers): - GoogleADC auth -> Vertex AI endpoints (via _build_url + _get_vertex_endpoint) @@ -37,6 +37,8 @@ class {Provider}{Api}Client(APIMixin): await self._predict(inputs, endpoint=config.{Provider}{Api}Endpoint.CREATE_..., **parameters) """ + _content_fields: ClassVar[set[str]] = {"..."} + # Optional: Vertex AI endpoint routing. Add these two methods when the provider # supports Vertex AI. They replace the inline f"{config.BASE_URL}{endpoint}" URL # construction in _make_request() / _make_stream_request() with self._build_url(endpoint). @@ -177,13 +179,5 @@ class {Provider}{Api}Client(APIMixin): """Extract finish reason from {Provider} {Api} API response.""" return FinishReason(reason=None) - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary, filtering out content fields.""" - content_fields = {"..."} - filtered_data = { - k: v for k, v in response_data.items() if k not in content_fields - } - return super()._build_metadata(filtered_data) - __all__ = ["{Provider}{Api}Client"] diff --git a/tests/unit_tests/test_provider_api_templates.py b/tests/unit_tests/test_provider_api_templates.py index a7380abc..cc675c3e 100644 --- a/tests/unit_tests/test_provider_api_templates.py +++ b/tests/unit_tests/test_provider_api_templates.py @@ -19,7 +19,6 @@ class TemplateExpectations: "_parse_usage", "_parse_content", "_parse_finish_reason", - "_build_metadata", }