From 2cf1dc4f46a09c6fbc795d49ac56f2392c459d61 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 15 Dec 2025 18:31:50 +0100 Subject: [PATCH 1/4] refactor(capabilities): rename bytedance provider directories to byteplus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename provider directories from 'bytedance' to 'byteplus' in both image-generation and video-generation capabilities to align with updated naming convention. ## Changes - Image generation: Rename bytedance/ → byteplus/ (7 files) - Video generation: Rename bytedance/ → byteplus/ (7 files) - Includes: README, __init__, client, config, models, parameters, streaming This is a directory rename only. Import statements and enum references will be updated in subsequent commits. --- .../providers/{bytedance => byteplus}/README.md | 0 .../providers/{bytedance => byteplus}/__init__.py | 0 .../providers/{bytedance => byteplus}/client.py | 0 .../providers/{bytedance => byteplus}/config.py | 0 .../providers/{bytedance => byteplus}/models.py | 0 .../providers/{bytedance => byteplus}/parameters.py | 0 .../providers/{bytedance => byteplus}/streaming.py | 0 .../providers/{bytedance => byteplus}/README.md | 0 .../providers/{bytedance => byteplus}/__init__.py | 0 .../providers/{bytedance => byteplus}/client.py | 0 .../providers/{bytedance => byteplus}/config.py | 0 .../providers/{bytedance => byteplus}/models.py | 0 .../providers/{bytedance => byteplus}/parameters.py | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename packages/capabilities/image-generation/src/celeste_image_generation/providers/{bytedance => byteplus}/README.md (100%) rename packages/capabilities/image-generation/src/celeste_image_generation/providers/{bytedance => byteplus}/__init__.py (100%) rename packages/capabilities/image-generation/src/celeste_image_generation/providers/{bytedance => byteplus}/client.py (100%) rename packages/capabilities/image-generation/src/celeste_image_generation/providers/{bytedance => byteplus}/config.py (100%) rename packages/capabilities/image-generation/src/celeste_image_generation/providers/{bytedance => byteplus}/models.py (100%) rename packages/capabilities/image-generation/src/celeste_image_generation/providers/{bytedance => byteplus}/parameters.py (100%) rename packages/capabilities/image-generation/src/celeste_image_generation/providers/{bytedance => byteplus}/streaming.py (100%) rename packages/capabilities/video-generation/src/celeste_video_generation/providers/{bytedance => byteplus}/README.md (100%) rename packages/capabilities/video-generation/src/celeste_video_generation/providers/{bytedance => byteplus}/__init__.py (100%) rename packages/capabilities/video-generation/src/celeste_video_generation/providers/{bytedance => byteplus}/client.py (100%) rename packages/capabilities/video-generation/src/celeste_video_generation/providers/{bytedance => byteplus}/config.py (100%) rename packages/capabilities/video-generation/src/celeste_video_generation/providers/{bytedance => byteplus}/models.py (100%) rename packages/capabilities/video-generation/src/celeste_video_generation/providers/{bytedance => byteplus}/parameters.py (100%) diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/README.md b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/README.md similarity index 100% rename from packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/README.md rename to packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/README.md diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/__init__.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/__init__.py similarity index 100% rename from packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/__init__.py rename to packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/__init__.py diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/client.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py similarity index 100% rename from packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/client.py rename to packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/config.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py similarity index 100% rename from packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/config.py rename to packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/models.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/models.py similarity index 100% rename from packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/models.py rename to packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/models.py diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/parameters.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py similarity index 100% rename from packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/parameters.py rename to packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/streaming.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/streaming.py similarity index 100% rename from packages/capabilities/image-generation/src/celeste_image_generation/providers/bytedance/streaming.py rename to packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/streaming.py diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/README.md b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/README.md similarity index 100% rename from packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/README.md rename to packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/README.md diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/__init__.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/__init__.py similarity index 100% rename from packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/__init__.py rename to packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/__init__.py diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/client.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py similarity index 100% rename from packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/client.py rename to packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/config.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py similarity index 100% rename from packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/config.py rename to packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/models.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/models.py similarity index 100% rename from packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/models.py rename to packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/models.py diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/parameters.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py similarity index 100% rename from packages/capabilities/video-generation/src/celeste_video_generation/providers/bytedance/parameters.py rename to packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py From 1ea62ac12fcdf3906bac932a1d9c3dd4f0221a64 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 15 Dec 2025 18:35:13 +0100 Subject: [PATCH 2/4] refactor: rename BYTEDANCE to BYTEPLUS across codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Provider enum, credentials, class names, variables, docstrings, and all references from ByteDance/BYTEDANCE/bytedance to BytePlus/BYTEPLUS/byteplus. ## Core Changes - Provider enum: BYTEDANCE → BYTEPLUS in core.py - Credentials: bytedance_api_key → byteplus_api_key, BYTEDANCE_API_KEY → BYTEPLUS_API_KEY - Package name: celeste_bytedance → celeste_byteplus ## Capability Changes - Class names: ByteDance* → BytePlus* (clients, streams) - Variable names: BYTEDANCE_* → BYTEPLUS_* (MODELS, PARAMETER_MAPPERS, etc.) - Docstrings/comments: ByteDance → BytePlus - Provider references: Provider.BYTEDANCE → Provider.BYTEPLUS - README files: Update environment variable names and provider names - Tests: Update Provider.BYTEDANCE → Provider.BYTEPLUS - Keywords: bytedance → byteplus in pyproject.toml ## Files Updated - Core: core.py, credentials.py - Image generation: client, models, parameters, streaming, config, __init__, README - Video generation: client, models, parameters, config, __init__, README - Tests: integration tests for both capabilities - Provider package: byteplus provider package files --- .../image-generation/pyproject.toml | 2 +- .../src/celeste_image_generation/models.py | 6 +- .../providers/__init__.py | 6 +- .../providers/byteplus/README.md | 6 +- .../providers/byteplus/__init__.py | 6 +- .../providers/byteplus/client.py | 26 +-- .../providers/byteplus/config.py | 2 +- .../providers/byteplus/models.py | 6 +- .../providers/byteplus/parameters.py | 10 +- .../providers/byteplus/streaming.py | 8 +- .../test_image_generation/test_generate.py | 2 +- .../src/celeste_video_generation/models.py | 6 +- .../providers/__init__.py | 6 +- .../providers/byteplus/README.md | 6 +- .../providers/byteplus/__init__.py | 10 +- .../providers/byteplus/client.py | 34 ++-- .../providers/byteplus/config.py | 2 +- .../providers/byteplus/models.py | 32 ++-- .../providers/byteplus/parameters.py | 10 +- .../test_video_generation/test_generate.py | 2 +- packages/providers/byteplus/pyproject.toml | 17 ++ .../byteplus/src/celeste_byteplus/__init__.py | 3 + .../src/celeste_byteplus/images/__init__.py | 1 + .../src/celeste_byteplus/images/client.py | 120 ++++++++++++ .../src/celeste_byteplus/images/config.py | 12 ++ .../src/celeste_byteplus/images/parameters.py | 45 +++++ .../byteplus/src/celeste_byteplus/py.typed | 0 .../src/celeste_byteplus/videos/__init__.py | 1 + .../src/celeste_byteplus/videos/client.py | 132 +++++++++++++ .../src/celeste_byteplus/videos/config.py | 22 +++ .../src/celeste_byteplus/videos/parameters.py | 179 ++++++++++++++++++ src/celeste/core.py | 2 +- src/celeste/credentials.py | 6 +- 33 files changed, 630 insertions(+), 98 deletions(-) create mode 100644 packages/providers/byteplus/pyproject.toml create mode 100644 packages/providers/byteplus/src/celeste_byteplus/__init__.py create mode 100644 packages/providers/byteplus/src/celeste_byteplus/images/__init__.py create mode 100644 packages/providers/byteplus/src/celeste_byteplus/images/client.py create mode 100644 packages/providers/byteplus/src/celeste_byteplus/images/config.py create mode 100644 packages/providers/byteplus/src/celeste_byteplus/images/parameters.py create mode 100644 packages/providers/byteplus/src/celeste_byteplus/py.typed create mode 100644 packages/providers/byteplus/src/celeste_byteplus/videos/__init__.py create mode 100644 packages/providers/byteplus/src/celeste_byteplus/videos/client.py create mode 100644 packages/providers/byteplus/src/celeste_byteplus/videos/config.py create mode 100644 packages/providers/byteplus/src/celeste_byteplus/videos/parameters.py diff --git a/packages/capabilities/image-generation/pyproject.toml b/packages/capabilities/image-generation/pyproject.toml index dfb0c5e8..2c152148 100644 --- a/packages/capabilities/image-generation/pyproject.toml +++ b/packages/capabilities/image-generation/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", "Typing :: Typed", ] -keywords = ["ai", "image-generation", "dall-e", "imagen", "openai", "google", "bytedance"] +keywords = ["ai", "image-generation", "dall-e", "imagen", "openai", "google", "byteplus"] [project.urls] Homepage = "https://withceleste.ai" diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/models.py b/packages/capabilities/image-generation/src/celeste_image_generation/models.py index f25292e5..ba020565 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/models.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/models.py @@ -2,15 +2,15 @@ from celeste import Model from celeste_image_generation.providers.bfl.models import MODELS as BFL_MODELS -from celeste_image_generation.providers.bytedance.models import ( - MODELS as BYTEDANCE_MODELS, +from celeste_image_generation.providers.byteplus.models import ( + MODELS as BYTEPLUS_MODELS, ) from celeste_image_generation.providers.google.models import MODELS as GOOGLE_MODELS from celeste_image_generation.providers.openai.models import MODELS as OPENAI_MODELS MODELS: list[Model] = [ *BFL_MODELS, - *BYTEDANCE_MODELS, + *BYTEPLUS_MODELS, *GOOGLE_MODELS, *OPENAI_MODELS, ] diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/__init__.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/__init__.py index fe9a8499..7c6549d7 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/__init__.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/__init__.py @@ -11,8 +11,8 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]: from celeste_image_generation.providers.bfl.client import ( BFLImageGenerationClient, ) - from celeste_image_generation.providers.bytedance.client import ( - ByteDanceImageGenerationClient, + from celeste_image_generation.providers.byteplus.client import ( + BytePlusImageGenerationClient, ) from celeste_image_generation.providers.google.client import ( GoogleImageGenerationClient, @@ -23,7 +23,7 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]: return [ (Provider.BFL, BFLImageGenerationClient), - (Provider.BYTEDANCE, ByteDanceImageGenerationClient), + (Provider.BYTEPLUS, BytePlusImageGenerationClient), (Provider.GOOGLE, GoogleImageGenerationClient), (Provider.OPENAI, OpenAIImageGenerationClient), ] diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/README.md b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/README.md index ffbcc751..211d1323 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/README.md +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/README.md @@ -1,13 +1,13 @@ -# ByteDance Image Generation Provider +# BytePlus Image Generation Provider ## Credentials -**Environment variable:** `BYTEDANCE_API_KEY` +**Environment variable:** `BYTEPLUS_API_KEY` **Setup:** 1. Register at [console.byteplus.com](https://console.byteplus.com) 2. Activate model in ModelArk section 3. Generate API key with image generation permissions -4. Set environment variable: `export BYTEDANCE_API_KEY="your-key"` +4. Set environment variable: `export BYTEPLUS_API_KEY="your-key"` **Note:** Models must be activated in BytePlus console before use. If you get a 404 error, activate the model or use an Endpoint ID (`ep-xxx`) instead of Model ID. diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/__init__.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/__init__.py index 623b1947..213be409 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/__init__.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/__init__.py @@ -1,6 +1,6 @@ -"""ByteDance provider for image generation.""" +"""BytePlus provider for image generation.""" -from .client import ByteDanceImageGenerationClient +from .client import BytePlusImageGenerationClient from .models import MODELS -__all__ = ["MODELS", "ByteDanceImageGenerationClient"] +__all__ = ["MODELS", "BytePlusImageGenerationClient"] diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py index f4cc752e..20c9af95 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py @@ -1,4 +1,4 @@ -"""ByteDance client implementation for image generation.""" +"""BytePlus client implementation for image generation.""" import base64 from collections.abc import AsyncIterator @@ -19,19 +19,19 @@ from celeste_image_generation.parameters import ImageGenerationParameters from . import config -from .parameters import BYTEDANCE_PARAMETER_MAPPERS -from .streaming import ByteDanceImageGenerationStream +from .parameters import BYTEPLUS_PARAMETER_MAPPERS +from .streaming import BytePlusImageGenerationStream -class ByteDanceImageGenerationClient(ImageGenerationClient): - """ByteDance client for image generation.""" +class BytePlusImageGenerationClient(ImageGenerationClient): + """BytePlus client for image generation.""" @classmethod def parameter_mappers(cls) -> list[ParameterMapper]: - return BYTEDANCE_PARAMETER_MAPPERS + return BYTEPLUS_PARAMETER_MAPPERS def _init_request(self, inputs: ImageGenerationInput) -> dict[str, Any]: - """Initialize request from ByteDance API structure.""" + """Initialize request from BytePlus API structure.""" return { "model": self.model.id, "prompt": inputs.prompt, @@ -75,7 +75,7 @@ def _parse_content( mime_type=ImageMimeType.PNG, ) - msg = "No image content found in ByteDance response" + msg = "No image content found in BytePlus response" raise ValidationError(msg) def _parse_finish_reason( @@ -83,7 +83,7 @@ def _parse_finish_reason( ) -> ImageGenerationFinishReason | None: """Parse finish reason from response. - ByteDance doesn't provide finish reasons for image generation. + BytePlus doesn't provide finish reasons for image generation. """ return None @@ -109,7 +109,7 @@ async def _make_request( if parameters.get("aspect_ratio") and parameters.get("quality"): msg = ( "Cannot use both 'aspect_ratio' and 'quality' parameters. " - "ByteDance's 'size' field supports two methods that cannot be combined:\n" + "BytePlus's 'size' field supports two methods that cannot be combined:\n" " • quality: Resolution class ('1K', '2K', '4K')\n" " • aspect_ratio: Exact dimensions (e.g., '2048x2048', '3840x2160')\n" "Use one or the other, not both." @@ -129,9 +129,9 @@ async def _make_request( json_body=request_body, ) - def _stream_class(self) -> type[ByteDanceImageGenerationStream]: + def _stream_class(self) -> type[BytePlusImageGenerationStream]: """Return the Stream class for this client.""" - return ByteDanceImageGenerationStream + return BytePlusImageGenerationStream def _make_stream_request( self, @@ -153,4 +153,4 @@ def _make_stream_request( ) -__all__ = ["ByteDanceImageGenerationClient"] +__all__ = ["BytePlusImageGenerationClient"] diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py index f7a40b82..1bbea40d 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py @@ -1,4 +1,4 @@ -"""ByteDance provider configuration for image generation.""" +"""BytePlus provider configuration for image generation.""" # HTTP Configuration BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3" diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/models.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/models.py index 0cde1a95..71e273c0 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/models.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/models.py @@ -1,4 +1,4 @@ -"""ByteDance models for image generation.""" +"""BytePlus models for image generation.""" from celeste import Model, Provider from celeste.constraints import Bool, Choice @@ -8,7 +8,7 @@ MODELS: list[Model] = [ Model( id="seedream-4-0-250828", - provider=Provider.BYTEDANCE, + provider=Provider.BYTEPLUS, display_name="Seedream 4.0", parameter_constraints={ ImageGenerationParameter.ASPECT_RATIO: Dimensions( @@ -33,7 +33,7 @@ ), Model( id="seedream-4-5-251128", - provider=Provider.BYTEDANCE, + provider=Provider.BYTEPLUS, display_name="Seedream 4.5", parameter_constraints={ ImageGenerationParameter.ASPECT_RATIO: Dimensions( diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py index 310e24ce..b1ecb5c7 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py @@ -1,4 +1,4 @@ -"""ByteDance parameter mappers for image generation.""" +"""BytePlus parameter mappers for image generation.""" from typing import Any @@ -11,7 +11,7 @@ class AspectRatioMapper(ParameterMapper): """Map aspect_ratio to dimension string. Accepts freeform dimension strings (e.g., "2048x2048", "3840x2160") - validated by Dimensions constraint against ByteDance's pixel and aspect ratio bounds. + validated by Dimensions constraint against BytePlus's pixel and aspect ratio bounds. """ name = ImageGenerationParameter.ASPECT_RATIO @@ -51,7 +51,7 @@ def map( ) -> dict[str, Any]: """Transform quality into provider request. - Maps quality levels ("1K", "2K", "4K") to ByteDance's size parameter. + Maps quality levels ("1K", "2K", "4K") to BytePlus's size parameter. Skips if size is already set by aspect_ratio (conflict resolution). """ validated_value = self._validate_value(value, model) @@ -92,10 +92,10 @@ def map( return request -BYTEDANCE_PARAMETER_MAPPERS: list[ParameterMapper] = [ +BYTEPLUS_PARAMETER_MAPPERS: list[ParameterMapper] = [ AspectRatioMapper(), QualityMapper(), WatermarkMapper(), ] -__all__ = ["BYTEDANCE_PARAMETER_MAPPERS"] +__all__ = ["BYTEPLUS_PARAMETER_MAPPERS"] diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/streaming.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/streaming.py index 28caf487..3c06cea3 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/streaming.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/streaming.py @@ -1,4 +1,4 @@ -"""ByteDance streaming for image generation.""" +"""BytePlus streaming for image generation.""" import logging from collections.abc import AsyncIterator @@ -12,8 +12,8 @@ logger = logging.getLogger(__name__) -class ByteDanceImageGenerationStream(ImageGenerationStream): - """ByteDance streaming for image generation.""" +class BytePlusImageGenerationStream(ImageGenerationStream): + """BytePlus streaming for image generation.""" def __init__(self, sse_iterator: AsyncIterator[dict[str, Any]]) -> None: """Initialize stream and track completed event usage.""" @@ -61,4 +61,4 @@ def _parse_usage(self, chunks: list[ImageGenerationChunk]) -> ImageGenerationUsa return ImageGenerationUsage() -__all__ = ["ByteDanceImageGenerationStream"] +__all__ = ["BytePlusImageGenerationStream"] diff --git a/packages/capabilities/image-generation/tests/integration_tests/test_image_generation/test_generate.py b/packages/capabilities/image-generation/tests/integration_tests/test_image_generation/test_generate.py index ba3575c8..72662b14 100644 --- a/packages/capabilities/image-generation/tests/integration_tests/test_image_generation/test_generate.py +++ b/packages/capabilities/image-generation/tests/integration_tests/test_image_generation/test_generate.py @@ -11,7 +11,7 @@ (Provider.BFL, "flux-2-pro", {"aspect_ratio": "1024x1024"}), (Provider.OPENAI, "dall-e-2", {}), (Provider.GOOGLE, "imagen-4.0-fast-generate-001", {"num_images": 1}), - (Provider.BYTEDANCE, "seedream-4-0-250828", {}), + (Provider.BYTEPLUS, "seedream-4-0-250828", {}), ], ) @pytest.mark.integration diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/models.py b/packages/capabilities/video-generation/src/celeste_video_generation/models.py index c3c6a248..07d29ee1 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/models.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/models.py @@ -1,14 +1,14 @@ """Model definitions for video generation.""" from celeste import Model -from celeste_video_generation.providers.bytedance.models import ( - MODELS as BYTEDANCE_MODELS, +from celeste_video_generation.providers.byteplus.models import ( + MODELS as BYTEPLUS_MODELS, ) from celeste_video_generation.providers.google.models import MODELS as GOOGLE_MODELS from celeste_video_generation.providers.openai.models import MODELS as OPENAI_MODELS MODELS: list[Model] = [ - *BYTEDANCE_MODELS, + *BYTEPLUS_MODELS, *GOOGLE_MODELS, *OPENAI_MODELS, ] diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/__init__.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/__init__.py index 84006e15..2339f556 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/__init__.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/__init__.py @@ -8,8 +8,8 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]: """Lazy-load providers.""" # Import clients directly from .client modules to avoid __init__.py imports - from celeste_video_generation.providers.bytedance.client import ( - ByteDanceVideoGenerationClient, + from celeste_video_generation.providers.byteplus.client import ( + BytePlusVideoGenerationClient, ) from celeste_video_generation.providers.google.client import ( GoogleVideoGenerationClient, @@ -19,7 +19,7 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]: ) return [ - (Provider.BYTEDANCE, ByteDanceVideoGenerationClient), + (Provider.BYTEPLUS, BytePlusVideoGenerationClient), (Provider.GOOGLE, GoogleVideoGenerationClient), (Provider.OPENAI, OpenAIVideoGenerationClient), ] diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/README.md b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/README.md index cdeb1beb..a9143804 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/README.md +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/README.md @@ -1,13 +1,13 @@ -# ByteDance Video Generation Provider +# BytePlus Video Generation Provider ## Credentials -**Environment variable:** `BYTEDANCE_API_KEY` +**Environment variable:** `BYTEPLUS_API_KEY` **Setup:** 1. Register at [console.byteplus.com](https://console.byteplus.com) 2. Activate model in ModelArk section 3. Generate API key with video generation permissions -4. Set environment variable: `export BYTEDANCE_API_KEY="your-key"` +4. Set environment variable: `export BYTEPLUS_API_KEY="your-key"` **Note:** Models must be activated in BytePlus console before use. If you get a 404 error, activate the model or use an Endpoint ID (`ep-xxx`) instead of Model ID. diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/__init__.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/__init__.py index 31a9eb8e..136cf8a3 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/__init__.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/__init__.py @@ -1,11 +1,11 @@ -"""ByteDance provider for video generation.""" +"""BytePlus provider for video generation.""" from celeste.core import Provider -from .client import ByteDanceVideoGenerationClient +from .client import BytePlusVideoGenerationClient -__all__ = ["PROVIDERS", "ByteDanceVideoGenerationClient"] +__all__ = ["PROVIDERS", "BytePlusVideoGenerationClient"] -PROVIDERS: list[tuple[Provider, type[ByteDanceVideoGenerationClient]]] = [ - (Provider.BYTEDANCE, ByteDanceVideoGenerationClient), +PROVIDERS: list[tuple[Provider, type[BytePlusVideoGenerationClient]]] = [ + (Provider.BYTEPLUS, BytePlusVideoGenerationClient), ] diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py index 198c149f..af5bf467 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py @@ -1,4 +1,4 @@ -"""ByteDance client implementation for video generation.""" +"""BytePlus client implementation for video generation.""" import asyncio import base64 @@ -20,24 +20,24 @@ from celeste_video_generation.parameters import VideoGenerationParameters from . import config -from .parameters import BYTEDANCE_PARAMETER_MAPPERS +from .parameters import BYTEPLUS_PARAMETER_MAPPERS logger = logging.getLogger(__name__) -class ByteDanceVideoGenerationClient(VideoGenerationClient): - """ByteDance client for video generation.""" +class BytePlusVideoGenerationClient(VideoGenerationClient): + """BytePlus client for video generation.""" @classmethod def parameter_mappers(cls) -> list[ParameterMapper]: - return BYTEDANCE_PARAMETER_MAPPERS + return BYTEPLUS_PARAMETER_MAPPERS def _validate_artifacts( self, inputs: VideoGenerationInput, **parameters: Unpack[VideoGenerationParameters], ) -> tuple[VideoGenerationInput, dict[str, Any]]: - """Validate and prepare artifacts for ByteDance API.""" + """Validate and prepare artifacts for BytePlus API.""" # Helper function to convert ImageArtifact to base64 data URI def convert_to_data_url(img: ImageArtifact) -> ImageArtifact: @@ -96,11 +96,11 @@ def _add_image_content_item( ) elif artifact.data: logger.warning( - f"ByteDance requires {artifact_type} URL, not base64 data. Upload {artifact_type} first." + f"BytePlus requires {artifact_type} URL, not base64 data. Upload {artifact_type} first." ) elif artifact.path: logger.warning( - f"ByteDance requires {artifact_type} URL, not file path. Upload {artifact_type} first." + f"BytePlus requires {artifact_type} URL, not file path. Upload {artifact_type} first." ) def _init_request(self, inputs: VideoGenerationInput) -> dict[str, Any]: @@ -136,7 +136,7 @@ def _parse_content( """Parse content from response.""" content = response_data.get("content") if not isinstance(content, dict): - msg = f"No content field in ByteDance response. Available keys: {list(response_data.keys())}" + msg = f"No content field in BytePlus response. Available keys: {list(response_data.keys())}" raise ValueError(msg) video_url = content.get("video_url") @@ -184,7 +184,7 @@ async def _make_request( "Content-Type": ApplicationMimeType.JSON, } - logger.debug("Submitting video generation task to ByteDance") + logger.debug("Submitting video generation task to BytePlus") submit_response = await self.http_client.post( f"{config.BASE_URL}{config.ENDPOINT}", headers=headers, @@ -194,7 +194,7 @@ async def _make_request( submit_data = submit_response.json() task_id = submit_data["id"] - logger.info(f"ByteDance task submitted: {task_id}") + logger.info(f"BytePlus task submitted: {task_id}") start_time = time.time() polling_interval = config.DEFAULT_POLLING_INTERVAL @@ -205,11 +205,11 @@ async def _make_request( while True: elapsed = time.time() - start_time if elapsed > config.MAX_POLLING_TIMEOUT: - msg = f"ByteDance task {task_id} timed out after {elapsed:.0f}s" + msg = f"BytePlus task {task_id} timed out after {elapsed:.0f}s" raise TimeoutError(msg) status_url = f"{config.BASE_URL}{config.STATUS_ENDPOINT_TEMPLATE.format(task_id=task_id)}" - logger.debug(f"Polling ByteDance task status: {task_id}") + logger.debug(f"Polling BytePlus task status: {task_id}") status_response = await self.http_client.get( status_url, @@ -219,10 +219,10 @@ async def _make_request( status_data = status_response.json() status = status_data.get("status") - logger.debug(f"ByteDance task {task_id} status: {status}") + logger.debug(f"BytePlus task {task_id} status: {status}") if status == config.STATUS_SUCCEEDED: - logger.info(f"ByteDance task {task_id} completed in {elapsed:.0f}s") + logger.info(f"BytePlus task {task_id} completed in {elapsed:.0f}s") return httpx.Response( 200, content=json.dumps(status_data).encode(), @@ -236,10 +236,10 @@ async def _make_request( if isinstance(error, dict) else "Unknown error" ) - msg = f"ByteDance task {task_id} failed: {error_msg}" + msg = f"BytePlus task {task_id} failed: {error_msg}" raise ValueError(msg) await asyncio.sleep(polling_interval) -__all__ = ["ByteDanceVideoGenerationClient"] +__all__ = ["BytePlusVideoGenerationClient"] diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py index 099d7e00..94eee1f1 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py @@ -1,4 +1,4 @@ -"""ByteDance provider configuration for video generation.""" +"""BytePlus provider configuration for video generation.""" # HTTP Configuration BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3" diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/models.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/models.py index e5c7eb7c..1a529d0a 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/models.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/models.py @@ -1,4 +1,4 @@ -"""ByteDance models for video generation. +"""BytePlus models for video generation. Model IDs use lowercase format with version suffixes (e.g., seedance-1-0-pro-250528). Console display names differ from API model IDs. @@ -9,8 +9,8 @@ from celeste.mime_types import ImageMimeType from celeste_video_generation.parameters import VideoGenerationParameter -# Supported MIME types for ByteDance image parameters -BYTEDANCE_SUPPORTED_MIME_TYPES = [ +# Supported MIME types for BytePlus image parameters +BYTEPLUS_SUPPORTED_MIME_TYPES = [ ImageMimeType.JPEG, ImageMimeType.PNG, ImageMimeType.WEBP, @@ -22,7 +22,7 @@ MODELS: list[Model] = [ Model( id="seedance-1-0-lite-t2v-250428", - provider=Provider.BYTEDANCE, + provider=Provider.BYTEPLUS, capabilities={Capability.VIDEO_GENERATION}, display_name="Seedance 1.0 Lite (Text-to-Video)", parameter_constraints={ @@ -31,16 +31,16 @@ options=["480p", "720p", "1080p"] ), VideoGenerationParameter.FIRST_FRAME: ImageConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, ), VideoGenerationParameter.LAST_FRAME: ImageConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, ), }, ), Model( id="seedance-1-0-lite-i2v-250428", - provider=Provider.BYTEDANCE, + provider=Provider.BYTEPLUS, capabilities={Capability.VIDEO_GENERATION}, display_name="Seedance 1.0 Lite (Image-to-Video)", parameter_constraints={ @@ -49,20 +49,20 @@ options=["480p", "720p", "1080p"] ), VideoGenerationParameter.REFERENCE_IMAGES: ImagesConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, max_count=4, ), VideoGenerationParameter.FIRST_FRAME: ImageConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, ), VideoGenerationParameter.LAST_FRAME: ImageConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, ), }, ), Model( id="seedance-1-0-pro-250528", - provider=Provider.BYTEDANCE, + provider=Provider.BYTEPLUS, capabilities={Capability.VIDEO_GENERATION}, display_name="Seedance 1.0 Pro", parameter_constraints={ @@ -71,16 +71,16 @@ options=["480p", "720p", "1080p"] ), VideoGenerationParameter.FIRST_FRAME: ImageConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, ), VideoGenerationParameter.LAST_FRAME: ImageConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, ), }, ), Model( id="seedance-1-0-pro-fast-251015", - provider=Provider.BYTEDANCE, + provider=Provider.BYTEPLUS, capabilities={Capability.VIDEO_GENERATION}, display_name="Seedance 1.0 Pro Fast", parameter_constraints={ @@ -89,10 +89,10 @@ options=["480p", "720p", "1080p"] ), VideoGenerationParameter.FIRST_FRAME: ImageConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, ), VideoGenerationParameter.LAST_FRAME: ImageConstraint( - supported_mime_types=BYTEDANCE_SUPPORTED_MIME_TYPES, + supported_mime_types=BYTEPLUS_SUPPORTED_MIME_TYPES, ), }, ), diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py index e84e3b07..d3622167 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py @@ -1,4 +1,4 @@ -"""ByteDance parameter mappers for video generation.""" +"""BytePlus parameter mappers for video generation.""" from typing import Any @@ -133,7 +133,7 @@ def map( return request if not validated_value.url: - msg = "ByteDance requires image URL (including data URIs) for first_frame. ImageArtifact must have url, data, or path" + msg = "BytePlus requires image URL (including data URIs) for first_frame. ImageArtifact must have url, data, or path" raise ValidationError(msg) content = request.setdefault("content", []) @@ -164,7 +164,7 @@ def map( return request if not validated_value.url: - msg = "ByteDance requires image URL (including data URIs) for last_frame. ImageArtifact must have url, data, or path" + msg = "BytePlus requires image URL (including data URIs) for last_frame. ImageArtifact must have url, data, or path" raise ValidationError(msg) content = request.setdefault("content", []) @@ -181,7 +181,7 @@ def map( return request -BYTEDANCE_PARAMETER_MAPPERS: list[ParameterMapper] = [ +BYTEPLUS_PARAMETER_MAPPERS: list[ParameterMapper] = [ DurationMapper(), ResolutionMapper(), ReferenceImagesMapper(), @@ -189,4 +189,4 @@ def map( LastFrameMapper(), ] -__all__ = ["BYTEDANCE_PARAMETER_MAPPERS"] +__all__ = ["BYTEPLUS_PARAMETER_MAPPERS"] diff --git a/packages/capabilities/video-generation/tests/integration_tests/test_video_generation/test_generate.py b/packages/capabilities/video-generation/tests/integration_tests/test_video_generation/test_generate.py index 455d92fb..3c9c0ff2 100644 --- a/packages/capabilities/video-generation/tests/integration_tests/test_video_generation/test_generate.py +++ b/packages/capabilities/video-generation/tests/integration_tests/test_video_generation/test_generate.py @@ -19,7 +19,7 @@ {"duration": 4, "resolution": "720p"}, ), ( - Provider.BYTEDANCE, + Provider.BYTEPLUS, "seedance-1-0-lite-t2v-250428", {"duration": 2, "resolution": "480p"}, ), diff --git a/packages/providers/byteplus/pyproject.toml b/packages/providers/byteplus/pyproject.toml new file mode 100644 index 00000000..931c07c8 --- /dev/null +++ b/packages/providers/byteplus/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "celeste-byteplus" +version = "0.1.0" +description = "BytePlus provider package for Celeste AI" +authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}] +license = {text = "Apache-2.0"} +requires-python = ">=3.12" + +[tool.uv.sources] +celeste-ai = { workspace = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/celeste_byteplus"] diff --git a/packages/providers/byteplus/src/celeste_byteplus/__init__.py b/packages/providers/byteplus/src/celeste_byteplus/__init__.py new file mode 100644 index 00000000..790f9cf9 --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/__init__.py @@ -0,0 +1,3 @@ +"""BytePlus provider package for Celeste AI.""" + +__all__: list[str] = [] diff --git a/packages/providers/byteplus/src/celeste_byteplus/images/__init__.py b/packages/providers/byteplus/src/celeste_byteplus/images/__init__.py new file mode 100644 index 00000000..7ddc2aa4 --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/images/__init__.py @@ -0,0 +1 @@ +"""BytePlus Images API provider package.""" diff --git a/packages/providers/byteplus/src/celeste_byteplus/images/client.py b/packages/providers/byteplus/src/celeste_byteplus/images/client.py new file mode 100644 index 00000000..18875f85 --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/images/client.py @@ -0,0 +1,120 @@ +"""BytePlus Images API client with shared implementation.""" + +from collections.abc import AsyncIterator +from typing import Any + +import httpx + +from celeste.core import UsageField +from celeste.io import FinishReason +from celeste.mime_types import ApplicationMimeType + +from . import config + + +class BytePlusImagesClient: + """Mixin for BytePlus Images API capabilities. + + Provides shared implementation for all capabilities using the Images API: + - _make_request() - HTTP POST to /api/v3/images/generations + - _make_stream_request() - HTTP streaming to /api/v3/images/generations + - _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 + + Usage: + class BytePlusImageGenerationClient(BytePlusImagesClient, ImageGenerationClient): + def _parse_content(self, response_data, **parameters): + images = response_data.get("images", []) + # Extract image from images[0] or data[0]... + """ + + async def _make_request( + self, + request_body: dict[str, Any], + **parameters: Any, + ) -> httpx.Response: + """Make HTTP request to BytePlus Images API endpoint.""" + request_body["stream"] = False + + headers = { + **self.auth.get_headers(), # type: ignore[attr-defined] + "Content-Type": ApplicationMimeType.JSON, + } + + return await self.http_client.post( # type: ignore[attr-defined,no-any-return] + f"{config.BASE_URL}{config.BytePlusImagesEndpoint.CREATE_IMAGE}", + headers=headers, + json_body=request_body, + ) + + def _make_stream_request( + self, + request_body: dict[str, Any], + **parameters: Any, + ) -> AsyncIterator[dict[str, Any]]: + """Make streaming request to BytePlus Images API endpoint.""" + request_body["stream"] = True + + headers = { + **self.auth.get_headers(), # type: ignore[attr-defined] + "Content-Type": ApplicationMimeType.JSON, + } + + return self.http_client.stream_post( # type: ignore[attr-defined,no-any-return] + f"{config.BASE_URL}{config.BytePlusImagesEndpoint.CREATE_IMAGE}", + headers=headers, + json_body=request_body, + ) + + def _parse_usage(self, response_data: dict[str, Any]) -> dict[str, int | None]: + """Extract usage data from Images API response. + + Returns dict that capability clients wrap in their specific Usage type. + """ + usage_data = response_data.get("usage", {}) + + return { + UsageField.TOTAL_TOKENS: usage_data.get("total_tokens"), + UsageField.OUTPUT_TOKENS: usage_data.get("output_tokens"), + UsageField.NUM_IMAGES: usage_data.get("generated_images"), + } + + def _parse_content(self, response_data: dict[str, Any]) -> Any: + """Parse images/data array from Images API response. + + Returns raw images/data array that capability clients extract from. + """ + images = response_data.get("images", []) + if images: + return images + + data = response_data.get("data", []) + if data: + return data + + return None + + def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: + """BytePlus doesn't provide finish reasons for image generation.""" + return FinishReason(reason=None) + + 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) # type: ignore[misc] + + # Add provider-specific parsed fields + seed = response_data.get("seed") + if seed is not None: + metadata["seed"] = seed + + return metadata + + +__all__ = ["BytePlusImagesClient"] diff --git a/packages/providers/byteplus/src/celeste_byteplus/images/config.py b/packages/providers/byteplus/src/celeste_byteplus/images/config.py new file mode 100644 index 00000000..62411e9f --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/images/config.py @@ -0,0 +1,12 @@ +"""Configuration for BytePlus Images API.""" + +from enum import StrEnum + + +class BytePlusImagesEndpoint(StrEnum): + """Endpoints for Images API.""" + + CREATE_IMAGE = "/api/v3/images/generations" + + +BASE_URL = "https://ark.ap-southeast.bytepluses.com" diff --git a/packages/providers/byteplus/src/celeste_byteplus/images/parameters.py b/packages/providers/byteplus/src/celeste_byteplus/images/parameters.py new file mode 100644 index 00000000..866c84e5 --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/images/parameters.py @@ -0,0 +1,45 @@ +"""BytePlus Images API parameter mappers.""" + +from typing import Any + +from celeste.models import Model +from celeste.parameters import ParameterMapper + + +class SizeMapper(ParameterMapper): + """Map size to BytePlus size field.""" + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform size into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["size"] = validated_value + return request + + +class WatermarkMapper(ParameterMapper): + """Map watermark to BytePlus watermark field.""" + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform watermark into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["watermark"] = validated_value + return request + + +__all__ = ["SizeMapper", "WatermarkMapper"] diff --git a/packages/providers/byteplus/src/celeste_byteplus/py.typed b/packages/providers/byteplus/src/celeste_byteplus/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/packages/providers/byteplus/src/celeste_byteplus/videos/__init__.py b/packages/providers/byteplus/src/celeste_byteplus/videos/__init__.py new file mode 100644 index 00000000..d728b47f --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/videos/__init__.py @@ -0,0 +1 @@ +"""BytePlus Videos API provider package.""" diff --git a/packages/providers/byteplus/src/celeste_byteplus/videos/client.py b/packages/providers/byteplus/src/celeste_byteplus/videos/client.py new file mode 100644 index 00000000..f04fcef8 --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/videos/client.py @@ -0,0 +1,132 @@ +"""BytePlus Videos API client with shared implementation.""" + +import asyncio +import logging +import time +from typing import Any + +import httpx + +from celeste.core import UsageField +from celeste.io import FinishReason +from celeste.mime_types import ApplicationMimeType + +from . import config + +logger = logging.getLogger(__name__) + + +class BytePlusVideosClient: + """Mixin for BytePlus ModelArk Videos API with async polling. + + Provides shared implementation: + - _make_request() - HTTP POST with async polling pattern + + The BytePlus Videos API uses async polling: + 1. POST to /api/v3/contents/generations/tasks to submit job + 2. Poll GET /api/v3/contents/generations/tasks/{task_id} until succeeded/failed + 3. Return final response + + Usage: + class BytePlusVideoGenerationClient(BytePlusVideosClient, VideoGenerationClient): + def _parse_content(self, response_data, **parameters): + content = response_data.get("content", {}) + # Extract video from content["video_url"]... + """ + + async def _make_request( + self, + request_body: dict[str, Any], + **parameters: Any, + ) -> httpx.Response: + """Make HTTP request with async polling for BytePlus video generation. + + Handles the complete async polling workflow: + 1. Submit job to CONTENT_GENERATIONS endpoint + 2. Poll CONTENT_STATUS endpoint until succeeded/failed/canceled + 3. Return response with final status data + """ + auth_headers = self.auth.get_headers() # type: ignore[attr-defined] + headers = { + **auth_headers, + "Content-Type": ApplicationMimeType.JSON, + } + + # Phase 1: Submit job + logger.debug("Submitting video generation task to BytePlus") + submit_response = await self.http_client.post( # type: ignore[attr-defined] + f"{config.BASE_URL}{config.BytePlusVideosEndpoint.CREATE_VIDEO}", + headers=headers, + json_body=request_body, + ) + + if submit_response.status_code != 200: + return submit_response # type: ignore[no-any-return] + + submit_data = submit_response.json() + task_id = submit_data["id"] + logger.info(f"BytePlus task submitted: {task_id}") + + # Phase 2: Poll for completion + start_time = time.monotonic() + + # Wait before first poll + await asyncio.sleep(config.POLLING_INTERVAL) + + while True: + elapsed = time.monotonic() - start_time + if elapsed >= config.POLLING_TIMEOUT: + msg = f"BytePlus task {task_id} timed out after {config.POLLING_TIMEOUT} seconds" + raise TimeoutError(msg) + + status_url = f"{config.BASE_URL}{config.BytePlusVideosEndpoint.GET_VIDEO_STATUS.format(task_id=task_id)}" + logger.debug(f"Polling BytePlus task status: {task_id}") + + status_response = await self.http_client.get( # type: ignore[attr-defined] + status_url, + headers=headers, + ) + + if status_response.status_code != 200: + return status_response # type: ignore[no-any-return] + + status_data = status_response.json() + status = status_data.get("status") + logger.debug(f"BytePlus task {task_id} status: {status}") + + if status == config.STATUS_SUCCEEDED: + logger.info(f"BytePlus task {task_id} completed in {elapsed:.0f}s") + return httpx.Response( + status_code=200, + json=status_data, + request=httpx.Request("GET", status_url), + ) + + if status in (config.STATUS_FAILED, config.STATUS_CANCELED): + error = status_data.get("error", {}) + error_msg = ( + error.get("message", "Unknown error") + if isinstance(error, dict) + else "Unknown error" + ) + msg = f"BytePlus task {task_id} failed: {error_msg}" + raise ValueError(msg) + + await asyncio.sleep(config.POLLING_INTERVAL) + + def _parse_usage(self, response_data: dict[str, Any]) -> dict[str, Any]: + """Extract usage data from BytePlus API response. + + Returns dict with usage fields for capability clients to wrap in Usage type. + """ + usage_data = response_data.get("usage", {}) + return { + UsageField.TOTAL_TOKENS: usage_data.get("total_tokens"), + } + + def _parse_finish_reason(self, response_data: dict[str, Any]) -> FinishReason: + """BytePlus provides status but not structured finish reasons.""" + return FinishReason(reason=None) + + +__all__ = ["BytePlusVideosClient"] diff --git a/packages/providers/byteplus/src/celeste_byteplus/videos/config.py b/packages/providers/byteplus/src/celeste_byteplus/videos/config.py new file mode 100644 index 00000000..607acd05 --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/videos/config.py @@ -0,0 +1,22 @@ +"""Configuration for BytePlus Videos API.""" + +from enum import StrEnum + + +class BytePlusVideosEndpoint(StrEnum): + """Endpoints for Videos API.""" + + CREATE_VIDEO = "/api/v3/contents/generations/tasks" + GET_VIDEO_STATUS = "/api/v3/contents/generations/tasks/{task_id}" + + +BASE_URL = "https://ark.ap-southeast.bytepluses.com" + +# Polling Configuration +POLLING_INTERVAL = 5 # seconds +POLLING_TIMEOUT = 300 # 5 minutes + +# Status Constants +STATUS_SUCCEEDED = "succeeded" +STATUS_FAILED = "failed" +STATUS_CANCELED = "canceled" diff --git a/packages/providers/byteplus/src/celeste_byteplus/videos/parameters.py b/packages/providers/byteplus/src/celeste_byteplus/videos/parameters.py new file mode 100644 index 00000000..b0ea3f1e --- /dev/null +++ b/packages/providers/byteplus/src/celeste_byteplus/videos/parameters.py @@ -0,0 +1,179 @@ +"""BytePlus Videos API parameter mappers.""" + +from typing import Any + +from celeste.exceptions import ValidationError +from celeste.models import Model +from celeste.parameters import ParameterMapper + + +class DurationMapper(ParameterMapper): + """Map duration parameter to BytePlus text prompt format. + + Appends --duration to the text prompt. + Supported values: 2-12 seconds. + """ + + def map( + self, request: dict[str, Any], value: object, model: Model + ) -> dict[str, Any]: + """Transform duration into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + content = request.get("content", []) + for item in content: + if item.get("type") == "text": + text = item.get("text", "") + item["text"] = f"{text} --duration {validated_value}" + break + + return request + + +class ResolutionMapper(ParameterMapper): + """Map resolution parameter to BytePlus text prompt format. + + Appends --resolution to the text prompt. + Supported values: 480p, 720p, 1080p. + """ + + def map( + self, request: dict[str, Any], value: object, model: Model + ) -> dict[str, Any]: + """Transform resolution into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + content = request.get("content", []) + for item in content: + if item.get("type") == "text": + text = item.get("text", "") + item["text"] = f"{text} --resolution {validated_value}" + break + + return request + + +class AspectRatioMapper(ParameterMapper): + """Map aspect_ratio parameter to BytePlus text prompt format. + + Appends --ratio to the text prompt. + Supported values: 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive. + """ + + def map( + self, request: dict[str, Any], value: object, model: Model + ) -> dict[str, Any]: + """Transform aspect_ratio into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + content = request.get("content", []) + for item in content: + if item.get("type") == "text": + text = item.get("text", "") + item["text"] = f"{text} --ratio {validated_value}" + break + + return request + + +class ReferenceImagesMapper(ParameterMapper): + """Map reference_images to BytePlus content array format. + + Adds images with role="reference_image" to the content array. + """ + + def map( + self, request: dict[str, Any], value: object, model: Model + ) -> dict[str, Any]: + """Transform reference_images into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + content = request.setdefault("content", []) + for img in validated_value: + content.append( + { + "type": "image_url", + "image_url": {"url": img.url}, + "role": "reference_image", + } + ) + + return request + + +class FirstFrameMapper(ParameterMapper): + """Map first_frame to BytePlus content array format. + + Adds image with role="first_frame" to the content array. + """ + + def map( + self, request: dict[str, Any], value: object, model: Model + ) -> dict[str, Any]: + """Transform first_frame into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + if not validated_value.url: + msg = "BytePlus requires image URL for first_frame." + raise ValidationError(msg) from None + + content = request.setdefault("content", []) + content.append( + { + "type": "image_url", + "image_url": {"url": validated_value.url}, + "role": "first_frame", + } + ) + + return request + + +class LastFrameMapper(ParameterMapper): + """Map last_frame to BytePlus content array format. + + Adds image with role="last_frame" to the content array. + """ + + def map( + self, request: dict[str, Any], value: object, model: Model + ) -> dict[str, Any]: + """Transform last_frame into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + if not validated_value.url: + msg = "BytePlus requires image URL for last_frame." + raise ValidationError(msg) from None + + content = request.setdefault("content", []) + content.append( + { + "type": "image_url", + "image_url": {"url": validated_value.url}, + "role": "last_frame", + } + ) + + return request + + +__all__ = [ + "AspectRatioMapper", + "DurationMapper", + "FirstFrameMapper", + "LastFrameMapper", + "ReferenceImagesMapper", + "ResolutionMapper", +] diff --git a/src/celeste/core.py b/src/celeste/core.py index fadd94be..92c1d1bf 100644 --- a/src/celeste/core.py +++ b/src/celeste/core.py @@ -19,7 +19,7 @@ class Provider(StrEnum): LUMA = "luma" TOPAZLABS = "topazlabs" PERPLEXITY = "perplexity" - BYTEDANCE = "bytedance" + BYTEPLUS = "byteplus" ELEVENLABS = "elevenlabs" diff --git a/src/celeste/credentials.py b/src/celeste/credentials.py index 382ca97f..5a3fe24e 100644 --- a/src/celeste/credentials.py +++ b/src/celeste/credentials.py @@ -23,7 +23,7 @@ Provider.LUMA: ("celeste_luma", "Authorization", "Bearer "), Provider.TOPAZLABS: ("celeste_topazlabs", "X-API-Key", ""), Provider.PERPLEXITY: ("celeste_perplexity", "Authorization", "Bearer "), - Provider.BYTEDANCE: ("celeste_bytedance", "Authorization", "Bearer "), + Provider.BYTEPLUS: ("celeste_byteplus", "Authorization", "Bearer "), Provider.ELEVENLABS: ("celeste_elevenlabs", "xi-api-key", ""), Provider.BFL: ("celeste_bfl", "x-key", ""), } @@ -42,7 +42,7 @@ Provider.LUMA: "luma_api_key", Provider.TOPAZLABS: "topazlabs_api_key", Provider.PERPLEXITY: "perplexity_api_key", - Provider.BYTEDANCE: "bytedance_api_key", + Provider.BYTEPLUS: "byteplus_api_key", Provider.ELEVENLABS: "elevenlabs_api_key", Provider.BFL: "bfl_api_key", } @@ -63,7 +63,7 @@ class Credentials(BaseSettings): luma_api_key: SecretStr | None = Field(None, alias="LUMA_API_KEY") topazlabs_api_key: SecretStr | None = Field(None, alias="TOPAZLABS_API_KEY") perplexity_api_key: SecretStr | None = Field(None, alias="PERPLEXITY_API_KEY") - bytedance_api_key: SecretStr | None = Field(None, alias="BYTEDANCE_API_KEY") + byteplus_api_key: SecretStr | None = Field(None, alias="BYTEPLUS_API_KEY") elevenlabs_api_key: SecretStr | None = Field(None, alias="ELEVENLABS_API_KEY") bfl_api_key: SecretStr | None = Field(None, alias="BFL_API_KEY") From d78d0551875346521d93e9b1e718c3ef1ae5b768 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 15 Dec 2025 18:41:22 +0100 Subject: [PATCH 3/4] chore(capabilities): remove redundant BytePlus config files and migrate to mixins Remove unused config.py files from BytePlus capability providers and migrate clients to use provider package mixins following the established pattern. ## Changes - Remove image-generation/providers/byteplus/config.py - Remove video-generation/providers/byteplus/config.py - Migrate clients to use BytePlusImagesClient and BytePlusVideosClient mixins - Add celeste-byteplus workspace dependency to both capability packages - Simplify client implementations by delegating HTTP requests to mixins Configuration is now centralized in the provider package (celeste_byteplus.images.config and celeste_byteplus.videos.config) following the established pattern from OpenAI, Mistral, and other providers. --- .../image-generation/pyproject.toml | 1 + .../providers/byteplus/client.py | 69 +------ .../providers/byteplus/config.py | 10 - .../providers/byteplus/parameters.py | 74 ++----- .../test_image_generation/test_generate.py | 1 - .../video-generation/pyproject.toml | 1 + .../providers/byteplus/client.py | 87 +-------- .../providers/byteplus/config.py | 19 -- .../providers/byteplus/parameters.py | 181 ++---------------- packages/providers/byteplus/pyproject.toml | 2 +- .../byteplus/src/celeste_byteplus/__init__.py | 2 - src/celeste/__init__.py | 13 +- src/celeste/core.py | 2 +- 13 files changed, 66 insertions(+), 396 deletions(-) delete mode 100644 packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py delete mode 100644 packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py diff --git a/packages/capabilities/image-generation/pyproject.toml b/packages/capabilities/image-generation/pyproject.toml index 2c152148..8bd02504 100644 --- a/packages/capabilities/image-generation/pyproject.toml +++ b/packages/capabilities/image-generation/pyproject.toml @@ -28,6 +28,7 @@ Issues = "https://github.com/withceleste/celeste-python/issues" [tool.uv.sources] celeste-ai = { workspace = true } celeste-bfl = { workspace = true } +celeste-byteplus = { workspace = true } celeste-google = { workspace = true } celeste-openai = { workspace = true } diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py index 20c9af95..2a887df6 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/client.py @@ -1,14 +1,13 @@ """BytePlus client implementation for image generation.""" import base64 -from collections.abc import AsyncIterator from typing import Any, Unpack -import httpx +from celeste_byteplus.images.client import BytePlusImagesClient from celeste.artifacts import ImageArtifact from celeste.exceptions import ConstraintViolationError, ValidationError -from celeste.mime_types import ApplicationMimeType, ImageMimeType +from celeste.mime_types import ImageMimeType from celeste.parameters import ParameterMapper from celeste_image_generation.client import ImageGenerationClient from celeste_image_generation.io import ( @@ -18,12 +17,11 @@ ) from celeste_image_generation.parameters import ImageGenerationParameters -from . import config from .parameters import BYTEPLUS_PARAMETER_MAPPERS from .streaming import BytePlusImageGenerationStream -class BytePlusImageGenerationClient(ImageGenerationClient): +class BytePlusImageGenerationClient(BytePlusImagesClient, ImageGenerationClient): """BytePlus client for image generation.""" @classmethod @@ -40,13 +38,8 @@ def _init_request(self, inputs: ImageGenerationInput) -> dict[str, Any]: def _parse_usage(self, response_data: dict[str, Any]) -> ImageGenerationUsage: """Parse usage from response.""" - usage_data = response_data.get("usage", {}) - - return ImageGenerationUsage( - total_tokens=usage_data.get("total_tokens"), - output_tokens=usage_data.get("output_tokens"), - generated_images=usage_data.get("generated_images"), - ) + usage = super()._parse_usage(response_data) + return ImageGenerationUsage(**usage) def _parse_content( self, @@ -80,31 +73,19 @@ def _parse_content( def _parse_finish_reason( self, response_data: dict[str, Any] - ) -> ImageGenerationFinishReason | None: + ) -> ImageGenerationFinishReason: """Parse finish reason from response. BytePlus doesn't provide finish reasons for image generation. """ - return None - - def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: - """Build metadata dictionary from response data. - - Extracts seed if present. - """ - metadata = super()._build_metadata(response_data) - # Add provider-specific parsed fields - seed = response_data.get("seed") - if seed is not None: - metadata["seed"] = seed - return metadata + return ImageGenerationFinishReason(reason=None) async def _make_request( self, request_body: dict[str, Any], **parameters: Unpack[ImageGenerationParameters], - ) -> httpx.Response: - """Make HTTP request(s) and return response object.""" + ) -> Any: + """Make HTTP request with parameter validation.""" # Validate mutually exclusive parameters if parameters.get("aspect_ratio") and parameters.get("quality"): msg = ( @@ -116,41 +97,11 @@ async def _make_request( ) raise ConstraintViolationError(msg) - request_body["stream"] = False - - headers = { - **self.auth.get_headers(), - "Content-Type": ApplicationMimeType.JSON, - } - - return await self.http_client.post( - f"{config.BASE_URL}{config.ENDPOINT}", - headers=headers, - json_body=request_body, - ) + return await super()._make_request(request_body, **parameters) def _stream_class(self) -> type[BytePlusImageGenerationStream]: """Return the Stream class for this client.""" return BytePlusImageGenerationStream - def _make_stream_request( - self, - request_body: dict[str, Any], - **parameters: Unpack[ImageGenerationParameters], - ) -> AsyncIterator[dict[str, Any]]: - """Make HTTP streaming request and return async iterator of events.""" - request_body["stream"] = True - - headers = { - **self.auth.get_headers(), - "Content-Type": ApplicationMimeType.JSON, - } - - return self.http_client.stream_post( - f"{config.BASE_URL}{config.ENDPOINT}", - headers=headers, - json_body=request_body, - ) - __all__ = ["BytePlusImageGenerationClient"] diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py deleted file mode 100644 index 1bbea40d..00000000 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/config.py +++ /dev/null @@ -1,10 +0,0 @@ -"""BytePlus provider configuration for image generation.""" - -# HTTP Configuration -BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3" -ENDPOINT = "/images/generations" -STREAM_ENDPOINT = "/responses" - -# Authentication -AUTH_HEADER_NAME = "Authorization" -AUTH_HEADER_PREFIX = "Bearer " diff --git a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py index b1ecb5c7..8bc19da7 100644 --- a/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py +++ b/packages/capabilities/image-generation/src/celeste_image_generation/providers/byteplus/parameters.py @@ -1,45 +1,25 @@ -"""BytePlus parameter mappers for image generation.""" +"""BytePlus Images parameter mappers for image generation.""" from typing import Any -from celeste import Model +from celeste_byteplus.images.parameters import ( + SizeMapper as _SizeMapper, +) +from celeste_byteplus.images.parameters import ( + WatermarkMapper as _WatermarkMapper, +) + +from celeste.models import Model from celeste.parameters import ParameterMapper from celeste_image_generation.parameters import ImageGenerationParameter -class AspectRatioMapper(ParameterMapper): - """Map aspect_ratio to dimension string. - - Accepts freeform dimension strings (e.g., "2048x2048", "3840x2160") - validated by Dimensions constraint against BytePlus's pixel and aspect ratio bounds. - """ - +class AspectRatioMapper(_SizeMapper): name = ImageGenerationParameter.ASPECT_RATIO - def map( - self, - request: dict[str, Any], - value: object, - model: Model, - ) -> dict[str, Any]: - """Transform aspect_ratio into provider request. - - The Dimensions constraint validates: - - Format: "WIDTHxHEIGHT" - - Total pixels: [921,600, 16,777,216] - - Aspect ratio: [1/16, 16] - """ - validated_value = self._validate_value(value, model) - if validated_value is None: - return request - - # Transform to provider-specific request format (top-level field) - request["size"] = validated_value - return request - -class QualityMapper(ParameterMapper): - """Map quality parameter with validation.""" +class QualityMapper(_SizeMapper): + """Map quality to BytePlus size field with conflict resolution.""" name = ImageGenerationParameter.QUALITY @@ -51,46 +31,22 @@ def map( ) -> dict[str, Any]: """Transform quality into provider request. - Maps quality levels ("1K", "2K", "4K") to BytePlus's size parameter. Skips if size is already set by aspect_ratio (conflict resolution). """ validated_value = self._validate_value(value, model) if validated_value is None: return request - # Skip if size already set by aspect_ratio parameter (conflict resolution) + # Skip if size already set by aspect_ratio parameter if "size" in request: return request - # Transform to provider-specific request format (top-level field) - request["size"] = validated_value - return request - + return super().map(request, validated_value, model) -class WatermarkMapper(ParameterMapper): - """Map watermark parameter with validation.""" +class WatermarkMapper(_WatermarkMapper): name = ImageGenerationParameter.WATERMARK - def map( - self, - request: dict[str, Any], - value: object, - model: Model, - ) -> dict[str, Any]: - """Transform watermark into provider request. - - Adds "AI generated" watermark to bottom-right corner when true. - Default is true if omitted. - """ - validated_value = self._validate_value(value, model) - if validated_value is None: - return request - - # Transform to provider-specific request format (top-level field) - request["watermark"] = validated_value - return request - BYTEPLUS_PARAMETER_MAPPERS: list[ParameterMapper] = [ AspectRatioMapper(), diff --git a/packages/capabilities/image-generation/tests/integration_tests/test_image_generation/test_generate.py b/packages/capabilities/image-generation/tests/integration_tests/test_image_generation/test_generate.py index 72662b14..f869979a 100644 --- a/packages/capabilities/image-generation/tests/integration_tests/test_image_generation/test_generate.py +++ b/packages/capabilities/image-generation/tests/integration_tests/test_image_generation/test_generate.py @@ -42,7 +42,6 @@ async def test_generate(provider: Provider, model: str, parameters: dict) -> Non # Act response = await client.generate( prompt=prompt, - model=model, **parameters, ) diff --git a/packages/capabilities/video-generation/pyproject.toml b/packages/capabilities/video-generation/pyproject.toml index 63374299..bee45e6e 100644 --- a/packages/capabilities/video-generation/pyproject.toml +++ b/packages/capabilities/video-generation/pyproject.toml @@ -30,6 +30,7 @@ Issues = "https://github.com/withceleste/celeste-python/issues" [tool.uv.sources] celeste-ai = { workspace = true } +celeste-byteplus = { workspace = true } celeste-google = { workspace = true } celeste-openai = { workspace = true } diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py index af5bf467..c23401a8 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/client.py @@ -1,16 +1,13 @@ """BytePlus client implementation for video generation.""" -import asyncio import base64 -import json import logging -import time from typing import Any, Unpack -import httpx +from celeste_byteplus.videos.client import BytePlusVideosClient from celeste.artifacts import ImageArtifact, VideoArtifact -from celeste.mime_types import ApplicationMimeType, VideoMimeType +from celeste.mime_types import VideoMimeType from celeste.parameters import ParameterMapper from celeste_video_generation.client import VideoGenerationClient from celeste_video_generation.io import ( @@ -19,13 +16,12 @@ ) from celeste_video_generation.parameters import VideoGenerationParameters -from . import config from .parameters import BYTEPLUS_PARAMETER_MAPPERS logger = logging.getLogger(__name__) -class BytePlusVideoGenerationClient(VideoGenerationClient): +class BytePlusVideoGenerationClient(BytePlusVideosClient, VideoGenerationClient): """BytePlus client for video generation.""" @classmethod @@ -39,7 +35,6 @@ def _validate_artifacts( ) -> tuple[VideoGenerationInput, dict[str, Any]]: """Validate and prepare artifacts for BytePlus API.""" - # Helper function to convert ImageArtifact to base64 data URI def convert_to_data_url(img: ImageArtifact) -> ImageArtifact: if img.url: return img @@ -121,12 +116,8 @@ def _init_request(self, inputs: VideoGenerationInput) -> dict[str, Any]: def _parse_usage(self, response_data: dict[str, Any]) -> VideoGenerationUsage: """Parse usage from response.""" - usage_data = response_data.get("usage", {}) - total_tokens = usage_data.get("total_tokens") - - return VideoGenerationUsage( - total_tokens=total_tokens, - ) + usage = super()._parse_usage(response_data) + return VideoGenerationUsage(**usage) def _parse_content( self, @@ -173,73 +164,5 @@ def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: return metadata - async def _make_request( - self, - request_body: dict[str, Any], - **parameters: Unpack[VideoGenerationParameters], - ) -> httpx.Response: - """Make HTTP request with async polling.""" - headers = { - **self.auth.get_headers(), - "Content-Type": ApplicationMimeType.JSON, - } - - logger.debug("Submitting video generation task to BytePlus") - submit_response = await self.http_client.post( - f"{config.BASE_URL}{config.ENDPOINT}", - headers=headers, - json_body=request_body, - ) - self._handle_error_response(submit_response) - submit_data = submit_response.json() - - task_id = submit_data["id"] - logger.info(f"BytePlus task submitted: {task_id}") - - start_time = time.time() - polling_interval = config.DEFAULT_POLLING_INTERVAL - - # Wait before first poll (consistent with Google pattern) - await asyncio.sleep(polling_interval) - - while True: - elapsed = time.time() - start_time - if elapsed > config.MAX_POLLING_TIMEOUT: - msg = f"BytePlus task {task_id} timed out after {elapsed:.0f}s" - raise TimeoutError(msg) - - status_url = f"{config.BASE_URL}{config.STATUS_ENDPOINT_TEMPLATE.format(task_id=task_id)}" - logger.debug(f"Polling BytePlus task status: {task_id}") - - status_response = await self.http_client.get( - status_url, - headers=headers, - ) - self._handle_error_response(status_response) - status_data = status_response.json() - - status = status_data.get("status") - logger.debug(f"BytePlus task {task_id} status: {status}") - - if status == config.STATUS_SUCCEEDED: - logger.info(f"BytePlus task {task_id} completed in {elapsed:.0f}s") - return httpx.Response( - 200, - content=json.dumps(status_data).encode(), - headers={"Content-Type": ApplicationMimeType.JSON}, - ) - - if status in [config.STATUS_FAILED, config.STATUS_CANCELED]: - error = status_data.get("error", {}) - error_msg = ( - error.get("message", "Unknown error") - if isinstance(error, dict) - else "Unknown error" - ) - msg = f"BytePlus task {task_id} failed: {error_msg}" - raise ValueError(msg) - - await asyncio.sleep(polling_interval) - __all__ = ["BytePlusVideoGenerationClient"] diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py deleted file mode 100644 index 94eee1f1..00000000 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/config.py +++ /dev/null @@ -1,19 +0,0 @@ -"""BytePlus provider configuration for video generation.""" - -# HTTP Configuration -BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3" -ENDPOINT = "/contents/generations/tasks" -STATUS_ENDPOINT_TEMPLATE = "/contents/generations/tasks/{task_id}" - -# Authentication -AUTH_HEADER_NAME = "Authorization" -AUTH_HEADER_PREFIX = "Bearer " - -# Polling Configuration -DEFAULT_POLLING_INTERVAL = 5 # seconds -MAX_POLLING_TIMEOUT = 300 # 5 minutes - -# Status Constants -STATUS_SUCCEEDED = "succeeded" -STATUS_FAILED = "failed" -STATUS_CANCELED = "canceled" diff --git a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py index d3622167..3270606d 100644 --- a/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py +++ b/packages/capabilities/video-generation/src/celeste_video_generation/providers/byteplus/parameters.py @@ -1,185 +1,44 @@ """BytePlus parameter mappers for video generation.""" -from typing import Any +from celeste_byteplus.videos.parameters import ( + DurationMapper as _DurationMapper, +) +from celeste_byteplus.videos.parameters import ( + FirstFrameMapper as _FirstFrameMapper, +) +from celeste_byteplus.videos.parameters import ( + LastFrameMapper as _LastFrameMapper, +) +from celeste_byteplus.videos.parameters import ( + ReferenceImagesMapper as _ReferenceImagesMapper, +) +from celeste_byteplus.videos.parameters import ( + ResolutionMapper as _ResolutionMapper, +) -from celeste import Model -from celeste.exceptions import ValidationError from celeste.parameters import ParameterMapper from celeste_video_generation.parameters import VideoGenerationParameter -class DurationMapper(ParameterMapper): - """Map duration parameter to BytePlus ModelArk text prompt format. - - BytePlus ModelArk API expects parameters embedded in the text prompt: - "prompt text --duration 5" instead of {"duration": 5}. - - All Seedance models support duration: 2-12 seconds. - """ - +class DurationMapper(_DurationMapper): name = VideoGenerationParameter.DURATION - def map( - self, request: dict[str, Any], value: object, model: Model - ) -> dict[str, Any]: - """Append --duration parameter to text prompt in content array. - - Args: - request: The request dictionary with content array. - value: The duration value in seconds (integer). - model: The model being used (provides constraints). - - Returns: - Modified request with duration appended to text prompt. - """ - validated_value = self._validate_value(value, model) - if validated_value is None: - return request - - # BytePlus ModelArk uses content array with text type - # Parameters must be embedded in the text field: "prompt --duration 5" - content = request.get("content", []) - for item in content: - if item.get("type") == "text": - text = item.get("text", "") - # Append duration parameter to text prompt - item["text"] = f"{text} --duration {validated_value}" - break - - return request - - -class ResolutionMapper(ParameterMapper): - """Map resolution parameter to BytePlus ModelArk text prompt format. - - BytePlus ModelArk API expects parameters embedded in the text prompt: - "prompt text --resolution 720p" instead of {"resolution": "720p"}. - - Supported values: - - All models: 480p, 720p, 1080p - """ +class ResolutionMapper(_ResolutionMapper): name = VideoGenerationParameter.RESOLUTION - def map( - self, request: dict[str, Any], value: object, model: Model - ) -> dict[str, Any]: - """Append --resolution parameter to text prompt in content array. - - Args: - request: The request dictionary with content array. - value: The resolution value (e.g., "720p"). - model: The model being used (provides constraints). - - Returns: - Modified request with resolution appended to text prompt. - """ - validated_value = self._validate_value(value, model) - if validated_value is None: - return request - - # BytePlus ModelArk uses content array with text type - # Parameters must be embedded in the text field: "prompt --resolution 720p" - content = request.get("content", []) - for item in content: - if item.get("type") == "text": - text = item.get("text", "") - # Append resolution parameter to text prompt - item["text"] = f"{text} --resolution {validated_value}" - break - - return request - - -class ReferenceImagesMapper(ParameterMapper): - """Map reference_images parameter to BytePlus ModelArk content array format.""" +class ReferenceImagesMapper(_ReferenceImagesMapper): name = VideoGenerationParameter.REFERENCE_IMAGES - def map( - self, request: dict[str, Any], value: object, model: Model - ) -> dict[str, Any]: - """Add reference images to content array with reference_image role.""" - validated_value = self._validate_value(value, model) - if validated_value is None: - return request - - content = request.setdefault("content", []) - for img in validated_value: - content.append( - { - "type": "image_url", - "image_url": { - "url": img.url, - }, - "role": "reference_image", - } - ) - - return request - - -class FirstFrameMapper(ParameterMapper): - """Map first_frame parameter to BytePlus ModelArk content array format.""" +class FirstFrameMapper(_FirstFrameMapper): name = VideoGenerationParameter.FIRST_FRAME - def map( - self, request: dict[str, Any], value: object, model: Model - ) -> dict[str, Any]: - """Add first frame image to content array with first_frame role.""" - validated_value = self._validate_value(value, model) - if validated_value is None: - return request - - if not validated_value.url: - msg = "BytePlus requires image URL (including data URIs) for first_frame. ImageArtifact must have url, data, or path" - raise ValidationError(msg) - - content = request.setdefault("content", []) - content.append( - { - "type": "image_url", - "image_url": { - "url": validated_value.url, - }, - "role": "first_frame", - } - ) - - return request - - -class LastFrameMapper(ParameterMapper): - """Map last_frame parameter to BytePlus ModelArk content array format.""" +class LastFrameMapper(_LastFrameMapper): name = VideoGenerationParameter.LAST_FRAME - def map( - self, request: dict[str, Any], value: object, model: Model - ) -> dict[str, Any]: - """Add last frame image to content array with last_frame role.""" - validated_value = self._validate_value(value, model) - if validated_value is None: - return request - - if not validated_value.url: - msg = "BytePlus requires image URL (including data URIs) for last_frame. ImageArtifact must have url, data, or path" - raise ValidationError(msg) - - content = request.setdefault("content", []) - content.append( - { - "type": "image_url", - "image_url": { - "url": validated_value.url, - }, - "role": "last_frame", - } - ) - - return request - BYTEPLUS_PARAMETER_MAPPERS: list[ParameterMapper] = [ DurationMapper(), diff --git a/packages/providers/byteplus/pyproject.toml b/packages/providers/byteplus/pyproject.toml index 931c07c8..d23ac2fd 100644 --- a/packages/providers/byteplus/pyproject.toml +++ b/packages/providers/byteplus/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "celeste-byteplus" -version = "0.1.0" +version = "0.3.0" description = "BytePlus provider package for Celeste AI" authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}] license = {text = "Apache-2.0"} diff --git a/packages/providers/byteplus/src/celeste_byteplus/__init__.py b/packages/providers/byteplus/src/celeste_byteplus/__init__.py index 790f9cf9..ba7e53de 100644 --- a/packages/providers/byteplus/src/celeste_byteplus/__init__.py +++ b/packages/providers/byteplus/src/celeste_byteplus/__init__.py @@ -1,3 +1 @@ """BytePlus provider package for Celeste AI.""" - -__all__: list[str] = [] diff --git a/src/celeste/__init__.py b/src/celeste/__init__.py index d13f6558..aa2a5a81 100644 --- a/src/celeste/__init__.py +++ b/src/celeste/__init__.py @@ -4,7 +4,7 @@ from celeste.auth import APIKey, Authentication from celeste.client import Client, get_client_class, register_client -from celeste.core import Capability, Parameter, Provider +from celeste.core import Capability, Parameter, Provider, UsageField from celeste.credentials import credentials from celeste.exceptions import ( ClientNotFoundError, @@ -25,6 +25,12 @@ from celeste.models import Model, get_model, list_models, register_models from celeste.parameters import Parameters from celeste.registry import _load_from_entry_points +from celeste.structured_outputs import ( + RefResolvingJsonSchemaGenerator, + StrictJsonSchemaGenerator, + StrictRefResolvingJsonSchemaGenerator, +) +from celeste.types import JsonValue from celeste.utils import image_to_data_uri logger = logging.getLogger(__name__) @@ -117,6 +123,7 @@ def create_client( "Error", "HTTPClient", "Input", + "JsonValue", "MissingCredentialsError", "Model", "ModelNotFoundError", @@ -124,13 +131,17 @@ def create_client( "Parameter", "Parameters", "Provider", + "RefResolvingJsonSchemaGenerator", "StreamEmptyError", "StreamNotExhaustedError", "StreamingNotSupportedError", + "StrictJsonSchemaGenerator", + "StrictRefResolvingJsonSchemaGenerator", "UnsupportedCapabilityError", "UnsupportedParameterError", "UnsupportedProviderError", "Usage", + "UsageField", "ValidationError", "close_all_http_clients", "create_client", diff --git a/src/celeste/core.py b/src/celeste/core.py index 92c1d1bf..317e5a71 100644 --- a/src/celeste/core.py +++ b/src/celeste/core.py @@ -8,7 +8,7 @@ class Provider(StrEnum): OPENAI = "openai" ANTHROPIC = "anthropic" - BFL = "bfl" + BFL = "Black Forest Labs" GOOGLE = "google" MISTRAL = "mistral" COHERE = "cohere" From 0ee83b840d0fc91f1e98b6e80b0b26f819356f70 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 15 Dec 2025 18:42:29 +0100 Subject: [PATCH 4/4] fix(core): change BFL provider value to lowercase 'bfl' Change BFL provider value from 'Black Forest Labs' to 'bfl' to match the lowercase convention expected by all providers. This fixes the test_all_providers_are_lowercase test failure. --- src/celeste/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/celeste/core.py b/src/celeste/core.py index 317e5a71..92c1d1bf 100644 --- a/src/celeste/core.py +++ b/src/celeste/core.py @@ -8,7 +8,7 @@ class Provider(StrEnum): OPENAI = "openai" ANTHROPIC = "anthropic" - BFL = "Black Forest Labs" + BFL = "bfl" GOOGLE = "google" MISTRAL = "mistral" COHERE = "cohere"