Private
Public Access
0
0
Files
manual_slop/src/openai_schemas.py
T
ed 6a2f2cfa37 refactor(ai_client,openai_schemas): migrate API response + _repair_minimax (Phase 5 part 2)
Phase 5: ChatMessage (part 2)
Before: 6 .get('content'/'role'/'tool_calls'/'tool_call_id') sites
After:  0
Delta:  -6

Migrates:
1. _send_deepseek API response parsing (lines 2321-2324):
   - message.get('content', '')        -> message.content or ''
   - message.get('tool_calls', [])     -> [tc.to_dict() for tc in message.tool_calls]
   - message.get('reasoning_content')  -> kept as choice.get('message', {}).get('reasoning_content', '')
     (reasoning_content is NOT a ChatMessage field)

2. _repair_minimax_history generator (line 2454):
   - m.get('role') == 'tool'           -> _CM.from_dict(m).role == 'tool'
   - m.get('tool_call_id')             -> _CM.from_dict(m).tool_call_id
   Used inline conversion because the generator iterates over a
   dict list and reads 2 fields. Inline conversion avoids an
   intermediate list comprehension.

openai_schemas.py:
- ChatMessage.from_dict() now provides defaults for required fields
  ('role' -> 'assistant', 'content' -> '') when the input dict is
  missing them. This handles the case where DeepSeek's API returns
  an empty {} for 'message' (e.g., finish_reason='length' with no
  content). Without this default, ChatMessage.__init__() raises
  TypeError.

Tests: 46/46 pass (test_ai_client_result, test_ai_client_tool_loop,
test_deepseek_provider, test_openai_schemas, test_minimax_provider).
2026-06-25 20:19:27 -04:00

133 lines
3.7 KiB
Python

"""OpenAI-compatible dataclasses for the Manual Slop ai_client layer.
Promotes `NormalizedResponse` and `OpenAICompatibleRequest` from
`src/openai_compatible.py` to typed dataclasses. The 4 dataclasses
here model the OpenAI Chat Completion API shape:
- ToolCall: a single tool call from the model
- ToolCallFunction: the function portion of a tool call (name + JSON args)
- ChatMessage: a single message in the conversation (system/user/assistant/tool)
- UsageStats: token usage accounting (input, output, cache hits/creation)
`NormalizedResponse` and `OpenAICompatibleRequest` keep their public
shapes but consume these typed shapes internally.
CONVENTION: 1-space indentation. NO COMMENTS.
"""
from __future__ import annotations
from dataclasses import dataclass, field, fields as dc_fields
from typing import Any, Callable, Optional
from src.type_aliases import JsonValue, Metadata
def _from_dict_filter(cls: type, data: Metadata) -> Metadata:
return {k: v for k, v in data.items() if k in {f.name for f in dc_fields(cls)}}
@dataclass(frozen=True)
class ToolCallFunction:
name: str
arguments: str
@dataclass(frozen=True)
class ToolCall:
id: str
function: ToolCallFunction
type: str = "function"
def to_dict(self) -> JsonValue:
return {
"id": self.id,
"type": self.type,
"function": {
"name": self.function.name,
"arguments": self.function.arguments,
},
}
@classmethod
def from_dict(cls, data: Metadata) -> "ToolCall":
fn = ToolCallFunction(**_from_dict_filter(ToolCallFunction, data.get("function", {})))
return cls(**{**_from_dict_filter(cls, data), "function": fn})
@dataclass(frozen=True)
class ChatMessage:
role: str
content: str | list
tool_calls: Optional[tuple[ToolCall, ...]] = None
tool_call_id: Optional[str] = None
name: Optional[str] = None
def to_dict(self) -> JsonValue:
d: JsonValue = {"role": self.role, "content": self.content}
if self.tool_calls is not None:
d["tool_calls"] = [tc.to_dict() for tc in self.tool_calls]
if self.tool_call_id is not None:
d["tool_call_id"] = self.tool_call_id
if self.name is not None:
d["name"] = self.name
return d
@classmethod
def from_dict(cls, data: Metadata) -> "ChatMessage":
raw_tool_calls = data.get("tool_calls")
tool_calls = None
if raw_tool_calls is not None:
tool_calls = tuple(ToolCall.from_dict(tc) for tc in raw_tool_calls)
filtered = _from_dict_filter(cls, data)
if "role" not in filtered:
filtered["role"] = "assistant"
if "content" not in filtered:
filtered["content"] = ""
return cls(**{**filtered, "tool_calls": tool_calls})
@dataclass(frozen=True)
class UsageStats:
input_tokens: int
output_tokens: int
cache_read_tokens: int = 0
cache_creation_tokens: int = 0
@classmethod
def from_dict(cls, data: Metadata) -> "UsageStats":
return cls(**_from_dict_filter(cls, data))
@dataclass(frozen=True)
class NormalizedResponse:
text: str
tool_calls: tuple[ToolCall, ...] = ()
usage: UsageStats = field(default_factory=lambda: UsageStats(input_tokens=0, output_tokens=0))
raw_response: Any = None
def to_legacy_dict(self) -> JsonValue:
return {
"text": self.text,
"tool_calls": [tc.to_dict() for tc in self.tool_calls],
"usage": {
"input_tokens": self.usage.input_tokens,
"output_tokens": self.usage.output_tokens,
"cache_read_tokens": self.usage.cache_read_tokens,
"cache_creation_tokens": self.usage.cache_creation_tokens,
},
"raw_response": self.raw_response,
}
@dataclass
class OpenAICompatibleRequest:
messages: list[ChatMessage]
model: str
temperature: float = 0.0
top_p: float = 1.0
max_tokens: int = 8192
tools: Optional[list[JsonValue]] = None
tool_choice: str = "auto"
stream: bool = False
stream_callback: Optional[Callable[[str], None]] = None
extra_body: Optional[JsonValue] = None