refactor(app_controller): migrate _fetch_models.do_fetch to per-provider Result (Phase 6 Group 6.4)

Replaces per-provider logging.debug body with _list_models_for_provider_result
SDK-boundary helper. Aggregates per-provider failures into self._model_fetch_errors
and returns Result with aggregated errors. Stderr summary on partial failure.

The SDK boundary (ai_client.list_models call) is the canonical place to
catch vendor exceptions and convert to ErrorInfo(kind=NETWORK), per
error_handling.md §'Boundary Types'.

Audit: INTERNAL_SILENT_SWALLOW for src/app_controller.py: 23 -> 22.
This commit is contained in:
ed
2026-06-19 15:56:53 -04:00
parent fd91c83a0c
commit 50750f3183
2 changed files with 106 additions and 10 deletions
+45 -10
View File
@@ -845,6 +845,11 @@ class AppController:
self._inject_preview_error: Optional[ErrorInfo] = None
self._mcp_config_parse_error: Optional[ErrorInfo] = None
self._save_project_error: Optional[ErrorInfo] = None
# --- Per-provider model-fetch errors (Phase 6: Group 6.4) ---
# Aggregated per-provider failures from _fetch_models. The map is the
# durable data plane for sub-track 4 GUI to display in the models panel;
# stderr write IS the visible-but-incomplete drain (full drain = GUI).
self._model_fetch_errors: Dict[str, ErrorInfo] = {}
# --- Shared background pool + proactive warmup (startup_speedup_20260606) ---
self._io_pool = make_io_pool()
_install_sigint_exit_handler(self)
@@ -3322,6 +3327,27 @@ class AppController:
#region: AI Settings
def _list_models_for_provider_result(self, p: str) -> "Result[List[str]]":
"""SDK boundary (Phase 6 Group 6.4): wrap ai_client.list_models(p).
Returns Result[List[str]]. On failure: any SDK exception
(OSError/ValueError/TypeError/KeyError/AttributeError/RuntimeError/etc.)
-> ErrorInfo(kind=NETWORK, original=e).
Per the styleguide §"Boundary Types", SDK calls are the canonical
boundary: catch vendor exceptions and convert to ErrorInfo. The
per-provider accumulation in self._model_fetch_errors makes the
aggregated failures visible to sub-track 4 GUI."""
try:
models = ai_client.list_models(p)
return Result(data=list(models) if models else [])
except Exception as e:
return Result(data=[], errors=[ErrorInfo(
kind=ErrorKind.NETWORK,
message=str(e),
source=f"app_controller._list_models_for_provider_result[{p}]",
original=e,
)])
def _fetch_models(self, provider: str) -> None:
"""
[C: src/gui_2.py:App.run]
@@ -3336,12 +3362,16 @@ class AppController:
self.ai_status = "fetching models..."
def do_fetch() -> Result[None]:
aggregated_errors: List[ErrorInfo] = []
try:
for p in ai_client.PROVIDERS:
try:
self.all_available_models[p] = ai_client.list_models(p)
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
logging.getLogger(__name__).debug("list_models failed for %s: %s", p, e, extra={"source": "app_controller._fetch_models.do_fetch.per_provider"})
result = self._list_models_for_provider_result(p)
if result.ok:
self.all_available_models[p] = result.data
else:
err = result.errors[0]
self._model_fetch_errors[p] = err
aggregated_errors.append(err)
self.all_available_models[p] = []
models_list = self.all_available_models.get(provider, [])
@@ -3351,17 +3381,22 @@ class AppController:
ai_client.set_provider(self._current_provider, self.current_model)
if self.ai_status == "fetching models...":
self.ai_status = f"models loaded: {len(models_list)}"
if aggregated_errors:
summary = f"{len(aggregated_errors)} provider(s) failed: " + "; ".join(e.message for e in aggregated_errors)
sys.stderr.write(f"model fetch partial failure: {summary}\n")
sys.stderr.flush()
return Result(data=None, errors=aggregated_errors)
return OK
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
logging.getLogger(__name__).debug("model fetch error: %s", e, extra={"source": "app_controller._fetch_models.do_fetch"})
if self.ai_status == "fetching models...":
self.ai_status = f"model fetch error: {e}"
return Result(data=None, errors=[ErrorInfo(
except Exception as e:
err = ErrorInfo(
kind=ErrorKind.INTERNAL,
message=str(e),
source="app_controller._fetch_models.do_fetch",
original=e,
)])
)
if self.ai_status == "fetching models...":
self.ai_status = f"model fetch error: {e}"
return Result(data=None, errors=[err])
self.submit_io(do_fetch)
def _apply_preset(self, name: str, scope: str) -> None: