refactor(ai_client): obliterate 5 legacy model-list wrappers (Phase 4)

Phase 4 (5 of 9 cruft sites obliterated):

OBLITERATED wrappers:
1. _reread_file_items (4 callers in _send_gemini + _send_gemini_cli + 2 others)
2. _list_anthropic_models (1 caller in list_models)
3. _list_gemini_models (1 caller in list_models)
4. _extract_gemini_thoughts (1 caller in _send_gemini)
5. _list_minimax_models (2 callers in _set_minimax_provider_result + set_provider)

Migration: each caller now uses the _result sibling directly with .ok check
+ .data extraction. The Result[T] error context (structured ErrorInfo) is now
propagated instead of dropped. _send_gemini gets .data with explicit .ok check.

Updated tests to assert OBLITERATED state (5 sub-track 5 tests inverted from
'_legacy_preserved' to '_legacy_obliterated'):
- tests/test_baseline_result.py: test_phase9_redo_modules_import_cleanly
- tests/tier2/phase10_invariant_test.py: _list_gemini_models removed from list
- tests/tier2/phase10_site1_test.py: _legacy_unchanged -> _legacy_obliterated
- tests/tier2/phase11_invariant_test.py: _extract/_list_minimax moved to obliterated
- tests/tier2/phase11_sites78_test.py: _legacy_preserved -> _legacy_obliterated
- tests/tier2/phase12_invariant_test.py: _list_anthropic moved to obliterated
- tests/tier2/phase12_site4_test.py: _legacy_preserved -> _legacy_obliterated
- tests/test_gemini_thinking_format.py: helper uses _result directly
- tests/test_cruft_removal.py: 5 new obliterated-wrappers invariant tests

