From e9a1228d5d9c145f6976922aa8b718e9253767a1 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 1 Dec 2025 22:58:56 +0100 Subject: [PATCH 1/3] feat(image-generation): add BFL (Black Forest Labs) provider Add support for BFL's FLUX.2 image generation models: - flux-2-pro: Professional model with automatic prompt upsampling - flux-2-flex: Flexible model with configurable steps and guidance Changes: - Add BFL provider with polling-based async generation - Add BFL to Provider enum and credential system - Add new ImageGenerationParameter values (guidance, steps, seed, etc.) - Add billed_units field to ImageGenerationUsage - Add BFL to integration tests and CI workflow - Support BFL_API_KEY environment variable --- .env.example | 3 + .github/workflows/publish.yml | 1 + .../src/celeste_image_generation/io.py | 1 + .../src/celeste_image_generation/models.py | 2 + .../celeste_image_generation/parameters.py | 12 ++ .../providers/__init__.py | 4 + .../providers/bfl/__init__.py | 6 + .../providers/bfl/client.py | 146 +++++++++++++++ .../providers/bfl/config.py | 13 ++ .../providers/bfl/models.py | 65 +++++++ .../providers/bfl/parameters.py | 169 ++++++++++++++++++ .../test_image_generation/test_generate.py | 1 + src/celeste/core.py | 1 + src/celeste/credentials.py | 2 + 14 files changed, 426 insertions(+) create mode 100644 packages/image-generation/src/celeste_image_generation/providers/bfl/__init__.py create mode 100644 packages/image-generation/src/celeste_image_generation/providers/bfl/client.py create mode 100644 packages/image-generation/src/celeste_image_generation/providers/bfl/config.py create mode 100644 packages/image-generation/src/celeste_image_generation/providers/bfl/models.py create mode 100644 packages/image-generation/src/celeste_image_generation/providers/bfl/parameters.py diff --git a/.env.example b/.env.example index 5a15b666..87b3885f 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,9 @@ OPENAI_API_KEY=your-openai-api-key-here # Anthropic ANTHROPIC_API_KEY=your-anthropic-api-key-here +# BFL (Black Forest Labs) +BFL_API_KEY=your-bfl-api-key-here + # Google GOOGLE_API_KEY=your-google-api-key-here diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3ccc3e72..fb4ceb92 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,6 +55,7 @@ jobs: env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + BFL_API_KEY: ${{ secrets.BFL_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} diff --git a/packages/image-generation/src/celeste_image_generation/io.py b/packages/image-generation/src/celeste_image_generation/io.py index a110b551..90a86ec7 100644 --- a/packages/image-generation/src/celeste_image_generation/io.py +++ b/packages/image-generation/src/celeste_image_generation/io.py @@ -31,6 +31,7 @@ class ImageGenerationUsage(Usage): output_tokens: int | None = None reasoning_tokens: int | None = None generated_images: int | None = None + billed_units: float | None = None class ImageGenerationOutput(Output[ImageArtifact]): diff --git a/packages/image-generation/src/celeste_image_generation/models.py b/packages/image-generation/src/celeste_image_generation/models.py index f8ef9727..f25292e5 100644 --- a/packages/image-generation/src/celeste_image_generation/models.py +++ b/packages/image-generation/src/celeste_image_generation/models.py @@ -1,6 +1,7 @@ """Model definitions for image generation.""" 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, ) @@ -8,6 +9,7 @@ from celeste_image_generation.providers.openai.models import MODELS as OPENAI_MODELS MODELS: list[Model] = [ + *BFL_MODELS, *BYTEDANCE_MODELS, *GOOGLE_MODELS, *OPENAI_MODELS, diff --git a/packages/image-generation/src/celeste_image_generation/parameters.py b/packages/image-generation/src/celeste_image_generation/parameters.py index ba5ad249..eebe195a 100644 --- a/packages/image-generation/src/celeste_image_generation/parameters.py +++ b/packages/image-generation/src/celeste_image_generation/parameters.py @@ -9,8 +9,14 @@ class ImageGenerationParameter(StrEnum): """Unified parameter names for image generation capability.""" ASPECT_RATIO = "aspect_ratio" + GUIDANCE = "guidance" + OUTPUT_FORMAT = "output_format" PARTIAL_IMAGES = "partial_images" + PROMPT_UPSAMPLING = "prompt_upsampling" QUALITY = "quality" + SAFETY_TOLERANCE = "safety_tolerance" + SEED = "seed" + STEPS = "steps" WATERMARK = "watermark" @@ -18,6 +24,12 @@ class ImageGenerationParameters(Parameters): """Parameters for image generation.""" aspect_ratio: str | None + guidance: float | None + output_format: str | None partial_images: int | None + prompt_upsampling: bool | None quality: str | None + safety_tolerance: int | None + seed: int | None + steps: int | None watermark: bool | None diff --git a/packages/image-generation/src/celeste_image_generation/providers/__init__.py b/packages/image-generation/src/celeste_image_generation/providers/__init__.py index 1549ef52..fe9a8499 100644 --- a/packages/image-generation/src/celeste_image_generation/providers/__init__.py +++ b/packages/image-generation/src/celeste_image_generation/providers/__init__.py @@ -8,6 +8,9 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]: """Lazy-load providers.""" # Import clients directly from .client modules to avoid __init__.py imports + from celeste_image_generation.providers.bfl.client import ( + BFLImageGenerationClient, + ) from celeste_image_generation.providers.bytedance.client import ( ByteDanceImageGenerationClient, ) @@ -19,6 +22,7 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]: ) return [ + (Provider.BFL, BFLImageGenerationClient), (Provider.BYTEDANCE, ByteDanceImageGenerationClient), (Provider.GOOGLE, GoogleImageGenerationClient), (Provider.OPENAI, OpenAIImageGenerationClient), diff --git a/packages/image-generation/src/celeste_image_generation/providers/bfl/__init__.py b/packages/image-generation/src/celeste_image_generation/providers/bfl/__init__.py new file mode 100644 index 00000000..f82295b9 --- /dev/null +++ b/packages/image-generation/src/celeste_image_generation/providers/bfl/__init__.py @@ -0,0 +1,6 @@ +"""BFL (Black Forest Labs) provider for image generation.""" + +from .client import BFLImageGenerationClient +from .models import MODELS + +__all__ = ["MODELS", "BFLImageGenerationClient"] diff --git a/packages/image-generation/src/celeste_image_generation/providers/bfl/client.py b/packages/image-generation/src/celeste_image_generation/providers/bfl/client.py new file mode 100644 index 00000000..009aabfa --- /dev/null +++ b/packages/image-generation/src/celeste_image_generation/providers/bfl/client.py @@ -0,0 +1,146 @@ +"""BFL (Black Forest Labs) client implementation for FLUX.2 image generation.""" + +import asyncio +import time +from typing import Any, Unpack + +import httpx + +from celeste.artifacts import ImageArtifact +from celeste.mime_types import ApplicationMimeType +from celeste.parameters import ParameterMapper +from celeste_image_generation.client import ImageGenerationClient +from celeste_image_generation.io import ( + ImageGenerationFinishReason, + ImageGenerationInput, + ImageGenerationUsage, +) +from celeste_image_generation.parameters import ImageGenerationParameters + +from . import config +from .parameters import BFL_PARAMETER_MAPPERS + + +class BFLImageGenerationClient(ImageGenerationClient): + """Black Forest Labs client for image generation.""" + + @classmethod + def parameter_mappers(cls) -> list[ParameterMapper]: + return BFL_PARAMETER_MAPPERS + + def _init_request(self, inputs: ImageGenerationInput) -> dict[str, Any]: + """Initialize request for BFL API format.""" + return { + "prompt": inputs.prompt, + } + + def _parse_usage(self, response_data: dict[str, Any]) -> ImageGenerationUsage: + """Parse usage from response.""" + submit_metadata = response_data.get("_submit_metadata", {}) + cost = submit_metadata.get("cost") + + return ImageGenerationUsage( + billed_units=float(cost) if cost is not None else None, + ) + + def _parse_content( + self, + response_data: dict[str, Any], + **parameters: Unpack[ImageGenerationParameters], + ) -> ImageArtifact: + """Parse content from response.""" + result = response_data.get("result", {}) + sample_url = result.get("sample") + + if not sample_url: + msg = f"No image URL in {self.provider} response" + raise ValueError(msg) + + return ImageArtifact(url=sample_url) + + def _parse_finish_reason( + self, response_data: dict[str, Any] + ) -> ImageGenerationFinishReason | None: + """Parse finish reason from response.""" + status = response_data.get("status") + if status == "Ready": + return ImageGenerationFinishReason(reason="COMPLETE") + elif status in ("Error", "Failed"): + error_msg = response_data.get("error", "Generation failed") + return ImageGenerationFinishReason(reason="ERROR", message=error_msg) + return None + + async def _make_request( + self, + request_body: dict[str, Any], + **parameters: Unpack[ImageGenerationParameters], + ) -> httpx.Response: + """Make HTTP request(s) and return response object.""" + headers = { + config.AUTH_HEADER_NAME: self.api_key.get_secret_value(), + "Content-Type": ApplicationMimeType.JSON, + "Accept": ApplicationMimeType.JSON, + } + + endpoint = config.ENDPOINT.format(model_id=self.model.id) + + submit_response = await self.http_client.post( + f"{config.BASE_URL}{endpoint}", + headers=headers, + json_body=request_body, + ) + + if submit_response.status_code != 200: + return submit_response + + submit_data = submit_response.json() + polling_url = submit_data.get("polling_url") + + if not polling_url: + msg = f"No polling_url in {self.provider} response" + raise ValueError(msg) + + start_time = time.monotonic() + poll_headers = { + config.AUTH_HEADER_NAME: self.api_key.get_secret_value(), + "Accept": ApplicationMimeType.JSON, + } + + while True: + elapsed = time.monotonic() - start_time + if elapsed >= config.POLLING_TIMEOUT: + msg = f"{self.provider} polling timed out after {config.POLLING_TIMEOUT} seconds" + raise TimeoutError(msg) + + poll_response = await self.http_client.get( + polling_url, + headers=poll_headers, + ) + + if poll_response.status_code != 200: + return poll_response + + poll_data = poll_response.json() + status = poll_data.get("status") + + if status == "Ready": + final_data = { + **poll_data, + "_submit_metadata": submit_data, + } + return httpx.Response( + status_code=200, + json=final_data, + request=httpx.Request("GET", polling_url), + ) + elif status in ("Error", "Failed"): + return httpx.Response( + status_code=400, + json=poll_data, + request=httpx.Request("GET", polling_url), + ) + + await asyncio.sleep(config.POLLING_INTERVAL) + + +__all__ = ["BFLImageGenerationClient"] diff --git a/packages/image-generation/src/celeste_image_generation/providers/bfl/config.py b/packages/image-generation/src/celeste_image_generation/providers/bfl/config.py new file mode 100644 index 00000000..86d7bbf3 --- /dev/null +++ b/packages/image-generation/src/celeste_image_generation/providers/bfl/config.py @@ -0,0 +1,13 @@ +"""BFL (Black Forest Labs) provider configuration for image generation.""" + +# HTTP Configuration +BASE_URL = "https://api.bfl.ai" +ENDPOINT = "/v1/{model_id}" + +# Authentication +AUTH_HEADER_NAME = "x-key" +AUTH_HEADER_PREFIX = "" # Direct API key, no prefix + +# Polling Configuration +POLLING_INTERVAL = 0.5 # seconds between polling attempts +POLLING_TIMEOUT = 120.0 # 2 minutes for image generation diff --git a/packages/image-generation/src/celeste_image_generation/providers/bfl/models.py b/packages/image-generation/src/celeste_image_generation/providers/bfl/models.py new file mode 100644 index 00000000..4f7d80f5 --- /dev/null +++ b/packages/image-generation/src/celeste_image_generation/providers/bfl/models.py @@ -0,0 +1,65 @@ +"""BFL (Black Forest Labs) models for FLUX.2 image generation.""" + +from celeste import Model, Provider +from celeste.constraints import Bool, Choice, Int, Range +from celeste_image_generation.constraints import Dimensions +from celeste_image_generation.parameters import ImageGenerationParameter + +MODELS: list[Model] = [ + Model( + id="flux-2-pro", + provider=Provider.BFL, + display_name="FLUX.2 [pro]", + parameter_constraints={ + ImageGenerationParameter.ASPECT_RATIO: Dimensions( + min_pixels=64 * 64, # 4,096 + max_pixels=2048 * 2048, # 4,194,304 (4MP) + min_aspect_ratio=9 / 21, # ~0.429 + max_aspect_ratio=21 / 9, # ~2.333 + presets={ + "Square 1K": "1024x1024", + "Square 2K": "2048x2048", + "HD 16:9": "1920x1080", + "Portrait HD": "1080x1920", + "4:3": "1280x960", + "3:4": "960x1280", + "Ultra-wide 21:9": "1920x832", + "Portrait 9:21": "832x1920", + }, + ), + # Note: flux-2-pro always upsamples prompts (no prompt_upsampling parameter) + ImageGenerationParameter.SEED: Int(), + ImageGenerationParameter.SAFETY_TOLERANCE: Range(min=0, max=5), + ImageGenerationParameter.OUTPUT_FORMAT: Choice(options=["jpeg", "png"]), + }, + ), + Model( + id="flux-2-flex", + provider=Provider.BFL, + display_name="FLUX.2 [flex]", + parameter_constraints={ + ImageGenerationParameter.ASPECT_RATIO: Dimensions( + min_pixels=64 * 64, # 4,096 + max_pixels=2048 * 2048, # 4,194,304 (4MP) + min_aspect_ratio=9 / 21, # ~0.429 + max_aspect_ratio=21 / 9, # ~2.333 + presets={ + "Square 1K": "1024x1024", + "Square 2K": "2048x2048", + "HD 16:9": "1920x1080", + "Portrait HD": "1080x1920", + "4:3": "1280x960", + "3:4": "960x1280", + "Ultra-wide 21:9": "1920x832", + "Portrait 9:21": "832x1920", + }, + ), + ImageGenerationParameter.PROMPT_UPSAMPLING: Bool(), + ImageGenerationParameter.SEED: Int(), + ImageGenerationParameter.SAFETY_TOLERANCE: Range(min=0, max=5), + ImageGenerationParameter.OUTPUT_FORMAT: Choice(options=["jpeg", "png"]), + ImageGenerationParameter.STEPS: Range(min=1, max=50), + ImageGenerationParameter.GUIDANCE: Range(min=1.5, max=10.0), + }, + ), +] diff --git a/packages/image-generation/src/celeste_image_generation/providers/bfl/parameters.py b/packages/image-generation/src/celeste_image_generation/providers/bfl/parameters.py new file mode 100644 index 00000000..64781148 --- /dev/null +++ b/packages/image-generation/src/celeste_image_generation/providers/bfl/parameters.py @@ -0,0 +1,169 @@ +"""BFL parameter mappers for image generation.""" + +from typing import Any + +from celeste import Model +from celeste.parameters import ParameterMapper +from celeste_image_generation.parameters import ImageGenerationParameter + + +class AspectRatioMapper(ParameterMapper): + """Map aspect_ratio to BFL width/height parameters.""" + + name = ImageGenerationParameter.ASPECT_RATIO + + 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 + + parts = validated_value.split("x") + width = int(parts[0]) + height = int(parts[1]) + + # Round to nearest multiple of 16 as required by BFL API + width = ((width + 8) // 16) * 16 + height = ((height + 8) // 16) * 16 + + request["width"] = width + request["height"] = height + return request + + +class PromptUpsamplingMapper(ParameterMapper): + """Map prompt_upsampling parameter to BFL request format.""" + + name = ImageGenerationParameter.PROMPT_UPSAMPLING + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform prompt_upsampling into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["prompt_upsampling"] = validated_value + return request + + +class SeedMapper(ParameterMapper): + """Map seed parameter to BFL request format.""" + + name = ImageGenerationParameter.SEED + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform seed into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["seed"] = validated_value + return request + + +class SafetyToleranceMapper(ParameterMapper): + """Map safety_tolerance parameter to BFL request format.""" + + name = ImageGenerationParameter.SAFETY_TOLERANCE + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform safety_tolerance into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["safety_tolerance"] = validated_value + return request + + +class OutputFormatMapper(ParameterMapper): + """Map output_format parameter to BFL request format.""" + + name = ImageGenerationParameter.OUTPUT_FORMAT + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform output_format into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["output_format"] = validated_value + return request + + +class StepsMapper(ParameterMapper): + """Map steps parameter to BFL request format (flex only).""" + + name = ImageGenerationParameter.STEPS + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform steps into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["steps"] = validated_value + return request + + +class GuidanceMapper(ParameterMapper): + """Map guidance parameter to BFL request format (flex only).""" + + name = ImageGenerationParameter.GUIDANCE + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform guidance into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["guidance"] = validated_value + return request + + +BFL_PARAMETER_MAPPERS: list[ParameterMapper] = [ + AspectRatioMapper(), + PromptUpsamplingMapper(), + SeedMapper(), + SafetyToleranceMapper(), + OutputFormatMapper(), + StepsMapper(), + GuidanceMapper(), +] + +__all__ = ["BFL_PARAMETER_MAPPERS"] diff --git a/packages/image-generation/tests/integration_tests/test_image_generation/test_generate.py b/packages/image-generation/tests/integration_tests/test_image_generation/test_generate.py index 73dd9150..0f783cf7 100644 --- a/packages/image-generation/tests/integration_tests/test_image_generation/test_generate.py +++ b/packages/image-generation/tests/integration_tests/test_image_generation/test_generate.py @@ -8,6 +8,7 @@ @pytest.mark.parametrize( ("provider", "model", "parameters"), [ + (Provider.BFL, "flux-2-pro", {"aspect_ratio": "1024x1024"}), (Provider.OPENAI, "dall-e-2", {}), (Provider.GOOGLE, "imagen-4.0-fast-generate-001", {}), (Provider.BYTEDANCE, "seedream-4-0-250828", {}), diff --git a/src/celeste/core.py b/src/celeste/core.py index 11194ce7..8165702a 100644 --- a/src/celeste/core.py +++ b/src/celeste/core.py @@ -8,6 +8,7 @@ class Provider(StrEnum): OPENAI = "openai" ANTHROPIC = "anthropic" + BFL = "bfl" GOOGLE = "google" MISTRAL = "mistral" COHERE = "cohere" diff --git a/src/celeste/credentials.py b/src/celeste/credentials.py index f61ede4d..39949e96 100644 --- a/src/celeste/credentials.py +++ b/src/celeste/credentials.py @@ -11,6 +11,7 @@ PROVIDER_CREDENTIAL_MAP = { Provider.OPENAI: "openai_api_key", Provider.ANTHROPIC: "anthropic_api_key", + Provider.BFL: "bfl_api_key", Provider.GOOGLE: "google_api_key", Provider.MISTRAL: "mistral_api_key", Provider.HUGGINGFACE: "huggingface_token", @@ -31,6 +32,7 @@ class Credentials(BaseSettings): openai_api_key: SecretStr | None = Field(None, alias="OPENAI_API_KEY") anthropic_api_key: SecretStr | None = Field(None, alias="ANTHROPIC_API_KEY") + bfl_api_key: SecretStr | None = Field(None, alias="BFL_API_KEY") google_api_key: SecretStr | None = Field(None, alias="GOOGLE_API_KEY") mistral_api_key: SecretStr | None = Field(None, alias="MISTRAL_API_KEY") huggingface_token: SecretStr | None = Field(None, alias="HUGGINGFACE_TOKEN") From 9a61972070bd11a236aade0c8d8454b0f6308662 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 1 Dec 2025 23:07:01 +0100 Subject: [PATCH 2/3] chore: bump versions for BFL provider release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - celeste-ai: 0.2.11 → 0.2.12 - celeste-image-generation: 0.2.9 → 0.2.10 --- packages/image-generation/pyproject.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/image-generation/pyproject.toml b/packages/image-generation/pyproject.toml index 468c28a6..96f2cf7e 100644 --- a/packages/image-generation/pyproject.toml +++ b/packages/image-generation/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "celeste-image-generation" -version = "0.2.9" +version = "0.2.10" description = "Image generation package for Celeste AI. Unified interface for all providers" authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}] readme = "README.md" diff --git a/pyproject.toml b/pyproject.toml index 12717e26..d40a1a91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "celeste-ai" -version = "0.2.11" +version = "0.2.12" description = "Open source, type-safe primitives for multi-modal AI. All capabilities, all providers, one interface" authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}] readme = "README.md" From 58a04102ef11ed8488acbc178f7fbbcf9c4e8478 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 1 Dec 2025 23:22:15 +0100 Subject: [PATCH 3/3] fix: use content param for httpx.Response construction httpx.Response doesn't accept json parameter - use content with JSON-encoded bytes instead. --- .../src/celeste_image_generation/providers/bfl/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/image-generation/src/celeste_image_generation/providers/bfl/client.py b/packages/image-generation/src/celeste_image_generation/providers/bfl/client.py index 009aabfa..92cd1b53 100644 --- a/packages/image-generation/src/celeste_image_generation/providers/bfl/client.py +++ b/packages/image-generation/src/celeste_image_generation/providers/bfl/client.py @@ -1,6 +1,7 @@ """BFL (Black Forest Labs) client implementation for FLUX.2 image generation.""" import asyncio +import json import time from typing import Any, Unpack @@ -130,13 +131,15 @@ async def _make_request( } return httpx.Response( status_code=200, - json=final_data, + content=json.dumps(final_data).encode("utf-8"), + headers={"content-type": "application/json"}, request=httpx.Request("GET", polling_url), ) elif status in ("Error", "Failed"): return httpx.Response( status_code=400, - json=poll_data, + content=json.dumps(poll_data).encode("utf-8"), + headers={"content-type": "application/json"}, request=httpx.Request("GET", polling_url), )