From 546edaf3068d90c58822213919dd4631801ca276 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 24 Nov 2025 15:54:14 +0100 Subject: [PATCH 1/2] feat: add XAI provider for text generation - Add XAI provider implementation - Integrate XAI tests into main test suites (test_generate, test_stream) - Use grok-3-mini model for cost-effective testing --- packages/image-generation/README.md | 2 +- .../src/celeste_text_generation/models.py | 2 + .../providers/__init__.py | 4 + .../providers/mistral/config.py | 2 +- .../providers/xai/__init__.py | 7 + .../providers/xai/client.py | 142 ++++++++++++ .../providers/xai/config.py | 10 + .../providers/xai/models.py | 76 +++++++ .../providers/xai/parameters.py | 214 ++++++++++++++++++ .../providers/xai/streaming.py | 134 +++++++++++ .../test_text_generation/test_generate.py | 1 + .../test_text_generation/test_stream.py | 1 + packages/video-generation/README.md | 2 +- 13 files changed, 594 insertions(+), 3 deletions(-) create mode 100644 packages/text-generation/src/celeste_text_generation/providers/xai/__init__.py create mode 100644 packages/text-generation/src/celeste_text_generation/providers/xai/client.py create mode 100644 packages/text-generation/src/celeste_text_generation/providers/xai/config.py create mode 100644 packages/text-generation/src/celeste_text_generation/providers/xai/models.py create mode 100644 packages/text-generation/src/celeste_text_generation/providers/xai/parameters.py create mode 100644 packages/text-generation/src/celeste_text_generation/providers/xai/streaming.py diff --git a/packages/image-generation/README.md b/packages/image-generation/README.md index bc87b368..44c67b6e 100644 --- a/packages/image-generation/README.md +++ b/packages/image-generation/README.md @@ -41,7 +41,7 @@ uv add "celeste-ai[image-generation]" OpenAI Google -ByteDance +ByteDance **Missing a provider?** [Request it](https://github.com/withceleste/celeste-python/issues/new) – ⚡ **we ship fast**. diff --git a/packages/text-generation/src/celeste_text_generation/models.py b/packages/text-generation/src/celeste_text_generation/models.py index 899a4260..63081f98 100644 --- a/packages/text-generation/src/celeste_text_generation/models.py +++ b/packages/text-generation/src/celeste_text_generation/models.py @@ -8,6 +8,7 @@ from celeste_text_generation.providers.google.models import MODELS as GOOGLE_MODELS from celeste_text_generation.providers.mistral.models import MODELS as MISTRAL_MODELS from celeste_text_generation.providers.openai.models import MODELS as OPENAI_MODELS +from celeste_text_generation.providers.xai.models import MODELS as XAI_MODELS MODELS: list[Model] = [ *ANTHROPIC_MODELS, @@ -15,4 +16,5 @@ *GOOGLE_MODELS, *MISTRAL_MODELS, *OPENAI_MODELS, + *XAI_MODELS, ] diff --git a/packages/text-generation/src/celeste_text_generation/providers/__init__.py b/packages/text-generation/src/celeste_text_generation/providers/__init__.py index 96600e12..36b5d55f 100644 --- a/packages/text-generation/src/celeste_text_generation/providers/__init__.py +++ b/packages/text-generation/src/celeste_text_generation/providers/__init__.py @@ -23,6 +23,9 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]: from celeste_text_generation.providers.openai.client import ( OpenAITextGenerationClient, ) + from celeste_text_generation.providers.xai.client import ( + XAITextGenerationClient, + ) return [ (Provider.ANTHROPIC, AnthropicTextGenerationClient), @@ -30,6 +33,7 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]: (Provider.GOOGLE, GoogleTextGenerationClient), (Provider.MISTRAL, MistralTextGenerationClient), (Provider.OPENAI, OpenAITextGenerationClient), + (Provider.XAI, XAITextGenerationClient), ] diff --git a/packages/text-generation/src/celeste_text_generation/providers/mistral/config.py b/packages/text-generation/src/celeste_text_generation/providers/mistral/config.py index e889c537..0d9030a8 100644 --- a/packages/text-generation/src/celeste_text_generation/providers/mistral/config.py +++ b/packages/text-generation/src/celeste_text_generation/providers/mistral/config.py @@ -3,7 +3,7 @@ # HTTP Configuration BASE_URL = "https://api.mistral.ai" ENDPOINT = "/v1/chat/completions" -STREAM_ENDPOINT = ENDPOINT # Same endpoint +STREAM_ENDPOINT = ENDPOINT # Authentication AUTH_HEADER_NAME = "Authorization" diff --git a/packages/text-generation/src/celeste_text_generation/providers/xai/__init__.py b/packages/text-generation/src/celeste_text_generation/providers/xai/__init__.py new file mode 100644 index 00000000..61c3ce1e --- /dev/null +++ b/packages/text-generation/src/celeste_text_generation/providers/xai/__init__.py @@ -0,0 +1,7 @@ +"""XAI provider for text generation.""" + +from .client import XAITextGenerationClient +from .models import MODELS +from .streaming import XAITextGenerationStream + +__all__ = ["MODELS", "XAITextGenerationClient", "XAITextGenerationStream"] diff --git a/packages/text-generation/src/celeste_text_generation/providers/xai/client.py b/packages/text-generation/src/celeste_text_generation/providers/xai/client.py new file mode 100644 index 00000000..0e5a3b3a --- /dev/null +++ b/packages/text-generation/src/celeste_text_generation/providers/xai/client.py @@ -0,0 +1,142 @@ +"""XAI client implementation for text generation.""" + +from collections.abc import AsyncIterator +from typing import Any, Unpack + +import httpx +from pydantic import BaseModel + +from celeste.mime_types import ApplicationMimeType +from celeste.parameters import ParameterMapper +from celeste_text_generation.client import TextGenerationClient +from celeste_text_generation.io import ( + TextGenerationFinishReason, + TextGenerationInput, + TextGenerationUsage, +) +from celeste_text_generation.parameters import TextGenerationParameters + +from . import config +from .parameters import XAI_PARAMETER_MAPPERS +from .streaming import XAITextGenerationStream + + +class XAITextGenerationClient(TextGenerationClient): + """XAI client for text generation.""" + + @classmethod + def parameter_mappers(cls) -> list[ParameterMapper]: + return XAI_PARAMETER_MAPPERS + + def _init_request(self, inputs: TextGenerationInput) -> dict[str, Any]: + """Initialize request from XAI messages array format.""" + messages = [ + { + "role": "user", + "content": inputs.prompt, + } + ] + + return {"messages": messages} + + def _parse_usage(self, response_data: dict[str, Any]) -> TextGenerationUsage: + """Parse usage from response.""" + usage_data = response_data.get("usage", {}) + prompt_tokens_details = usage_data.get("prompt_tokens_details", {}) + completion_tokens_details = usage_data.get("completion_tokens_details", {}) + + return TextGenerationUsage( + input_tokens=usage_data.get("prompt_tokens"), + output_tokens=usage_data.get("completion_tokens"), + total_tokens=usage_data.get("total_tokens"), + cached_tokens=prompt_tokens_details.get("cached_tokens"), + reasoning_tokens=completion_tokens_details.get("reasoning_tokens"), + billed_tokens=None, + ) + + def _parse_content( + self, + response_data: dict[str, Any], + **parameters: Unpack[TextGenerationParameters], + ) -> str | BaseModel: + """Parse content from response.""" + choices = response_data.get("choices", []) + if not choices: + msg = "No choices in response" + raise ValueError(msg) + + message = choices[0].get("message", {}) + content = message.get("content") or "" + + return self._transform_output(content, **parameters) + + def _parse_finish_reason( + self, response_data: dict[str, Any] + ) -> TextGenerationFinishReason | None: + """Parse finish reason from response.""" + choices = response_data.get("choices", []) + if not choices: + return None + + choice = choices[0] + finish_reason = choice.get("finish_reason") + + if not finish_reason: + return None + + return TextGenerationFinishReason(reason=finish_reason) + + def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]: + """Build metadata dictionary from response data.""" + # Filter content field before calling super + content_fields = {"choices"} + filtered_data = { + k: v for k, v in response_data.items() if k not in content_fields + } + return super()._build_metadata(filtered_data) + + async def _make_request( + self, + request_body: dict[str, Any], + **parameters: Unpack[TextGenerationParameters], + ) -> httpx.Response: + """Make HTTP request(s) and return response object.""" + request_body["model"] = self.model.id + + headers = { + config.AUTH_HEADER_NAME: f"{config.AUTH_HEADER_PREFIX}{self.api_key.get_secret_value()}", + "Content-Type": ApplicationMimeType.JSON, + } + + return await self.http_client.post( + f"{config.BASE_URL}{config.ENDPOINT}", + headers=headers, + json_body=request_body, + ) + + def _stream_class(self) -> type[XAITextGenerationStream]: + """Return the Stream class for this client.""" + return XAITextGenerationStream + + def _make_stream_request( + self, + request_body: dict[str, Any], + **parameters: Unpack[TextGenerationParameters], + ) -> AsyncIterator[dict[str, Any]]: + """Make HTTP streaming request and return async iterator of events.""" + request_body["model"] = self.model.id + request_body["stream"] = True + + headers = { + config.AUTH_HEADER_NAME: f"{config.AUTH_HEADER_PREFIX}{self.api_key.get_secret_value()}", + "Content-Type": ApplicationMimeType.JSON, + } + + return self.http_client.stream_post( + f"{config.BASE_URL}{config.STREAM_ENDPOINT}", + headers=headers, + json_body=request_body, + ) + + +__all__ = ["XAITextGenerationClient"] diff --git a/packages/text-generation/src/celeste_text_generation/providers/xai/config.py b/packages/text-generation/src/celeste_text_generation/providers/xai/config.py new file mode 100644 index 00000000..04472e3b --- /dev/null +++ b/packages/text-generation/src/celeste_text_generation/providers/xai/config.py @@ -0,0 +1,10 @@ +"""XAI provider configuration for text generation.""" + +# HTTP Configuration +BASE_URL = "https://api.x.ai/v1" +ENDPOINT = "/chat/completions" +STREAM_ENDPOINT = ENDPOINT + +# Authentication +AUTH_HEADER_NAME = "Authorization" +AUTH_HEADER_PREFIX = "Bearer " diff --git a/packages/text-generation/src/celeste_text_generation/providers/xai/models.py b/packages/text-generation/src/celeste_text_generation/providers/xai/models.py new file mode 100644 index 00000000..449e66e6 --- /dev/null +++ b/packages/text-generation/src/celeste_text_generation/providers/xai/models.py @@ -0,0 +1,76 @@ +"""XAI models for text generation.""" + +from celeste import Model, Provider +from celeste.constraints import Choice, Range, Schema +from celeste.core import Parameter +from celeste_text_generation.parameters import TextGenerationParameter + +MODELS: list[Model] = [ + Model( + id="grok-4-1-fast-reasoning", + provider=Provider.XAI, + display_name="Grok 4.1 Fast Reasoning", + streaming=True, + parameter_constraints={ + Parameter.TEMPERATURE: Range(min=0.0, max=2.0), + Parameter.MAX_TOKENS: Range(min=1, max=30000), + TextGenerationParameter.OUTPUT_SCHEMA: Schema(), + }, + ), + Model( + id="grok-4-1-fast-non-reasoning", + provider=Provider.XAI, + display_name="Grok 4.1 Fast Non-Reasoning", + streaming=True, + parameter_constraints={ + Parameter.TEMPERATURE: Range(min=0.0, max=2.0), + Parameter.MAX_TOKENS: Range(min=1, max=30000), + TextGenerationParameter.OUTPUT_SCHEMA: Schema(), + }, + ), + Model( + id="grok-4-fast-reasoning", + provider=Provider.XAI, + display_name="Grok 4 Fast Reasoning", + streaming=True, + parameter_constraints={ + Parameter.TEMPERATURE: Range(min=0.0, max=2.0), + Parameter.MAX_TOKENS: Range(min=1, max=30000), + TextGenerationParameter.OUTPUT_SCHEMA: Schema(), + }, + ), + Model( + id="grok-4-fast-non-reasoning", + provider=Provider.XAI, + display_name="Grok 4 Fast Non-Reasoning", + streaming=True, + parameter_constraints={ + Parameter.TEMPERATURE: Range(min=0.0, max=2.0), + Parameter.MAX_TOKENS: Range(min=1, max=30000), + TextGenerationParameter.OUTPUT_SCHEMA: Schema(), + }, + ), + Model( + id="grok-4-0709", + provider=Provider.XAI, + display_name="Grok 4", + streaming=True, + parameter_constraints={ + Parameter.TEMPERATURE: Range(min=0.0, max=2.0), + Parameter.MAX_TOKENS: Range(min=1, max=64000), + TextGenerationParameter.OUTPUT_SCHEMA: Schema(), + }, + ), + Model( + id="grok-3-mini", + provider=Provider.XAI, + display_name="Grok 3 Mini", + streaming=True, + parameter_constraints={ + Parameter.TEMPERATURE: Range(min=0.0, max=2.0), + Parameter.MAX_TOKENS: Range(min=1, max=16000), + TextGenerationParameter.THINKING_LEVEL: Choice(options=["low", "high"]), + TextGenerationParameter.OUTPUT_SCHEMA: Schema(), + }, + ), +] diff --git a/packages/text-generation/src/celeste_text_generation/providers/xai/parameters.py b/packages/text-generation/src/celeste_text_generation/providers/xai/parameters.py new file mode 100644 index 00000000..903f6bfb --- /dev/null +++ b/packages/text-generation/src/celeste_text_generation/providers/xai/parameters.py @@ -0,0 +1,214 @@ +"""XAI parameter mappers for text generation.""" + +import json +from typing import Any, get_args, get_origin + +from pydantic import BaseModel, TypeAdapter + +from celeste.core import Parameter +from celeste.models import Model +from celeste.parameters import ParameterMapper +from celeste_text_generation.parameters import TextGenerationParameter + + +class OutputSchemaMapper(ParameterMapper): + """Map output_schema parameter to XAI response_format.""" + + name = TextGenerationParameter.OUTPUT_SCHEMA + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform output_schema into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + schema = self._convert_to_json_schema(validated_value) + schema_name = self._get_schema_name(validated_value) + + request["response_format"] = { + "type": "json_schema", + "json_schema": { + "name": schema_name, + "strict": True, + "schema": schema, + }, + } + + return request + + def parse_output(self, content: str, value: object | None) -> str | BaseModel: + """Parse JSON string to BaseModel instance if output_schema provided.""" + if value is None: + return content + + parsed_json = json.loads(content) + origin = get_origin(value) + if origin is list and isinstance(parsed_json, dict) and "items" in parsed_json: + parsed_json = parsed_json["items"] + + return TypeAdapter(value).validate_json(json.dumps(parsed_json)) + + def _convert_to_json_schema(self, output_schema: Any) -> dict[str, Any]: # noqa: ANN401 + """Convert Pydantic BaseModel or list[BaseModel] to JSON Schema format.""" + origin = get_origin(output_schema) + if origin is list: + inner_type = get_args(output_schema)[0] + items_schema = inner_type.model_json_schema() + json_schema = { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": items_schema, + } + }, + "required": ["items"], + } + else: + json_schema = output_schema.model_json_schema() + + json_schema = self._transform_schema(json_schema) + return json_schema + + def _transform_schema( + self, schema: dict[str, Any], defs: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Recursively transform schema for API compatibility.""" + if not isinstance(schema, dict): + return schema + + if defs is None: + defs = self._collect_all_defs(schema) + + if "$ref" in schema: + ref_path = schema["$ref"] + if ref_path.startswith("#/$defs/"): + def_name = ref_path.split("/")[-1] + if def_name in defs: + expanded = defs[def_name].copy() + expanded.pop("description", None) + return self._transform_schema(expanded, defs) + return schema + + result: dict[str, Any] = {} + for key, value in schema.items(): + if key == "$defs": + continue + elif isinstance(value, dict): + result[key] = self._transform_schema(value, defs) + elif isinstance(value, list): + result[key] = [ + self._transform_schema(item, defs) + if isinstance(item, dict) + else item + for item in value + ] + else: + result[key] = value + + if result.get("type") == "object": + result["additionalProperties"] = False + + return result + + def _collect_all_defs(self, schema: Any) -> dict[str, Any]: # noqa: ANN401 + """Recursively collect all $defs dictionaries from schema tree.""" + defs: dict[str, Any] = {} + + def collect(value: Any) -> None: # noqa: ANN401 + if isinstance(value, dict): + if "$defs" in value: + defs.update(value["$defs"]) + for v in value.values(): + collect(v) + elif isinstance(value, list): + for item in value: + collect(item) + + collect(schema) + return defs + + def _get_schema_name(self, output_schema: Any) -> str: # noqa: ANN401 + """Derive schema name from model class name.""" + origin = get_origin(output_schema) + if origin is list: + inner_type = get_args(output_schema)[0] + class_name = inner_type.__name__ + return f"{class_name.lower()}_list" + else: + return output_schema.__name__.lower() + + +class TemperatureMapper(ParameterMapper): + """Map temperature parameter to XAI temperature field.""" + + name = Parameter.TEMPERATURE + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform temperature into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["temperature"] = validated_value + return request + + +class MaxTokensMapper(ParameterMapper): + """Map max_tokens parameter to XAI max_tokens field.""" + + name = Parameter.MAX_TOKENS + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform max_tokens into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["max_tokens"] = validated_value + return request + + +class ThinkingLevelMapper(ParameterMapper): + """Map thinking_level parameter to XAI reasoning_effort field.""" + + name = TextGenerationParameter.THINKING_LEVEL + + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Transform thinking_level into provider request.""" + validated_value = self._validate_value(value, model) + if validated_value is None: + return request + + request["reasoning_effort"] = validated_value + return request + + +XAI_PARAMETER_MAPPERS: list[ParameterMapper] = [ + TemperatureMapper(), + MaxTokensMapper(), + ThinkingLevelMapper(), + OutputSchemaMapper(), +] + +__all__ = ["XAI_PARAMETER_MAPPERS"] diff --git a/packages/text-generation/src/celeste_text_generation/providers/xai/streaming.py b/packages/text-generation/src/celeste_text_generation/providers/xai/streaming.py new file mode 100644 index 00000000..1d1a74c7 --- /dev/null +++ b/packages/text-generation/src/celeste_text_generation/providers/xai/streaming.py @@ -0,0 +1,134 @@ +"""XAI streaming for text generation.""" + +from collections.abc import Callable +from typing import Any, Unpack + +from celeste_text_generation.io import ( + TextGenerationChunk, + TextGenerationFinishReason, + TextGenerationOutput, + TextGenerationUsage, +) +from celeste_text_generation.parameters import TextGenerationParameters +from celeste_text_generation.streaming import TextGenerationStream + + +class XAITextGenerationStream(TextGenerationStream): + """XAI streaming for text generation.""" + + def __init__( + self, + sse_iterator: Any, # noqa: ANN401 + transform_output: Callable[..., object], + **parameters: Unpack[TextGenerationParameters], + ) -> None: + """Initialize stream with output transformation support. + + Args: + sse_iterator: Server-Sent Events iterator. + transform_output: Function to transform accumulated content (e.g., JSON → BaseModel). + **parameters: Parameters passed to stream() for output transformation. + """ + super().__init__(sse_iterator, **parameters) + self._transform_output = transform_output + + def _parse_chunk(self, event: dict[str, Any]) -> TextGenerationChunk | None: + """Parse chunk from SSE event. + + Extract from choices[0].delta.content (content delta events). + Extract finish_reason and usage from final event when finish_reason is not null. + Return None if no text delta (filter lifecycle events). + """ + choices = event.get("choices", []) + if not choices: + return None + + first_choice = choices[0] + if not isinstance(first_choice, dict): + return None + + delta = first_choice.get("delta", {}) + if not isinstance(delta, dict): + return None + + # Extract content delta + content_delta = delta.get("content") + finish_reason_str = first_choice.get("finish_reason") + + # Extract usage from event if present (in final event) + usage = None + usage_dict = event.get("usage") + if isinstance(usage_dict, dict): + prompt_tokens_details = usage_dict.get("prompt_tokens_details", {}) + completion_tokens_details = usage_dict.get("completion_tokens_details", {}) + + usage = TextGenerationUsage( + input_tokens=usage_dict.get("prompt_tokens"), + output_tokens=usage_dict.get("completion_tokens"), + total_tokens=usage_dict.get("total_tokens"), + cached_tokens=prompt_tokens_details.get("cached_tokens"), + reasoning_tokens=completion_tokens_details.get("reasoning_tokens"), + billed_tokens=None, + ) + + # Create finish reason if present + finish_reason = ( + TextGenerationFinishReason(reason=finish_reason_str) + if finish_reason_str + else None + ) + + # If no content delta and no finish reason, filter this event + if not content_delta and not finish_reason: + return None + + return TextGenerationChunk( + content=content_delta or "", # Empty string if no content (final event) + finish_reason=finish_reason, + usage=usage, + ) + + def _parse_usage(self, chunks: list[TextGenerationChunk]) -> TextGenerationUsage: + """Parse usage from chunks. + + XAI provides usage metadata in the final event (when finish_reason is not null). + Use the last chunk that has usage metadata. + """ + if not chunks: + return TextGenerationUsage() + + # Usage metadata is typically in the final chunk (when finish_reason is set) + for chunk in reversed(chunks): + if chunk.usage: + return chunk.usage + + return TextGenerationUsage() + + def _parse_output( + self, + chunks: list[TextGenerationChunk], + **parameters: Unpack[TextGenerationParameters], + ) -> TextGenerationOutput: + """Assemble chunks into final output with structured output support. + + Concatenates text chunks, then applies parameter transformations + (e.g., JSON → BaseModel if output_schema provided). + """ + # Filter out empty chunks (from final events) + content_chunks = [chunk for chunk in chunks if chunk.content] + + # Concatenate text chunks + content = "".join(chunk.content for chunk in content_chunks) + + # Apply parameter transformations (e.g., JSON → BaseModel if output_schema provided) + content = self._transform_output(content, **parameters) + + usage = self._parse_usage(chunks) + finish_reason = chunks[-1].finish_reason if chunks else None + + return TextGenerationOutput( + content=content, + usage=usage, + finish_reason=finish_reason, + metadata={}, + ) diff --git a/packages/text-generation/tests/integration_tests/test_text_generation/test_generate.py b/packages/text-generation/tests/integration_tests/test_text_generation/test_generate.py index 7dcde54f..d3f57e15 100644 --- a/packages/text-generation/tests/integration_tests/test_text_generation/test_generate.py +++ b/packages/text-generation/tests/integration_tests/test_text_generation/test_generate.py @@ -13,6 +13,7 @@ (Provider.GOOGLE, "gemini-2.5-flash-lite", {"thinking_budget": 0}), (Provider.MISTRAL, "mistral-tiny", {}), (Provider.COHERE, "command-a-03-2025", {}), + (Provider.XAI, "grok-3-mini", {}), ], ) @pytest.mark.integration diff --git a/packages/text-generation/tests/integration_tests/test_text_generation/test_stream.py b/packages/text-generation/tests/integration_tests/test_text_generation/test_stream.py index 9fcbc8d1..b67b1ad6 100644 --- a/packages/text-generation/tests/integration_tests/test_text_generation/test_stream.py +++ b/packages/text-generation/tests/integration_tests/test_text_generation/test_stream.py @@ -13,6 +13,7 @@ (Provider.GOOGLE, "gemini-2.5-flash-lite", {"thinking_budget": 0}), (Provider.MISTRAL, "mistral-tiny", {}), (Provider.COHERE, "command-a-03-2025", {}), + (Provider.XAI, "grok-3-mini", {}), ], ) @pytest.mark.integration diff --git a/packages/video-generation/README.md b/packages/video-generation/README.md index 7e56c9e4..4df20632 100644 --- a/packages/video-generation/README.md +++ b/packages/video-generation/README.md @@ -39,7 +39,7 @@ uv add "celeste-ai[video-generation]"
-ByteDance +ByteDance OpenAI Google From ac76cc021b689db03c090ab0cd9feafb7ff7228a Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Mon, 24 Nov 2025 16:00:25 +0100 Subject: [PATCH 2/2] chore: bump versions for XAI provider release - celeste-text-generation: 0.2.9 -> 0.2.10 - celeste-ai: 0.2.10 -> 0.2.11 --- packages/text-generation/pyproject.toml | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/text-generation/pyproject.toml b/packages/text-generation/pyproject.toml index f084a438..446950c0 100644 --- a/packages/text-generation/pyproject.toml +++ b/packages/text-generation/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "celeste-text-generation" -version = "0.2.9" +version = "0.2.10" description = "Text 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 c7a473a2..12717e26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "celeste-ai" -version = "0.2.10" +version = "0.2.11" 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" @@ -33,11 +33,11 @@ Repository = "https://github.com/withceleste/celeste-python" Issues = "https://github.com/withceleste/celeste-python/issues" [project.optional-dependencies] -text-generation = ["celeste-text-generation>=0.2.9"] +text-generation = ["celeste-text-generation>=0.2.10"] image-generation = ["celeste-image-generation>=0.2.9"] video-generation = ["celeste-video-generation>=0.2.8"] all = [ - "celeste-text-generation>=0.2.9", + "celeste-text-generation>=0.2.10", "celeste-image-generation>=0.2.9", "celeste-video-generation>=0.2.8", ]