Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/celeste/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Base client and client registry for AI capabilities."""

from abc import ABC, abstractmethod
from json import JSONDecodeError
from typing import Any, Unpack

import httpx
from pydantic import BaseModel, ConfigDict, Field, SecretStr

from celeste.core import Capability, Provider
Expand Down Expand Up @@ -58,6 +60,23 @@ def _parse_content(
"""Parse content from provider response."""
pass

def _handle_error_response(self, response: httpx.Response) -> None:
"""Handle error responses from provider APIs"""
if not response.is_success:
# Try to extract error message from JSON response
try:
error_data = response.json()
error_msg = error_data.get("error", {}).get("message", response.text)
except JSONDecodeError:
error_msg = response.text or f"HTTP {response.status_code}"

# Raise HTTPStatusError with provider context
raise httpx.HTTPStatusError(
f"{self.provider.value} API error: {error_msg}",
request=response.request,
response=response,
)

def _transform_output(
self, content: object, **parameters: Unpack[Parameters]
) -> object:
Expand Down
2 changes: 2 additions & 0 deletions src/celeste/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Provider(StrEnum):
LUMA = "luma"
TOPAZLABS = "topazlabs"
PERPLEXITY = "perplexity"
BYTEDANCE = "bytedance"


class Capability(StrEnum):
Expand All @@ -29,6 +30,7 @@ class Capability(StrEnum):

# Image
IMAGE_GENERATION = "image_generation"
IMAGE_INTELLIGENCE = "image_intelligence"

# Video
VIDEO_INTELLIGENCE = "video_intelligence"
Expand Down
2 changes: 2 additions & 0 deletions src/celeste/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Provider.LUMA: "luma_api_key",
Provider.TOPAZLABS: "topazlabs_api_key",
Provider.PERPLEXITY: "perplexity_api_key",
Provider.BYTEDANCE: "bytedance_api_key",
}


Expand All @@ -38,6 +39,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")

model_config = {
"env_file": find_dotenv(),
Expand Down
42 changes: 31 additions & 11 deletions src/celeste/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class Model(BaseModel):

id: str
provider: Provider
capabilities: set[Capability] = Field(default_factory=set)
display_name: str
capabilities: set[Capability] = Field(default_factory=set)
parameter_constraints: dict[str, Constraint] = Field(default_factory=dict)

@property
Expand All @@ -25,25 +25,45 @@ def supported_parameters(self) -> set[str]:
_models: dict[tuple[str, Provider], Model] = {}


def register_models(models: Model | list[Model]) -> None:
def register_models(models: Model | list[Model], capability: Capability) -> None:
"""Register one or more models in the global registry.

Args:
models: Single Model instance or list of Models to register.
Each model is indexed by (model_id, provider) tuple.
capability: The capability these models are being registered for.

Raises:
ValueError: If a model with the same (id, provider) is already registered.
ValueError: If display_name differs for duplicate (id, provider) pairs.
"""
if isinstance(models, Model):
models = [models]

for model in models:
key = (model.id, model.provider)
if key in _models:
msg = f"Model '{model.id}' for provider {model.provider.value} is already registered"
raise ValueError(msg)
_models[key] = model

# Get existing or create new model with empty capabilities/constraints
registered = _models.setdefault(
key,
Model(
id=model.id,
provider=model.provider,
display_name=model.display_name,
capabilities=set(),
parameter_constraints={},
),
)

# Validate display name consistency
if registered.display_name != model.display_name:
raise ValueError(
f"Inconsistent display_name for {model.id}: "
f"'{registered.display_name}' vs '{model.display_name}'"
)

# Update capabilities and constraints (single code path)
registered.capabilities.add(capability)
registered.parameter_constraints.update(model.parameter_constraints)


def get_model(model_id: str, provider: Provider) -> Model | None:
Expand Down Expand Up @@ -72,15 +92,15 @@ def list_models(
Returns:
List of Model instances matching the filters.
"""
filtered = list(_models.values())
models = list(_models.values())

if provider is not None:
filtered = [m for m in filtered if m.provider == provider]
models = [m for m in models if m.provider == provider]

if capability is not None:
filtered = [m for m in filtered if capability in m.capabilities]
models = [m for m in models if capability in m.capabilities]

return filtered
return models


def clear() -> None:
Expand Down
Loading
Loading