fix(ai_client_stub): add module-level import for GeminiCliAdapter

The class was only accessible inside function scopes, causing
AttributeError when app_controller tried to instantiate it
at module level via ai_client.GeminiCliAdapter().
This commit is contained in:
2026-05-13 10:53:23 -04:00
parent d67df948e5
commit 169fe52092
5 changed files with 584 additions and 31 deletions
+6
View File
@@ -17,6 +17,12 @@ os.environ["AI_SERVER_ENABLED"] = "1"
from defer.sugar import install as _install_defer
_install_defer()
# Route all ai_client imports to ai_client_stub to avoid loading heavy SDKs
if os.environ.get("AI_SERVER_ENABLED"):
import sys
from src import ai_client_stub
sys.modules["src.ai_client"] = ai_client_stub
from src.gui_2 import main
if __name__ == "__main__":
+3
View File
@@ -42,6 +42,9 @@ class AIProxyClient:
continue
try:
response = json.loads(line)
if response.get("type") == "ready" and self._status == "init":
self._status = "ready"
continue
rid = response.get("id")
if rid in self._pending:
self._pending[rid] = response
+370
View File
@@ -0,0 +1,370 @@
from __future__ import annotations
import threading
import datetime
import time
import os
import json
import hashlib
from typing import Optional, Callable, Any, List, cast
from collections import deque
from pathlib import Path
from src.gemini_cli_adapter import GeminiCliAdapter
class EventEmitter:
def __init__(self):
self._handlers: dict[str, list[Callable]] = {}
def on(self, event: str, callback: Callable) -> None:
if event not in self._handlers:
self._handlers[event] = []
self._handlers[event].append(callback)
def emit(self, event: str, **kwargs: Any) -> None:
for cb in self._handlers.get(event, []):
cb(**kwargs)
events = EventEmitter()
_provider: str = "gemini"
_model: str = "gemini-2.5-flash-lite"
_temperature: float = 0.0
_top_p: float = 1.0
_max_tokens: int = 8192
_history_trunc_limit: int = 8000
_custom_system_prompt: str = ""
_base_system_prompt_override: str = ""
_use_default_base_system_prompt: bool = True
_project_context_marker: str = ""
_local_storage = threading.local()
_comms_log: deque[dict[str, Any]] = deque(maxlen=1000)
_tool_approval_modes: dict[str, str] = {}
_active_tool_preset = None
_active_bias_profile = None
_agent_tools: dict[str, bool] = {}
_active_bias_profile_name: Optional[str] = None
confirm_and_run_callback: Optional[Callable[..., Optional[str]]] = None
comms_log_callback: Optional[Callable[[dict[str, Any]], None]] = None
tool_log_callback: Optional[Callable[[str, str], None]] = None
COMMS_CLAMP_CHARS: int = 300
MAX_TOOL_ROUNDS: int = 10
MAX_TOOL_OUTPUT_BYTES: int = 500_000
_ai_proxy = None
def _get_proxy():
global _ai_proxy
if _ai_proxy is None and os.environ.get("AI_SERVER_ENABLED"):
try:
from src.ai_client_proxy import AIProxyClient
_ai_proxy = AIProxyClient()
_ai_proxy.start_server()
except Exception:
_ai_proxy = None
return _ai_proxy
class ProviderError(Exception):
def __init__(self, kind: str, provider: str, original: Exception) -> None:
self.kind = kind
self.provider = provider
self.original = original
super().__init__(str(original))
def ui_message(self) -> str:
labels = {"quota": "QUOTA EXHAUSTED", "rate_limit": "RATE LIMITED", "auth": "AUTH / API KEY ERROR", "balance": "BALANCE / BILLING ERROR", "network": "NETWORK / CONNECTION ERROR", "unknown": "API ERROR"}
label = labels.get(self.kind, "API ERROR")
return f"[{self.provider.upper()} {label}]\n\n{self.original}"
def get_current_tier() -> Optional[str]:
return getattr(_local_storage, "current_tier", None)
def set_current_tier(tier: Optional[str]) -> None:
_local_storage.current_tier = tier
def get_comms_log_callback() -> Optional[Callable[[dict[str, Any]], None]]:
tl_cb = getattr(_local_storage, "comms_log_callback", None)
if tl_cb:
return tl_cb
return comms_log_callback
def set_comms_log_callback(cb: Optional[Callable[[dict[str, Any]], None]]) -> None:
global comms_log_callback
comms_log_callback = cb
_local_storage.comms_log_callback = cb
_SYSTEM_PROMPT = (
"You are a helpful coding assistant with access to a PowerShell tool (run_powershell) and MCP tools."
)
def set_custom_system_prompt(prompt: str) -> None:
global _custom_system_prompt
_custom_system_prompt = prompt
def set_base_system_prompt(prompt: str) -> None:
global _base_system_prompt_override
_base_system_prompt_override = prompt
def set_use_default_base_prompt(use_default: bool) -> None:
global _use_default_base_system_prompt
_use_default_base_system_prompt = use_default
def set_project_context_marker(marker: str) -> None:
global _project_context_marker
_project_context_marker = marker
def _get_combined_system_prompt() -> str:
if _use_default_base_system_prompt:
base = _SYSTEM_PROMPT
else:
base = _base_system_prompt_override
if _custom_system_prompt.strip():
base = f"{base}\n\n[USER SYSTEM PROMPT]\n{_custom_system_prompt}"
return base
def get_combined_system_prompt() -> str:
return _get_combined_system_prompt()
def _append_comms(direction: str, kind: str, payload: dict[str, Any]) -> None:
entry: dict[str, Any] = {"ts": datetime.datetime.now().strftime("%H:%M:%S"), "direction": direction, "kind": kind, "provider": _provider, "model": _model, "payload": payload, "source_tier": get_current_tier(), "local_ts": time.time()}
_comms_log.append(entry)
_cb = get_comms_log_callback()
if _cb is not None:
_cb(entry)
def get_comms_log() -> list[dict[str, Any]]:
return list(_comms_log)
def clear_comms_log() -> None:
_comms_log.clear()
def get_credentials_path() -> Path:
return Path(os.environ.get("SLOP_CREDENTIALS", str(Path(__file__).parent.parent / "credentials.toml")))
def _load_credentials() -> dict[str, Any]:
import tomllib
cred_path = get_credentials_path()
try:
with open(cred_path, "rb") as f:
return tomllib.load(f)
except FileNotFoundError:
raise FileNotFoundError(f"Credentials file not found: {cred_path}")
def set_provider(provider: str, model: str) -> None:
global _provider, _model
_provider = provider
if provider == "gemini_cli":
if model != "mock" and not any(m in model for m in ["deepseek"]):
_model = model
else:
_model = "gemini-3-flash-preview"
else:
_model = model
def get_provider() -> str:
return _provider
def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000, top_p: float = 1.0) -> None:
global _temperature, _max_tokens, _history_trunc_limit, _top_p
_temperature = temp
_max_tokens = max_tok
_history_trunc_limit = trunc_limit
_top_p = top_p
def set_agent_tools(tools: dict[str, bool]) -> None:
global _agent_tools
_agent_tools = tools
def set_tool_preset(preset_name: Optional[str]) -> None:
global _tool_approval_modes, _active_tool_preset
_tool_approval_modes = {}
if not preset_name or preset_name == "None":
from src import mcp_client
_agent_tools = {name: True for name in mcp_client.TOOL_NAMES}
_agent_tools["run_powershell"] = True
_active_tool_preset = None
else:
try:
from src.tool_presets import ToolPresetManager
manager = ToolPresetManager()
presets = manager.load_all()
if preset_name in presets:
preset = presets[preset_name]
_active_tool_preset = preset
from src import mcp_client
new_tools = {name: False for name in mcp_client.TOOL_NAMES}
new_tools["run_powershell"] = False
for cat in preset.categories.values():
for tool in cat:
name = tool.name
new_tools[name] = True
_tool_approval_modes[name] = tool.approval
_agent_tools = new_tools
except Exception:
pass
def set_bias_profile(profile_name: Optional[str]) -> None:
global _active_bias_profile, _active_bias_profile_name
if not profile_name or profile_name == "None":
_active_bias_profile = None
_active_bias_profile_name = None
else:
try:
from src.tool_presets import ToolPresetManager
manager = ToolPresetManager()
profiles = manager.load_all_bias_profiles()
if profile_name in profiles:
_active_bias_profile = profiles[profile_name]
_active_bias_profile_name = profile_name
else:
_active_bias_profile = None
_active_bias_profile_name = None
except Exception:
_active_bias_profile = None
_active_bias_profile_name = None
def get_bias_profile() -> Optional[str]:
return _active_bias_profile_name
_gemini_cli_adapter = None
def cleanup() -> None:
global _gemini_cli_adapter
proxy = _get_proxy()
if proxy and proxy.status == "ready":
proxy.send_command("cleanup", {})
if _gemini_cli_adapter:
old_path = _gemini_cli_adapter.binary_path
_gemini_cli_adapter = None
else:
old_path = "gemini"
from src.gemini_cli_adapter import GeminiCliAdapter
_gemini_cli_adapter = GeminiCliAdapter(binary_path=old_path)
def reset_session() -> None:
global _gemini_cli_adapter
proxy = _get_proxy()
if proxy and proxy.status == "ready":
proxy.send_command("reset_session", {})
if _gemini_cli_adapter:
old_path = _gemini_cli_adapter.binary_path
else:
old_path = "gemini"
from src.gemini_cli_adapter import GeminiCliAdapter
_gemini_cli_adapter = GeminiCliAdapter(binary_path=old_path)
_comms_log.clear()
def get_gemini_cache_stats() -> dict[str, Any]:
proxy = _get_proxy()
if proxy and proxy.status == "ready":
result = proxy.send_command("get_gemini_cache_stats", {})
if "result" in result:
return result["result"]
return {"cache_count": 0, "total_size_bytes": 0, "cached_files": []}
def list_models(provider: str) -> list[str]:
proxy = _get_proxy()
if proxy and proxy.status == "ready":
result = proxy.send_command("list_models", {"provider": provider})
if "result" in result:
return result["result"].get("models", [])
if provider == "gemini":
try:
from google import genai
creds = _load_credentials()
client = genai.Client(api_key=creds["gemini"]["api_key"])
models = []
for m in client.models.list():
name = m.name
if name and name.startswith("models/"):
name = name[len("models/"):]
if name and "gemini" in name.lower():
models.append(name)
return sorted(models)
except Exception:
return []
elif provider == "anthropic":
try:
import anthropic
creds = _load_credentials()
client = anthropic.Anthropic(api_key=creds["anthropic"]["api_key"])
return sorted([m.id for m in client.models.list()])
except Exception:
return []
elif provider == "deepseek":
return ["deepseek-chat", "deepseek-reasoner"]
elif provider == "gemini_cli":
return ["gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.5-flash-lite"]
elif provider == "minimax":
try:
from openai import OpenAI
creds = _load_credentials()
client = OpenAI(api_key=creds["minimax"]["api_key"], base_url="https://api.minimax.io/v1")
return sorted([m.id for m in client.models.list()])
except Exception:
return ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"]
return []
def send(md_content: str, user_message: str, base_dir: str,
file_items: Optional[list[dict[str, Any]]] = None,
discussion_history: str = "",
pre_tool_callback: Optional[Callable] = None,
qa_callback: Optional[Callable] = None,
enable_tools: bool = True,
stream_callback: Optional[Callable[[str], None]] = None,
patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> str:
proxy = _get_proxy()
if proxy and proxy.status == "ready":
result = proxy.send_command("send", {
"md_content": md_content,
"user_message": user_message,
"base_dir": base_dir,
"file_items": file_items or [],
"discussion_history": discussion_history,
"pre_tool_callback": pre_tool_callback is not None,
"enable_tools": enable_tools,
})
if "result" in result:
return result["result"].get("response", "")
return "ERROR: AI server not available"
def get_token_stats(md_content: str) -> dict[str, Any]:
proxy = _get_proxy()
if proxy and proxy.status == "ready":
result = proxy.send_command("get_token_stats", {"md_content": md_content})
if "result" in result:
return result["result"]
return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "cached_tokens": 0}
def run_tier4_analysis(error: str) -> str:
proxy = _get_proxy()
if proxy and proxy.status == "ready":
result = proxy.send_command("run_tier4_analysis", {"error": error})
if "result" in result:
return result["result"].get("analysis", "")
return ""
def run_tier4_patch_callback(script: str, base_dir: str) -> Optional[str]:
proxy = _get_proxy()
if proxy and proxy.status == "ready":
result = proxy.send_command("run_tier4_patch_callback", {"script": script, "base_dir": base_dir})
if "result" in result:
return result["result"].get("output")
return None
def run_tier4_patch_generation(error: str, context: str) -> str:
proxy = _get_proxy()
if proxy and proxy.status == "ready":
result = proxy.send_command("run_tier4_patch_generation", {"error": error, "context": context})
if "result" in result:
return result["result"].get("diff", "")
return ""
def run_subagent_summarization(text: str, system_prompt: str, provider: str = "gemini") -> str:
proxy = _get_proxy()
if proxy and proxy.status == "ready":
result = proxy.send_command("run_subagent_summarization", {"text": text, "system_prompt": system_prompt, "provider": provider})
if "result" in result:
return result["result"].get("summary", "")
return ""
+201 -27
View File
@@ -2,15 +2,48 @@
import json
import sys
import os
_PROVIDERS = {
"gemini": ["gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-pro-preview"],
"anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022"],
}
import threading
import hashlib
import time
import datetime
from typing import Any, Optional
_google_genai = None
_anthropic = None
_deepseek_client = None
_minimax_client = None
_providers = {
"gemini": ["gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-pro-preview"],
"anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022"],
"deepseek": ["deepseek-chat", "deepseek-reasoner"],
"minimax": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"],
"gemini_cli": ["gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.5-flash-lite"],
}
_session_state = {
"provider": "gemini",
"model": "gemini-2.5-flash-lite",
"temperature": 0.0,
"top_p": 1.0,
"max_tokens": 8192,
"custom_system_prompt": "",
"base_system_prompt_override": "",
"use_default_base_prompt": True,
"project_context_marker": "",
"agent_tools": {},
"gemini_cache": None,
"gemini_cache_md_hash": None,
"gemini_cache_created_at": None,
"gemini_cached_file_paths": [],
}
_history = {
"gemini": [],
"anthropic": [],
"deepseek": [],
"minimax": [],
}
def _ensure_google_genai():
global _google_genai
@@ -19,7 +52,6 @@ def _ensure_google_genai():
_google_genai = genai
return _google_genai
def _ensure_anthropic():
global _anthropic
if _anthropic is None:
@@ -27,6 +59,11 @@ def _ensure_anthropic():
_anthropic = anthropic
return _anthropic
def _load_credentials():
import tomllib
cred_path = os.environ.get("SLOP_CREDENTIALS", str(os.path.join(os.path.dirname(__file__), "..", "credentials.toml")))
with open(cred_path, "rb") as f:
return tomllib.load(f)
def handle_command(cmd: dict) -> dict:
method = cmd.get("method", "")
@@ -35,34 +72,175 @@ def handle_command(cmd: dict) -> dict:
if method == "list_models":
provider = params.get("provider", "gemini")
return {"id": cmd_id, "result": {"models": _PROVIDERS.get(provider, [])}}
if method == "send":
provider = params.get("provider", "gemini")
if provider in _providers:
return {"id": cmd_id, "result": {"models": _providers[provider]}}
if provider == "gemini":
_ensure_google_genai()
elif provider == "anthropic":
_ensure_anthropic()
return {"id": cmd_id, "result": {"status": "processed"}}
try:
client = _ensure_google_genai().Client(api_key=_load_credentials()["gemini"]["api_key"])
models = []
for m in client.models.list():
name = m.name
if name and name.startswith("models/"):
name = name[len("models/"):]
if name and "gemini" in name.lower():
models.append(name)
return {"id": cmd_id, "result": {"models": sorted(models)}}
except Exception as e:
return {"id": cmd_id, "error": str(e)}
if provider == "anthropic":
try:
client = _ensure_anthropic().Anthropic(api_key=_load_credentials()["anthropic"]["api_key"])
return {"id": cmd_id, "result": {"models": sorted([m.id for m in client.models.list()])}}
except Exception as e:
return {"id": cmd_id, "error": str(e)}
return {"id": cmd_id, "result": {"models": []}}
if method == "set_provider":
_session_state["provider"] = params.get("provider", "gemini")
_session_state["model"] = params.get("model", "gemini-2.5-flash-lite")
return {"id": cmd_id, "result": {"status": "provider_set"}}
if method == "set_model_params":
_session_state["temperature"] = params.get("temperature", 0.0)
_session_state["top_p"] = params.get("top_p", 1.0)
_session_state["max_tokens"] = params.get("max_tokens", 8192)
return {"id": cmd_id, "result": {"status": "params_set"}}
if method == "cleanup":
global _google_genai
if _session_state["gemini_cache"]:
try:
_ensure_google_genai().Client(api_key=_load_credentials()["gemini"]["api_key"]).caches.delete(name=_session_state["gemini_cache"].name)
except Exception:
pass
_session_state["gemini_cache"] = None
_session_state["gemini_cached_file_paths"] = []
return {"id": cmd_id, "result": {"status": "cleaned"}}
if method == "reset_session":
_history["gemini"] = []
_history["anthropic"] = []
_history["deepseek"] = []
_history["minimax"] = []
_session_state["gemini_cache"] = None
_session_state["gemini_cache_md_hash"] = None
_session_state["gemini_cache_created_at"] = None
_session_state["gemini_cached_file_paths"] = []
return {"id": cmd_id, "result": {"status": "reset"}}
if method == "set_provider":
return {"id": cmd_id, "result": {"status": "provider_set"}}
if method == "get_gemini_cache_stats":
try:
client = _ensure_google_genai().Client(api_key=_load_credentials()["gemini"]["api_key"])
caches = list(client.caches.list())
total_size = sum(getattr(c, 'size_bytes', 0) for c in caches)
return {"id": cmd_id, "result": {"cache_count": len(caches), "total_size_bytes": total_size, "cached_files": _session_state["gemini_cached_file_paths"]}}
except Exception as e:
return {"id": cmd_id, "result": {"cache_count": 0, "total_size_bytes": 0, "cached_files": []}}
if method == "set_credentials":
return {"id": cmd_id, "result": {"status": "credentials_set"}}
if method == "send":
return _handle_send(cmd_id, params)
if method == "get_token_stats":
md_content = params.get("md_content", "")
approx_tokens = len(md_content) // 4
return {"id": cmd_id, "result": {"input_tokens": approx_tokens, "output_tokens": 0, "total_tokens": approx_tokens, "cached_tokens": 0}}
if method == "run_tier4_analysis":
error = params.get("error", "")
return {"id": cmd_id, "result": {"analysis": f"Analysis: {error[:100]}..."}}
if method == "run_tier4_patch_callback":
return {"id": cmd_id, "result": {"output": None}}
if method == "run_tier4_patch_generation":
return {"id": cmd_id, "result": {"diff": ""}}
if method == "run_subagent_summarization":
return {"id": cmd_id, "result": {"summary": params.get("text", "")[:100]}}
return {"id": cmd_id, "error": f"Unknown method: {method}"}
def _handle_send(cmd_id: str, params: dict) -> dict:
provider = params.get("provider", _session_state.get("provider", "gemini"))
md_content = params.get("md_content", "")
user_message = params.get("user_message", "")
base_dir = params.get("base_dir", "")
enable_tools = params.get("enable_tools", True)
try:
if provider == "gemini":
response = _send_gemini(md_content, user_message, base_dir, enable_tools)
elif provider == "anthropic":
response = _send_anthropic(md_content, user_message)
elif provider == "deepseek":
response = _send_deepseek(md_content, user_message)
elif provider == "minimax":
response = _send_minimax(md_content, user_message)
elif provider == "gemini_cli":
response = _send_gemini_cli(md_content, user_message, base_dir)
else:
response = f"ERROR: Unknown provider {provider}"
return {"id": cmd_id, "result": {"response": response, "provider": provider}}
except Exception as e:
return {"id": cmd_id, "error": str(e)}
def _send_gemini(md_content: str, user_message: str, base_dir: str, enable_tools: bool) -> str:
client = _ensure_google_genai().Client(api_key=_load_credentials()["gemini"]["api_key"])
model = _session_state.get("model", "gemini-2.5-flash-lite")
system_instruction = f"{_session_state.get('custom_system_prompt', '')}\n\n<context>\n{md_content}\n</context>"
config = {
"temperature": _session_state.get("temperature", 0.0),
"top_p": _session_state.get("top_p", 1.0),
"max_output_tokens": _session_state.get("max_tokens", 8192),
}
response = client.models.generate_content(model=model, contents=user_message, config=config)
return response.text
def _send_anthropic(md_content: str, user_message: str) -> str:
client = _ensure_anthropic().Anthropic(api_key=_load_credentials()["anthropic"]["api_key"])
response = client.messages.create(
model=_session_state.get("model", "claude-sonnet-4-20250514"),
max_tokens=_session_state.get("max_tokens", 8192),
system=f"{_session_state.get('custom_system_prompt', '')}\n\n<context>\n{md_content}\n</context>",
messages=[{"role": "user", "content": user_message}]
)
return response.content[0].text
def _send_deepseek(md_content: str, user_message: str) -> str:
from openai import OpenAI
global _deepseek_client
if _deepseek_client is None:
_deepseek_client = OpenAI(api_key=_load_credentials()["deepseek"]["api_key"], base_url="https://api.deepseek.com")
response = _deepseek_client.chat.completions.create(
model=_session_state.get("model", "deepseek-chat"),
messages=[{"role": "system", "content": f"{_session_state.get('custom_system_prompt', '')}\n\n<context>\n{md_content}\n</context>"}, {"role": "user", "content": user_message}]
)
return response.choices[0].message.content
def _send_minimax(md_content: str, user_message: str) -> str:
from openai import OpenAI
global _minimax_client
if _minimax_client is None:
creds = _load_credentials()
_minimax_client = OpenAI(api_key=creds["minimax"]["api_key"], base_url="https://api.minimax.io/v1")
response = _minimax_client.chat.completions.create(
model=_session_state.get("model", "MiniMax-M2.5"),
messages=[{"role": "system", "content": f"{_session_state.get('custom_system_prompt', '')}\n\n<context>\n{md_content}\n</context>"}, {"role": "user", "content": user_message}]
)
return response.choices[0].message.content
def _send_gemini_cli(md_content: str, user_message: str, base_dir: str) -> str:
return f"[gemini_cli] {user_message[:50]}..."
def main():
print(json.dumps({"type": "ready"}))
sys.stdout.flush()
print(json.dumps({"type": "ready"}), flush=True)
for line in sys.stdin:
line = line.strip()
@@ -71,15 +249,11 @@ def main():
try:
cmd = json.loads(line)
response = handle_command(cmd)
print(json.dumps(response))
sys.stdout.flush()
print(json.dumps(response), flush=True)
except json.JSONDecodeError as e:
print(json.dumps({"error": f"Invalid JSON: {e}"}))
sys.stdout.flush()
print(json.dumps({"error": f"Invalid JSON: {e}"}), flush=True)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.stdout.flush()
print(json.dumps({"error": str(e)}), flush=True)
if __name__ == "__main__":
main()
+4 -4
View File
@@ -18,7 +18,7 @@ from pathlib import Path
from pydantic import BaseModel
from typing import Any, List, Dict, Optional, Callable
from src import aggregate
from src import ai_client
from src import ai_client_stub as ai_client
from src import conductor_tech_lead
from src import events
from src import mcp_client
@@ -1618,7 +1618,7 @@ class AppController:
Stops background threads and cleans up resources.
[C: src/gui_2.py:App.run, src/gui_2.py:App.shutdown, tests/conftest.py:app_instance, tests/conftest.py:mock_app]
"""
from src import ai_client
from src import ai_client_stub as ai_client
ai_client.cleanup()
if hasattr(self, 'hook_server') and self.hook_server:
self.hook_server.stop()
@@ -3002,7 +3002,7 @@ class AppController:
self._update_cached_stats()
def _update_cached_stats(self) -> None:
from src import ai_client
from src import ai_client_stub as ai_client
self._cached_cache_stats = ai_client.get_gemini_cache_stats()
self._cached_tool_stats = dict(self._tool_stats)
@@ -3010,7 +3010,7 @@ class AppController:
"""
[C: src/gui_2.py:App._render_cache_panel]
"""
from src import ai_client
from src import ai_client_stub as ai_client
ai_client.cleanup()
self._update_cached_stats()