From 6a2f2cfa37d4d0a3a25efacd56f25c13f9bd23dd Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 25 Jun 2026 20:19:27 -0400 Subject: [PATCH] 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). --- src/ai_client.py | 12 +++++++----- src/openai_schemas.py | 7 ++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/ai_client.py b/src/ai_client.py index 215ceeda..028c4b14 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -2318,10 +2318,11 @@ def _send_deepseek(md_content: str, user_message: str, base_dir: str, _append_comms("IN", "response", {"round": round_idx, "text": "(No choices returned)", "usage": response_data.get("usage", {})}) break choice = choices[0] - message = choice.get("message", {}) - assistant_text = message.get("content", "") - tool_calls_raw = message.get("tool_calls", []) - reasoning_content = message.get("reasoning_content", "") + from src.openai_schemas import ChatMessage as _CM + message = _CM.from_dict(choice.get("message", {})) + assistant_text = message.content or "" + tool_calls_raw = [tc.to_dict() for tc in message.tool_calls] if message.tool_calls else [] + reasoning_content = choice.get("message", {}).get("reasoning_content", "") finish_reason = choice.get("finish_reason", "stop") usage = response_data.get("usage", {}) @@ -2451,7 +2452,8 @@ def _repair_minimax_history(history: list[Metadata]) -> None: elif isinstance(tc, dict) and tc.get("id"): call_ids.append(tc["id"]) for cid in call_ids: - already_has = any(m.get("role") == "tool" and m.get("tool_call_id") == cid for m in history[-len(call_ids)-1:]) + from src.openai_schemas import ChatMessage as _CM + already_has = any(_CM.from_dict(m).role == "tool" and _CM.from_dict(m).tool_call_id == cid for m in history[-len(call_ids)-1:]) if not already_has: history.append({ "role": "tool", diff --git a/src/openai_schemas.py b/src/openai_schemas.py index 7fab91e3..173eec2d 100644 --- a/src/openai_schemas.py +++ b/src/openai_schemas.py @@ -78,7 +78,12 @@ class ChatMessage: tool_calls = None if raw_tool_calls is not None: tool_calls = tuple(ToolCall.from_dict(tc) for tc in raw_tool_calls) - return cls(**{**_from_dict_filter(cls, data), "tool_calls": 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)