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
1 change: 1 addition & 0 deletions packages/capabilities/text-generation/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Issues = "https://github.com/withceleste/celeste-python/issues"

[tool.uv.sources]
celeste-ai = { workspace = true }
celeste-anthropic = { workspace = true }

[project.entry-points."celeste.packages"]
text-generation = "celeste_text_generation:register_package"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TextGenerationFinishReason(FinishReason):
Stores raw provider reason. Providers map their values in implementation.
"""

reason: str
reason: str | None = None


class TextGenerationUsage(Usage):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
"""Anthropic client implementation for text generation."""

from collections.abc import AsyncIterator
from typing import Any, Unpack

import httpx
from pydantic import BaseModel
from celeste_anthropic.messages.client import AnthropicMessagesClient

from celeste.mime_types import ApplicationMimeType
from celeste.parameters import ParameterMapper
from celeste.types import StructuredOutput
from celeste_text_generation.client import TextGenerationClient
from celeste_text_generation.io import (
TextGenerationFinishReason,
TextGenerationInput,
TextGenerationUsage,
)
from celeste_text_generation.parameters import (
TextGenerationParameter,
TextGenerationParameters,
)
from celeste_text_generation.parameters import TextGenerationParameters

from . import config
from .parameters import ANTHROPIC_PARAMETER_MAPPERS
from .streaming import AnthropicTextGenerationStream


class AnthropicTextGenerationClient(TextGenerationClient):
class AnthropicTextGenerationClient(AnthropicMessagesClient, TextGenerationClient):
"""Anthropic client for text generation."""

@classmethod
Expand All @@ -37,31 +31,16 @@ def _init_request(self, inputs: TextGenerationInput) -> dict[str, Any]:

def _parse_usage(self, response_data: dict[str, Any]) -> TextGenerationUsage:
"""Parse usage from response."""
usage_data = response_data.get("usage", {})
input_tokens = usage_data.get("input_tokens")
output_tokens = usage_data.get("output_tokens")

total_tokens = None
if input_tokens is not None and output_tokens is not None:
total_tokens = input_tokens + output_tokens

return TextGenerationUsage(
input_tokens=input_tokens,
output_tokens=output_tokens,
total_tokens=total_tokens,
cached_tokens=usage_data.get("cache_read_input_tokens"),
)
usage = super()._parse_usage(response_data)
return TextGenerationUsage(**usage)

def _parse_content(
self,
response_data: dict[str, Any],
**parameters: Unpack[TextGenerationParameters],
) -> str | BaseModel:
) -> StructuredOutput:
"""Parse content from response."""
content = response_data.get("content", [])
if not content:
msg = "No content blocks in response"
raise ValueError(msg)
content = super()._parse_content(response_data)

text_content = ""
for content_block in content:
Expand All @@ -73,75 +52,14 @@ def _parse_content(

def _parse_finish_reason(
self, response_data: dict[str, Any]
) -> TextGenerationFinishReason | None:
) -> TextGenerationFinishReason:
"""Parse finish reason from response."""
stop_reason = response_data.get("stop_reason")
if stop_reason is None:
return None

return TextGenerationFinishReason(reason=stop_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 = {"content"}
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
request_body["max_tokens"] = parameters.get("max_tokens") or 1024

headers = {
**self.auth.get_headers(),
config.ANTHROPIC_VERSION_HEADER: config.ANTHROPIC_VERSION,
"Content-Type": ApplicationMimeType.JSON,
}

if parameters.get(TextGenerationParameter.OUTPUT_SCHEMA) is not None:
headers[config.ANTHROPIC_BETA_HEADER] = config.STRUCTURED_OUTPUTS_BETA

return await self.http_client.post(
f"{config.BASE_URL}{config.ENDPOINT}",
headers=headers,
json_body=request_body,
)
finish_reason = super()._parse_finish_reason(response_data)
return TextGenerationFinishReason(reason=finish_reason.reason)

def _stream_class(self) -> type[AnthropicTextGenerationStream]:
"""Return the Stream class for this client."""
return AnthropicTextGenerationStream

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["max_tokens"] = parameters.get("max_tokens") or 1024
request_body["stream"] = True

headers = {
**self.auth.get_headers(),
config.ANTHROPIC_VERSION_HEADER: config.ANTHROPIC_VERSION,
"Content-Type": ApplicationMimeType.JSON,
}

if parameters.get(TextGenerationParameter.OUTPUT_SCHEMA) is not None:
headers[config.ANTHROPIC_BETA_HEADER] = config.STRUCTURED_OUTPUTS_BETA

return self.http_client.stream_post(
f"{config.BASE_URL}{config.STREAM_ENDPOINT}",
headers=headers,
json_body=request_body,
)


__all__ = ["AnthropicTextGenerationClient"]

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from celeste import Model, Provider
from celeste.constraints import Range, Schema
from celeste.core import Parameter
from celeste_text_generation.parameters import TextGenerationParameter

MODELS: list[Model] = [
Expand All @@ -11,6 +12,7 @@
display_name="Claude Sonnet 4.5",
streaming=True,
parameter_constraints={
Parameter.MAX_TOKENS: Range(min=1, max=64000),
TextGenerationParameter.THINKING_BUDGET: Range(min=-1, max=64000),
TextGenerationParameter.OUTPUT_SCHEMA: Schema(),
},
Expand All @@ -21,6 +23,7 @@
display_name="Claude Haiku 4.5",
streaming=True,
parameter_constraints={
Parameter.MAX_TOKENS: Range(min=1, max=64000),
TextGenerationParameter.THINKING_BUDGET: Range(min=-1, max=32000),
},
),
Expand All @@ -30,6 +33,18 @@
display_name="Claude Opus 4.1",
streaming=True,
parameter_constraints={
Parameter.MAX_TOKENS: Range(min=1, max=32000),
TextGenerationParameter.THINKING_BUDGET: Range(min=-1, max=32000),
TextGenerationParameter.OUTPUT_SCHEMA: Schema(),
},
),
Model(
id="claude-opus-4-5",
provider=Provider.ANTHROPIC,
display_name="Claude Opus 4.5",
streaming=True,
parameter_constraints={
Parameter.MAX_TOKENS: Range(min=1, max=64000),
TextGenerationParameter.THINKING_BUDGET: Range(min=-1, max=32000),
TextGenerationParameter.OUTPUT_SCHEMA: Schema(),
},
Expand All @@ -40,6 +55,7 @@
display_name="Claude Sonnet 4",
streaming=True,
parameter_constraints={
Parameter.MAX_TOKENS: Range(min=1, max=64000),
TextGenerationParameter.THINKING_BUDGET: Range(min=-1, max=64000),
},
),
Expand All @@ -49,6 +65,7 @@
display_name="Claude Opus 4",
streaming=True,
parameter_constraints={
Parameter.MAX_TOKENS: Range(min=1, max=32000),
TextGenerationParameter.THINKING_BUDGET: Range(min=-1, max=32000),
},
),
Expand Down
Loading
Loading