diff --git a/src/plyngent/agent/messages_bridge.py b/src/plyngent/agent/messages_bridge.py index 5f68b1a..b315465 100644 --- a/src/plyngent/agent/messages_bridge.py +++ b/src/plyngent/agent/messages_bridge.py @@ -30,14 +30,9 @@ from plyngent.lmproto.openai_compatible.model import ( AssistantFunctionTool, AssistantFunctionToolCall, ChatCompletionChoice, - ChatCompletionChunk, ChatCompletionResponse, ChatCompletionsParam, - ChunkChoice, - DeltaMessage, DeveloperChatMessage, - StreamFunctionDelta, - StreamToolCallDelta, SystemChatMessage, ToolFunctionItem, UserChatMessage, @@ -290,88 +285,6 @@ def anthropic_response_to_chat_completion( ) -def finish_reason_chunk( - *, - model: str, - finish_reason: str, - created: int = 0, -) -> ChatCompletionChunk: - return ChatCompletionChunk( - id="anthropic-stream", - object="chat.completion.chunk", - created=created, - model=model, - choices=[ - ChunkChoice( - index=0, - delta=DeltaMessage(), - finish_reason=cast("Any", finish_reason), - ) - ], - ) - - -def text_delta_chunk(*, model: str, content: str, created: int = 0) -> ChatCompletionChunk: - return ChatCompletionChunk( - id="anthropic-stream", - object="chat.completion.chunk", - created=created, - model=model, - choices=[ - ChunkChoice( - index=0, - delta=DeltaMessage(content=content), - ) - ], - ) - - -def tool_call_delta_chunk( - *, - model: str, - index: int, - call_id: str | None = None, - name: str | None = None, - arguments: str | None = None, - created: int = 0, -) -> ChatCompletionChunk: - return ChatCompletionChunk( - id="anthropic-stream", - object="chat.completion.chunk", - created=created, - model=model, - choices=[ - ChunkChoice( - index=0, - delta=DeltaMessage( - tool_calls=[ - StreamToolCallDelta( - index=index, - id=call_id if call_id is not None else UNSET, - type="function", - function=StreamFunctionDelta( - name=name if name is not None else UNSET, - arguments=arguments if arguments is not None else UNSET, - ), - ) - ] - ), - ) - ], - ) - - -def usage_chunk(*, model: str, usage: dict[str, Any], created: int = 0) -> ChatCompletionChunk: - return ChatCompletionChunk( - id="anthropic-stream", - object="chat.completion.chunk", - created=created, - model=model, - choices=[], - usage=usage, - ) - - def _max_tokens_from_param(param: ChatCompletionsParam) -> int: if param.max_completion_tokens is not UNSET: return int(param.max_completion_tokens) diff --git a/src/plyngent/agent/messages_dispatch.py b/src/plyngent/agent/messages_dispatch.py index ee99f33..c4865e2 100644 --- a/src/plyngent/agent/messages_dispatch.py +++ b/src/plyngent/agent/messages_dispatch.py @@ -13,6 +13,8 @@ from plyngent.agent.messages_bridge import ( anthropic_stop_to_finish_reason, anthropic_usage_to_dict, chat_param_to_anthropic_param, +) +from plyngent.agent.stream_chunks import ( finish_reason_chunk, text_delta_chunk, tool_call_delta_chunk, diff --git a/src/plyngent/agent/responses_bridge.py b/src/plyngent/agent/responses_bridge.py index e2342b7..e7b52d7 100644 --- a/src/plyngent/agent/responses_bridge.py +++ b/src/plyngent/agent/responses_bridge.py @@ -27,11 +27,7 @@ from plyngent.lmproto.openai_compatible.model import ( ChatCompletionChunk, ChatCompletionResponse, ChatCompletionsParam, - ChunkChoice, - DeltaMessage, DeveloperChatMessage, - StreamFunctionDelta, - StreamToolCallDelta, SystemChatMessage, ToolFunctionItem, UserChatMessage, @@ -204,58 +200,6 @@ def response_to_chat_completion(response: Response) -> ChatCompletionResponse: ) -def finish_reason_chunk( - *, - model: str, - finish_reason: str, - created: int = 0, -) -> ChatCompletionChunk: - """Terminal stream chunk carrying only ``finish_reason`` (no delta text).""" - return ChatCompletionChunk( - id="resp-stream", - object="chat.completion.chunk", - created=created, - model=model, - choices=[ - ChunkChoice( - index=0, - delta=DeltaMessage(), - finish_reason=cast("Any", finish_reason), - ) - ], - ) - - -def text_delta_chunk(*, model: str, content: str, created: int = 0) -> ChatCompletionChunk: - return ChatCompletionChunk( - id="resp-stream", - object="chat.completion.chunk", - created=created, - model=model, - choices=[ - ChunkChoice( - index=0, - delta=DeltaMessage(content=content), - ) - ], - ) - - -def reasoning_delta_chunk(*, model: str, content: str, created: int = 0) -> ChatCompletionChunk: - return ChatCompletionChunk( - id="resp-stream", - object="chat.completion.chunk", - created=created, - model=model, - choices=[ - ChunkChoice( - index=0, - delta=DeltaMessage(reasoning_content=content), - ) - ], - ) - - def tool_call_chunks_from_response( response: Response, *, @@ -263,50 +207,28 @@ def tool_call_chunks_from_response( created: int = 0, ) -> list[ChatCompletionChunk]: """Emit complete tool-call stream deltas (one chunk per call) for loop merge.""" + from .stream_chunks import tool_call_delta_chunk + calls = response_function_calls(response) - chunks: list[ChatCompletionChunk] = [] - for index, call in enumerate(calls): - chunks.append( - ChatCompletionChunk( - id=response.id, - object="chat.completion.chunk", - created=created, - model=model, - choices=[ - ChunkChoice( - index=0, - delta=DeltaMessage( - tool_calls=[ - StreamToolCallDelta( - index=index, - id=call.call_id, - type="function", - function=StreamFunctionDelta( - name=call.name, - arguments=call.arguments, - ), - ) - ] - ), - ) - ], - ) + return [ + tool_call_delta_chunk( + model=model, + index=index, + call_id=call.call_id, + name=call.name, + arguments=call.arguments, + created=created, ) - return chunks + for index, call in enumerate(calls) + ] def usage_chunk_from_response(response: Response, *, model: str) -> ChatCompletionChunk | None: + from .stream_chunks import usage_chunk + if response.usage is UNSET or response.usage is None: return None - created = int(response.created_at) - return ChatCompletionChunk( - id=response.id, - object="chat.completion.chunk", - created=created, - model=model, - choices=[], - usage=response.usage, - ) + return usage_chunk(model=model, usage=response.usage, created=int(response.created_at)) def _merge_response_tools( diff --git a/src/plyngent/agent/responses_dispatch.py b/src/plyngent/agent/responses_dispatch.py index e2fd0a8..f4db34b 100644 --- a/src/plyngent/agent/responses_dispatch.py +++ b/src/plyngent/agent/responses_dispatch.py @@ -16,15 +16,17 @@ from msgspec import UNSET from plyngent.agent.responses_bridge import ( chat_param_to_responses_kwargs, - finish_reason_chunk, - reasoning_delta_chunk, response_to_assistant_message, response_to_chat_completion, responses_status_to_finish_reason, - text_delta_chunk, tool_call_chunks_from_response, usage_chunk_from_response, ) +from plyngent.agent.stream_chunks import ( + finish_reason_chunk, + reasoning_delta_chunk, + text_delta_chunk, +) from plyngent.lmproto.openai.model import Response as ResponseModel from plyngent.lmproto.openai.model import ResponsesCreateParam diff --git a/src/plyngent/agent/stream_chunks.py b/src/plyngent/agent/stream_chunks.py new file mode 100644 index 0000000..7ac2ec8 --- /dev/null +++ b/src/plyngent/agent/stream_chunks.py @@ -0,0 +1,128 @@ +"""Shared chat-completions stream chunk builders for protocol bridges. + +Both the Responses bridge and the Anthropic Messages bridge synthesize +``ChatCompletionChunk`` objects so the agent loop can consume provider streams +uniformly. The generic builders below are provider-agnostic; provider bridges +map their events onto them. +""" + +from __future__ import annotations + +from typing import Any, cast + +from msgspec import UNSET + +from plyngent.lmproto.openai_compatible.model import ( + ChatCompletionChunk, + ChunkChoice, + DeltaMessage, + StreamFunctionDelta, + StreamToolCallDelta, +) + + +def finish_reason_chunk( + *, + model: str, + finish_reason: str, + created: int = 0, +) -> ChatCompletionChunk: + """Terminal stream chunk carrying only ``finish_reason`` (no delta text).""" + return ChatCompletionChunk( + id="bridge-stream", + object="chat.completion.chunk", + created=created, + model=model, + choices=[ + ChunkChoice( + index=0, + delta=DeltaMessage(), + finish_reason=cast("Any", finish_reason), + ) + ], + ) + + +def text_delta_chunk(*, model: str, content: str, created: int = 0) -> ChatCompletionChunk: + return ChatCompletionChunk( + id="bridge-stream", + object="chat.completion.chunk", + created=created, + model=model, + choices=[ + ChunkChoice( + index=0, + delta=DeltaMessage(content=content), + ) + ], + ) + + +def reasoning_delta_chunk(*, model: str, content: str, created: int = 0) -> ChatCompletionChunk: + return ChatCompletionChunk( + id="bridge-stream", + object="chat.completion.chunk", + created=created, + model=model, + choices=[ + ChunkChoice( + index=0, + delta=DeltaMessage(reasoning_content=content), + ) + ], + ) + + +def tool_call_delta_chunk( + *, + model: str, + index: int, + call_id: str | None = None, + name: str | None = None, + arguments: str | None = None, + created: int = 0, +) -> ChatCompletionChunk: + return ChatCompletionChunk( + id="bridge-stream", + object="chat.completion.chunk", + created=created, + model=model, + choices=[ + ChunkChoice( + index=0, + delta=DeltaMessage( + tool_calls=[ + StreamToolCallDelta( + index=index, + id=call_id if call_id is not None else UNSET, + type="function", + function=StreamFunctionDelta( + name=name if name is not None else UNSET, + arguments=arguments if arguments is not None else UNSET, + ), + ) + ] + ), + ) + ], + ) + + +def usage_chunk(*, model: str, usage: dict[str, Any], created: int = 0) -> ChatCompletionChunk: + return ChatCompletionChunk( + id="bridge-stream", + object="chat.completion.chunk", + created=created, + model=model, + choices=[], + usage=usage, + ) + + +__all__ = [ + "finish_reason_chunk", + "reasoning_delta_chunk", + "text_delta_chunk", + "tool_call_delta_chunk", + "usage_chunk", +]