Test result: 122/122 pass (31 baseline + 16 heuristic + 9 cruft + 5 thinking + 61 tier2).
Audit gate: src/ai_client.py --strict exits 0 (no new violations introduced).
Wrapper count: 9 -> 3 (Phase 5-6 remaining: rag_engine 1, gui_2 2).
This commit is contained in:
ed
2026-06-20 20:01:25 -04:00
parent da7ac0ddb3
commit c5a119d63f
10 changed files with 133 additions and 110 deletions
+23 -63
View File
@@ -389,12 +389,12 @@ def _set_minimax_provider_result(model: str) -> Result[list[str]]:
Returns the list of valid model names. On credentials load failure,
returns Result(data=[], errors=[ErrorInfo(...)]). The legacy caller
(set_provider) inspects result.ok to decide whether to use the
fetched list or fall back to _list_minimax_models("") for empty key.
fetched list or fall back to _list_minimax_models_result("") for empty key.
"""
try:
creds = _load_credentials()
api_key = creds.get("minimax", {}).get("api_key", "")
return Result(data=_list_minimax_models(api_key))
return Result(data=_list_minimax_models_result(api_key).data)
except (OSError, ValueError) as e:
return Result(
data=[],
@@ -424,7 +424,8 @@ def set_provider(provider: str, model: str, validate: bool = True) -> None:
_model = model
elif provider == "minimax":
result = _set_minimax_provider_result(model)
valid_models = result.data if result.ok else _list_minimax_models("")
fallback_result = _list_minimax_models_result("")
valid_models = result.data if result.ok else fallback_result.data
if model not in valid_models:
_model = "MiniMax-M2.5"
else:
@@ -492,11 +493,17 @@ def reset_session() -> None:
def list_models(provider: str) -> list[str]:
creds = _load_credentials()
if provider == "gemini": return _list_gemini_models(creds["gemini"]["api_key"])
elif provider == "anthropic": return _list_anthropic_models()
if provider == "gemini":
result = _list_gemini_models_result(creds["gemini"]["api_key"])
return result.data if result.ok else []
elif provider == "anthropic":
result = _list_anthropic_models_result()
return result.data if result.ok else []
elif provider == "deepseek": return _list_deepseek_models(creds["deepseek"]["api_key"])
elif provider == "gemini_cli": return _list_gemini_cli_models()
elif provider == "minimax": return _list_minimax_models(creds["minimax"]["api_key"])
elif provider == "minimax":
result = _list_minimax_models_result(creds["minimax"]["api_key"])
return result.data if result.ok else []
elif provider == "qwen": return _list_qwen_models()
elif provider == "grok": return _list_grok_models()
elif provider == "llama": return _list_llama_models()
@@ -1070,40 +1077,6 @@ def _reread_file_items_result(file_items: list[dict[str, Any]]) -> Result[tuple[
return Result(data=(refreshed, changed), errors=errors)
def _reread_file_items(file_items: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""
Re-reads file items from the filesystem if their modification times have changed.
Functional Purpose:
Iterates through context files, compares current filesystem mtime against cached mtime,
and reads file contents if changes are detected, returning both the full refreshed set
and the subset of changed items.
Parameters & Inputs: file_items (list[dict[str, Any]]): List of file dictionaries containing keys "path" and optionally "mtime", "content".
Returns: tuple[list[dict[str, Any]], list[dict[str, Any]]]: A tuple containing (refreshed_items, changed_items).
Immediate-Mode DAG / Thread Context:
Called by: _send_gemini
Calls: pathlib.Path.stat, pathlib.Path.read_text
SSDL: `o-> [I:get_mtime] -> [B:changed?] -> [I:read_file] -> [T:diff_text]`
Thread Boundaries: Runs synchronously in the caller thread. Does synchronous blocking file system I/O.
Thin wrapper over _reread_file_items_result; the legacy tuple shape is
preserved for backward compatibility, but the try/except Exception lives
in the Result variant (where it can capture structured ErrorInfo).
Per-file read errors are logged to stderr as warnings (operator-visible
drain) and included in err_item[\"error\"] = True for in-band flag checks.
"""
result = _reread_file_items_result(file_items)
if result.errors:
for err in result.errors:
sys.stderr.write(f"[AI_CLIENT] {err.ui_message()}\n")
sys.stderr.flush()
refreshed, changed = result.data
return refreshed, changed
def _build_file_context_text(file_items: list[dict[str, Any]]) -> str:
if not file_items:
return ""
@@ -1355,9 +1328,6 @@ def _list_anthropic_models_result() -> Result[list[str]]:
)
def _list_anthropic_models() -> list[str]:
return _list_anthropic_models_result().data
def _ensure_anthropic_client() -> None:
global _anthropic_client
anthropic = _require_warmed("anthropic")
@@ -1581,7 +1551,8 @@ def _send_anthropic(
})
_append_comms("OUT", "request", {"message": f"[TOOL OUTPUT BUDGET EXCEEDED: {_cumulative_tool_bytes} bytes]"})
if file_items:
file_items, changed = _reread_file_items(file_items)
_reread_result = _reread_file_items_result(file_items)
file_items, changed = _reread_result.data
refreshed_ctx = _build_file_diff_text(changed)
if refreshed_ctx:
tool_results.append({
@@ -1665,9 +1636,6 @@ def _list_gemini_models_result(api_key: str) -> Result[list[str]]:
)
def _list_gemini_models(api_key: str) -> list[str]:
return _list_gemini_models_result(api_key).data
def _ensure_gemini_client() -> None:
global _gemini_client
genai = _require_warmed("google.genai")
@@ -1812,15 +1780,6 @@ def _extract_gemini_thoughts_result(resp: Any) -> Result[str]:
)
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 []
if hasattr(chat, "_history"): return cast(list[Any], chat._history)
@@ -2014,7 +1973,8 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str,
# Check if this is the last tool to trigger file refresh
if i == len(results) - 1:
if file_items:
file_items, changed = _reread_file_items(file_items)
_reread_result = _reread_file_items_result(file_items)
file_items, changed = _reread_result.data
ctx = _build_file_diff_text(changed)
if ctx:
out += f"\n\n{_get_context_marker()}\n\n{ctx}"
@@ -2034,7 +1994,8 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str,
_append_comms("OUT", "tool_result_send", {"results": log})
payload = f_resps
res = "\n\n".join(all_text) if all_text else "(No text returned)"
thought_text = _extract_gemini_thoughts(final_resp if stream_callback else resp)
thought_text_result = _extract_gemini_thoughts_result(final_resp if stream_callback else resp)
thought_text = thought_text_result.data if thought_text_result.ok else ""
if thought_text:
res = f"<thinking>\n{thought_text}\n</thinking>\n\n{res}"
if monitor.enabled: monitor.end_component("ai_client._send_gemini")
@@ -2126,7 +2087,8 @@ def _send_gemini_cli(md_content: str, user_message: str, base_dir: str,
for i, (name, call_id, out, _) in enumerate(results_iter):
if i == len(results_iter) - 1:
if file_items:
file_items, changed = _reread_file_items(file_items)
_reread_result = _reread_file_items_result(file_items)
file_items, changed = _reread_result.data
ctx = _build_file_diff_text(changed)
if ctx:
out += f"\n\n{_get_context_marker()}\n\n{ctx}"
@@ -2416,7 +2378,8 @@ def _send_deepseek(md_content: str, user_message: str, base_dir: str,
for i, (name, call_id, out, _) in enumerate(results):
if i == len(results) - 1:
if file_items:
file_items, changed = _reread_file_items(file_items)
_reread_result = _reread_file_items_result(file_items)
file_items, changed = _reread_result.data
ctx = _build_file_diff_text(changed)
if ctx:
out += f"\n\n{_get_context_marker()}\n\n{ctx}"
@@ -2484,9 +2447,6 @@ def _list_minimax_models_result(api_key: str) -> Result[list[str]]:
)
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
last = history[-1]