From 89000dec7f91cc356709792d5773565a1fd9631a Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 20 Jun 2026 14:01:55 -0400 Subject: [PATCH] refactor(ai_client): migrate _extract_gemini_thoughts + _list_minimax_models (Phase 11 sites 7+8) Site 7 (_extract_gemini_thoughts): try: getattr(resp, 'candidates', None) or [] ... chunks.append(p.text) except Exception: pass return ''.join(chunks).strip() Body: pass + empty default '' = SS violation (silent + data loss). Site 8 (_list_minimax_models): try: client.models.list() ... if found: return sorted(found) except Exception: pass return ['MiniMax-M2.7', 'MiniMax-M2.5', 'MiniMax-M2.1', 'MiniMax-M2'] Body: pass + hardcoded default = SS violation. New helpers: - _extract_gemini_thoughts_result(resp) -> Result[str] Returns Result(data=thinking_text) on success, Result(data='', errors=[ErrorInfo]) on attribute access failure. - _list_minimax_models_result(api_key) -> Result[list[str]] Returns Result(data=sorted_models) on success, Result(data=defaults, errors=[ErrorInfo]) on SDK failure. Defaults extracted to _MINIMAX_DEFAULT_MODELS module constant. Legacy wrappers delegate to _result helpers and return result.data. Audit: ai_client SS 7 -> 5. COMPLIANT 29 -> 31. --- src/ai_client.py | 58 ++++++++++++++++++++++------- tests/tier2/phase11_sites78_test.py | 52 ++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 tests/tier2/phase11_sites78_test.py diff --git a/src/ai_client.py b/src/ai_client.py index a9720110..e0b584c9 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -1748,12 +1748,14 @@ def _send_cli_round_result(r_idx: int, adapter: Any, payload: Any, safety_settin errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="ai_client._send_cli_round_result", original=e)], ) -def _extract_gemini_thoughts(resp: Any) -> str: - """ - Extracts concatenated thinking text from a Gemini response object's parts. - Parts with thought=True are thinking segments; parts with thought=False or unset are visible text. - The google-genai SDK filters thoughts out of resp.text, so we must scan parts directly. - Returns "" if no thoughts are present. +def _extract_gemini_thoughts_result(resp: Any) -> Result[str]: + """Extracts concatenated thinking text from a Gemini response object's parts. + + Per the data-oriented convention: returns Result(data=thinking_text) on + success, Result(data="", errors=[ErrorInfo]) if attribute access fails. + The legacy caller (_extract_gemini_thoughts) returns result.data + (preserving the original str signature; an empty string signals "no + thoughts" to the caller). """ chunks: list[str] = [] try: @@ -1765,8 +1767,22 @@ def _extract_gemini_thoughts(resp: Any) -> str: for p in parts: if getattr(p, "thought", False) and getattr(p, "text", None): chunks.append(p.text) - except Exception: pass - return "".join(chunks).strip() + return Result(data="".join(chunks).strip()) + except Exception as e: + return Result( + data="", + errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"failed to extract gemini thoughts: {e}", source="ai_client._extract_gemini_thoughts_result", original=e)], + ) + + +def _extract_gemini_thoughts(resp: Any) -> str: + """ + Extracts concatenated thinking text from a Gemini response object's parts. + Parts with thought=True are thinking segments; parts with thought=False or unset are visible text. + The google-genai SDK filters thoughts out of resp.text, so we must scan parts directly. + Returns "" if no thoughts are present. + """ + return _extract_gemini_thoughts_result(resp).data def _get_gemini_history_list(chat: Any | None) -> list[Any]: if not chat: return [] @@ -2402,8 +2418,17 @@ def _send_deepseek(md_content: str, user_message: str, base_dir: str, #region: MiniMax Provider +_MINIMAX_DEFAULT_MODELS: list[str] = ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"] + #TODO(Ed): This causes a pause on gui thread, this should be cached. -def _list_minimax_models(api_key: str) -> list[str]: +def _list_minimax_models_result(api_key: str) -> Result[list[str]]: + """List available MiniMax models via the OpenAI-compatible SDK. + + Returns Result(data=sorted_models) on success, Result(data=defaults, errors=[ErrorInfo]) + on SDK failure. The legacy caller (_list_minimax_models) returns result.data + (preserving the original list[str] signature; defaults are returned on failure + to maintain the original behavior). + """ try: openai = _require_warmed("openai") OpenAI = openai.OpenAI @@ -2413,10 +2438,17 @@ def _list_minimax_models(api_key: str) -> list[str]: models_list = client.models.list() found = [m.id for m in models_list] if found: - return sorted(found) - except Exception: - pass - return ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"] + return Result(data=sorted(found)) + return Result(data=_MINIMAX_DEFAULT_MODELS) + except Exception as e: + return Result( + data=_MINIMAX_DEFAULT_MODELS, + errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"failed to list minimax models: {e}", source="ai_client._list_minimax_models_result", original=e)], + ) + + +def _list_minimax_models(api_key: str) -> list[str]: + return _list_minimax_models_result(api_key).data def _repair_minimax_history(history: list[dict[str, Any]]) -> None: if not history: return diff --git a/tests/tier2/phase11_sites78_test.py b/tests/tier2/phase11_sites78_test.py new file mode 100644 index 00000000..cb532825 --- /dev/null +++ b/tests/tier2/phase11_sites78_test.py @@ -0,0 +1,52 @@ +"""Phase 11 sites 7+8: _extract_gemini_thoughts + _list_minimax_models Result helpers. + +Site 7 (_extract_gemini_thoughts): + try: candidates = getattr(resp, "candidates", None) or [] + for ... parts = getattr(content, "parts", None) or [] + ... if thought: chunks.append(p.text) + except Exception: pass + return "".join(chunks).strip() + +Body: pass + empty default '' = SS violation (silent + data loss). + +Site 8 (_list_minimax_models): + try: client = OpenAI(api_key=api_key, base_url=base_url) + models_list = client.models.list() + found = [m.id for m in models_list] + if found: return sorted(found) + except Exception: pass + return ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"] + +Body: pass + hardcoded default = SS violation. +""" +import sys +sys.path.insert(0, ".") + + +def test_phase11_sites78_extract_gemini_thoughts_result_exists(): + import src.ai_client + assert hasattr(src.ai_client, "_extract_gemini_thoughts_result"), \ + "_extract_gemini_thoughts_result helper missing" + + +def test_phase11_sites78_list_minimax_models_result_exists(): + import src.ai_client + assert hasattr(src.ai_client, "_list_minimax_models_result"), \ + "_list_minimax_models_result helper missing" + + +def test_phase11_sites78_helpers_return_result(): + import src.ai_client + import inspect + for name in ("_extract_gemini_thoughts_result", + "_list_minimax_models_result"): + fn = getattr(src.ai_client, name) + sig = inspect.signature(fn) + assert "Result" in str(sig.return_annotation), \ + f"{name} return must be Result, got {sig.return_annotation}" + + +def test_phase11_sites78_legacy_preserved(): + import src.ai_client + assert callable(getattr(src.ai_client, "_extract_gemini_thoughts", None)) + assert callable(getattr(src.ai_client, "_list_minimax_models", None)) \ No newline at end of file