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
2 changes: 2 additions & 0 deletions src/celeste/modalities/text/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Message,
Role,
TextContent,
ToolActivity,
VideoContent,
)

Expand Down Expand Up @@ -78,6 +79,7 @@ class TextChunk(Chunk[str]):
"""Chunk for text streaming."""

reasoning: str | None = None
tool_activity: ToolActivity | None = None
finish_reason: TextFinishReason | None = None
usage: TextUsage | None = None

Expand Down
16 changes: 16 additions & 0 deletions src/celeste/protocols/openresponses/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any, ClassVar

from celeste.io import FinishReason
from celeste.types import ToolActivity, ToolActivityStatus

from .client import OpenResponsesClient

Expand Down Expand Up @@ -47,6 +48,21 @@ def _parse_chunk_reasoning(self, event_data: dict[str, Any]) -> str | None:
return event_data.get("delta") or None
return None

def _parse_chunk_tool_activity(
self, event_data: dict[str, Any]
) -> ToolActivity | None:
"""Extract native web-search activity from SSE event."""
event_type = event_data.get("type")
if event_type == "response.web_search_call.in_progress":
return ToolActivity(
tool_name="web_search", status=ToolActivityStatus.STARTED
)
if event_type == "response.web_search_call.completed":
return ToolActivity(
tool_name="web_search", status=ToolActivityStatus.COMPLETED
)
return None

def _parse_chunk_usage(
self, event_data: dict[str, Any]
) -> dict[str, int | float | None] | None:
Expand Down
19 changes: 19 additions & 0 deletions src/celeste/providers/anthropic/messages/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any

from celeste.io import FinishReason
from celeste.types import ToolActivity, ToolActivityStatus

from .client import AnthropicMessagesClient

Expand Down Expand Up @@ -110,6 +111,24 @@ def _parse_chunk_reasoning(self, event_data: dict[str, Any]) -> str | None:
return delta.get("thinking") or None
return None

def _parse_chunk_tool_activity(
self, event_data: dict[str, Any]
) -> ToolActivity | None:
"""Extract native web-search activity from content block starts."""
if event_data.get("type") != "content_block_start":
return None
block = event_data.get("content_block", {})
block_type = block.get("type")
if block_type == "server_tool_use" and block.get("name") == "web_search":
return ToolActivity(
tool_name="web_search", status=ToolActivityStatus.STARTED
)
if block_type == "web_search_tool_result":
return ToolActivity(
tool_name="web_search", status=ToolActivityStatus.COMPLETED
)
return None

def _parse_chunk_usage(
self, event_data: dict[str, Any]
) -> dict[str, int | float | None] | None:
Expand Down
12 changes: 11 additions & 1 deletion src/celeste/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from celeste.io import FinishReason, Output, Usage
from celeste.parameters import Parameters
from celeste.tools import ToolCall, validate_tool_calls
from celeste.types import RawUsage
from celeste.types import RawUsage, ToolActivity


async def enrich_stream_errors(
Expand Down Expand Up @@ -114,6 +114,12 @@ def _parse_chunk_reasoning(self, event_data: dict[str, Any]) -> str | None:
"""Parse reasoning delta from chunk event. Override in providers with reasoning."""
return None

def _parse_chunk_tool_activity(
self, event_data: dict[str, Any]
) -> ToolActivity | None:
"""Parse native tool activity from chunk event. Override in providers that emit it."""
return None

def _wrap_chunk_content(self, raw_content: Any) -> Any: # noqa: ANN401
"""Wrap raw content into chunk content type. Override for type transformation."""
return raw_content
Expand All @@ -130,11 +136,13 @@ def _parse_chunk(self, event: dict[str, Any]) -> Chunk | None:
)
content = self._parse_chunk_content(event)
reasoning = self._parse_chunk_reasoning(event)
tool_activity = self._parse_chunk_tool_activity(event)
usage = self._get_chunk_usage(event)
finish_reason = self._get_chunk_finish_reason(event)
if (
content is None
and reasoning is None
and tool_activity is None
and usage is None
and finish_reason is None
):
Expand All @@ -147,6 +155,8 @@ def _parse_chunk(self, event: dict[str, Any]) -> Chunk | None:
kwargs: dict[str, Any] = {}
if reasoning is not None:
kwargs["reasoning"] = reasoning
if tool_activity is not None:
kwargs["tool_activity"] = tool_activity
return self._chunk_class( # type: ignore[return-value]
content=content,
finish_reason=finish_reason,
Expand Down
16 changes: 16 additions & 0 deletions src/celeste/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ class ToolCall(BaseModel):
arguments: dict[str, Any]


class ToolActivityStatus(StrEnum):
"""Lifecycle of a native (server-side) tool invocation during streaming."""

STARTED = "started"
COMPLETED = "completed"


class ToolActivity(BaseModel):
"""A native tool invocation surfaced live in a streaming chunk."""

tool_name: str
status: ToolActivityStatus


class Message(BaseModel):
"""A message in a conversation."""

Expand Down Expand Up @@ -117,6 +131,8 @@ class Message(BaseModel):
"Role",
"TextContent",
"TextPart",
"ToolActivity",
"ToolActivityStatus",
"ToolCall",
"ToolResultContent",
"VideoContent",
Expand Down
Loading