Skip to content
Closed
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
16 changes: 14 additions & 2 deletions src/agentpool_server/openai_api_server/completions/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import TYPE_CHECKING, Any

import anyenv
from pydantic_ai import PartDeltaEvent, TextPartDelta
from pydantic_ai import PartDeltaEvent, TextPartDelta, ThinkingPartDelta

from agentpool.log import get_logger

Expand Down Expand Up @@ -47,7 +47,6 @@ async def stream_response(
async for event in events:
match event:
case PartDeltaEvent(delta=TextPartDelta(content_delta=chunk)):
# Skip empty chunks
if not chunk:
continue
delta = {"content": chunk}
Expand All @@ -60,6 +59,19 @@ async def stream_response(
"choices": [choice],
}
yield f"data: {anyenv.dump_json(chunk_data)}\n\n"
case PartDeltaEvent(delta=ThinkingPartDelta(content_delta=chunk)):
if not chunk:
continue
delta = {"reasoning_content": chunk}
choice = {"index": 0, "delta": delta, "finish_reason": None}
chunk_data = {
"id": response_id,
"object": "chat.completion.chunk",
"created": created,
"model": request.model,
"choices": [choice],
}
yield f"data: {anyenv.dump_json(chunk_data)}\n\n"
final_chunk = {
"id": response_id,
"object": "chat.completion.chunk",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class OpenAIMessage(Schema):
name: str | None = None
function_call: FunctionCall | None = None
tool_calls: list[ToolCall] | None = None
reasoning_content: str | None = None


class ChatCompletionRequest(Schema):
Expand Down
17 changes: 15 additions & 2 deletions src/agentpool_server/openai_api_server/responses/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
from typing import TYPE_CHECKING, Any
from uuid import uuid4

from pydantic_ai.messages import ModelResponse, ThinkingPart

from agentpool_server.openai_api_server.responses.models import (
Response,
ResponseMessage,
ResponseOutputText,
ResponseReasoning,
ResponseToolCall,
ResponseUsage,
)
Expand All @@ -23,13 +26,23 @@ async def handle_request(request: ResponseRequest, message: ChatMessage[Any]) ->
text = ResponseOutputText(text=str(message.content))
output_msg_id = f"msg_{uuid4().hex}"
output_msg = ResponseMessage(id=output_msg_id, role="assistant", content=[text])
output: list[ResponseMessage | ResponseToolCall] = [output_msg]
output: list[ResponseMessage | ResponseToolCall | ResponseReasoning] = [output_msg]

reasoning_parts = [
{"type": "text", "text": p.content}
for m in message.messages
if isinstance(m, ModelResponse)
for p in m.parts
if isinstance(p, ThinkingPart) and p.content
]
if reasoning_parts:
output.append(ResponseReasoning(content=reasoning_parts))

calls = [
ResponseToolCall(type=f"{tc.tool_name}_call", id=tc.tool_call_id)
for tc in message.get_tool_calls()
]
output = calls + output # type: ignore[assignment, operator]
output = calls + output

usage_info: ResponseUsage | None = None
if message.cost_info and (token_usage := message.cost_info.token_usage):
Expand Down
9 changes: 8 additions & 1 deletion src/agentpool_server/openai_api_server/responses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ class ResponseOutputText(Schema):
annotations: list[dict[str, Any]] = Field(default_factory=list)


class ResponseReasoning(Schema):
"""Reasoning output part for thinking content."""

type: Literal["reasoning"] = "reasoning"
content: list[dict[str, str]] = Field(default_factory=list)


class ResponseToolCall(Schema):
"""Tool call in response."""

Expand Down Expand Up @@ -84,7 +91,7 @@ class Response(Schema):
status: Literal["completed", "error"] = "completed"
error: str | None = None
model: str
output: Sequence[ResponseMessage | ResponseToolCall]
output: Sequence[ResponseMessage | ResponseToolCall | ResponseReasoning]

# Include all the request parameters
instructions: str | None = None
Expand Down
13 changes: 12 additions & 1 deletion src/agentpool_server/openai_api_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import anyenv
from fastapi import Header
from pydantic_ai.messages import ModelResponse, ThinkingPart

from agentpool.agents.events import StreamCompleteEvent
from agentpool.log import get_logger
Expand Down Expand Up @@ -219,7 +220,17 @@ async def create_chat_completion(self, request: ChatCompletionRequest) -> Respon
if final_message is None:
raise HTTPException(500, "No response received from agent")

msg = OpenAIMessage(role="assistant", content=str(final_message.content))
reasoning_text = ""
for m in final_message.messages:
if isinstance(m, ModelResponse):
for p in m.parts:
if isinstance(p, ThinkingPart) and p.content:
reasoning_text += p.content
msg = OpenAIMessage(
role="assistant",
content=str(final_message.content),
reasoning_content=reasoning_text or None,
)
completion_response = ChatCompletionResponse(
id=final_message.message_id,
created=int(final_message.timestamp.timestamp()),
Expand Down
47 changes: 47 additions & 0 deletions src/agentpool_server/opencode_server/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
RequestUsage,
RetryPromptPart,
TextPart as PydanticTextPart,
ThinkingPart as PydanticThinkingPart,
ToolCallPart as PydanticToolCallPart,
ToolReturnPart as PydanticToolReturnPart,
UserPromptPart,
Expand All @@ -30,6 +31,7 @@
MessagePath,
MessageTime,
MessageWithParts,
ReasoningPart,
Session,
SessionRevert,
SessionShare,
Expand Down Expand Up @@ -290,12 +292,43 @@ def chat_message_to_opencode( # noqa: PLR0915
if content:
ts_opt = TimeStartEndOptional(start=created_ms, end=completed_ms)
result.add_text_part(content, time=ts_opt)
elif isinstance(part_dict, dict) and part_dict.get("part_kind") == "thinking":
content = part_dict.get("content") or ""
if content:
ts_opt = TimeStartEndOptional(start=created_ms, end=completed_ms)
reasoning_meta: dict[str, Any] = {}
if part_dict.get("id") is not None:
reasoning_meta["thinking_id"] = part_dict["id"]
if part_dict.get("provider_name") is not None:
reasoning_meta["provider_name"] = part_dict["provider_name"]
if part_dict.get("signature") is not None:
reasoning_meta["signature"] = part_dict["signature"]
if part_dict.get("provider_details") is not None:
reasoning_meta["provider_details"] = part_dict["provider_details"]
result.add_reasoning_part(
content, time=ts_opt, metadata=reasoning_meta or None
)
continue
for p in model_msg.parts:
match p:
case PydanticTextPart(content=content):
ts_opt = TimeStartEndOptional(start=created_ms, end=completed_ms)
result.add_text_part(content, time=ts_opt)
case PydanticThinkingPart(content=content):
if content:
ts_opt = TimeStartEndOptional(start=created_ms, end=completed_ms)
thinking_meta: dict[str, Any] = {}
if p.id is not None:
thinking_meta["thinking_id"] = p.id
if p.provider_name is not None:
thinking_meta["provider_name"] = p.provider_name
if p.signature is not None:
thinking_meta["signature"] = p.signature
if p.provider_details is not None:
thinking_meta["provider_details"] = p.provider_details
result.add_reasoning_part(
content, time=ts_opt, metadata=thinking_meta or None
)
case PydanticToolCallPart(tool_name=tool_name, tool_call_id=call_id):
tool_input = _convert_params_for_ui(safe_args_as_dict(p))
ts = TimeStart(start=created_ms)
Expand Down Expand Up @@ -466,6 +499,20 @@ def opencode_to_chat_message( # noqa: PLR0915
match part:
case TextPart(text=text, id=part_id):
response_parts.append(PydanticTextPart(content=text, id=part_id))
case ReasoningPart(text=text):
if text:
thinking_kwargs: dict[str, Any] = {"content": text}
meta = part.metadata
if meta:
if meta.get("thinking_id") is not None:
thinking_kwargs["id"] = meta["thinking_id"]
if meta.get("provider_name") is not None:
thinking_kwargs["provider_name"] = meta["provider_name"]
if meta.get("signature") is not None:
thinking_kwargs["signature"] = meta["signature"]
if meta.get("provider_details") is not None:
thinking_kwargs["provider_details"] = meta["provider_details"]
response_parts.append(PydanticThinkingPart(**thinking_kwargs))
case ToolPart(tool=tool_name, call_id=call_id, state=state):
response_parts.append(
PydanticToolCallPart(
Expand Down
19 changes: 19 additions & 0 deletions src/agentpool_server/opencode_server/models/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
FilePart,
FilePartSource,
Part,
ReasoningPart,
RetryPart,
StepFinishPart,
StepStartPart,
Expand Down Expand Up @@ -372,6 +373,24 @@ def add_text_part(
self.parts.append(part)
return part

def add_reasoning_part(
self,
text: str,
time: TimeStartEndOptional | None = None,
metadata: dict[str, Any] | None = None,
) -> ReasoningPart:
"""Create and append a reasoning/thinking part."""
part = ReasoningPart(
id=identifier.ascending("part"),
message_id=self.info.id,
session_id=self.info.session_id,
text=text,
time=time,
metadata=metadata,
)
self.parts.append(part)
return part

def add_file_part(
self,
mime: str,
Expand Down
Loading