ai client pass (in gemini region)

This commit is contained in:
ed
2026-06-13 20:49:37 -04:00
parent 94ab6dcc6f
commit 5030bd848f
+86 -133
View File
@@ -647,12 +647,12 @@ def _gemini_tool_declaration() -> Optional[types.Tool]:
description=pdef.get("description", ""), description=pdef.get("description", ""),
) )
declarations.append(types.FunctionDeclaration( declarations.append(types.FunctionDeclaration(
name=tool_def["name"], name = tool_def["name"],
description=tool_def["description"], description = tool_def["description"],
parameters=types.Schema( parameters = types.Schema(
type=types.Type.OBJECT, type = types.Type.OBJECT,
properties=props, properties = props,
required=params.get("required", []), required = params.get("required", []),
), ),
)) ))
return types.Tool(function_declarations=declarations) if declarations else None return types.Tool(function_declarations=declarations) if declarations else None
@@ -705,12 +705,9 @@ async def _execute_tool_calls_concurrently(
tier = get_current_tier() tier = get_current_tier()
tasks = [] tasks = []
for fc in calls: for fc in calls:
if provider == "gemini": if provider == "gemini": name, args, call_id = fc.name, dict(fc.args), fc.name # Gemini 1.0.0 doesn't have call IDs in types.Part
name, args, call_id = fc.name, dict(fc.args), fc.name # Gemini 1.0.0 doesn't have call IDs in types.Part elif provider == "gemini_cli": name, args, call_id = cast(str, fc.get("name")), cast(dict[str, Any], fc.get("args", {})), cast(str, fc.get("id"))
elif provider == "gemini_cli": elif provider == "anthropic": name, args, call_id = cast(str, getattr(fc, "name")), cast(dict[str, Any], getattr(fc, "input")), cast(str, getattr(fc, "id"))
name, args, call_id = cast(str, fc.get("name")), cast(dict[str, Any], fc.get("args", {})), cast(str, fc.get("id"))
elif provider == "anthropic":
name, args, call_id = cast(str, getattr(fc, "name")), cast(dict[str, Any], getattr(fc, "input")), cast(str, getattr(fc, "id"))
elif provider == "deepseek": elif provider == "deepseek":
tool_info = fc.get("function", {}) tool_info = fc.get("function", {})
name = cast(str, tool_info.get("name")) name = cast(str, tool_info.get("name"))
@@ -809,17 +806,12 @@ def run_with_tool_loop(
if history_lock is not None and history is not None: if history_lock is not None and history is not None:
with history_lock: with history_lock:
msg: dict[str, Any] = {"role": "assistant", "content": response.text or None} msg: dict[str, Any] = {"role": "assistant", "content": response.text or None}
if reasoning_content: if reasoning_content: msg["reasoning_content"] = reasoning_content
msg["reasoning_content"] = reasoning_content if response.tool_calls: msg["tool_calls"] = response.tool_calls
if response.tool_calls:
msg["tool_calls"] = response.tool_calls
history.append(msg) history.append(msg)
if not response.tool_calls: if not response.tool_calls: break
break if on_pre_dispatch is not None: _adjusted_calls = on_pre_dispatch(_round_idx, response.tool_calls)
if on_pre_dispatch is not None: else: _adjusted_calls = response.tool_calls
_adjusted_calls = on_pre_dispatch(_round_idx, response.tool_calls)
else:
_adjusted_calls = response.tool_calls
try: try:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
results = asyncio.run_coroutine_threadsafe( results = asyncio.run_coroutine_threadsafe(
@@ -840,8 +832,7 @@ def run_with_tool_loop(
"tool_call_id": call_id, "tool_call_id": call_id,
"content": str(out) if out else "", "content": str(out) if out else "",
}) })
if trim_func is not None: if trim_func is not None: trim_func(history)
trim_func(history)
return response_text return response_text
async def _execute_single_tool_call_async( async def _execute_single_tool_call_async(
@@ -891,7 +882,7 @@ async def _execute_single_tool_call_async(
set_current_tier(tier) set_current_tier(tier)
out = "" out = ""
tool_executed = False tool_executed = False
events.emit("tool_execution", payload={"status": "started", "tool": name, "args": args, "round": r_idx}) events.emit("tool_execution", payload = {"status": "started", "tool": name, "args": args, "round": r_idx})
# Check for auto approval mode # Check for auto approval mode
approval_mode = _tool_approval_modes.get(name, "ask") approval_mode = _tool_approval_modes.get(name, "ask")
@@ -906,10 +897,8 @@ async def _execute_single_tool_call_async(
elif pre_tool_callback: elif pre_tool_callback:
# pre_tool_callback is synchronous and might block for HITL # pre_tool_callback is synchronous and might block for HITL
res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback) res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback)
if res is None: if res is None: out = "USER REJECTED: tool execution cancelled"
out = "USER REJECTED: tool execution cancelled" else: out = res
else:
out = res
tool_executed = True tool_executed = True
if not tool_executed: if not tool_executed:
@@ -943,12 +932,9 @@ def _run_script(script: str, base_dir: str, qa_callback: Optional[Callable[[str]
if confirm_and_run_callback is None: if confirm_and_run_callback is None:
return "ERROR: no confirmation handler registered" return "ERROR: no confirmation handler registered"
result = confirm_and_run_callback(script, base_dir, qa_callback, patch_callback) result = confirm_and_run_callback(script, base_dir, qa_callback, patch_callback)
if result is None: if result is None: output = "USER REJECTED: command was not executed"
output = "USER REJECTED: command was not executed" else: output = result
else: if tool_log_callback is not None: tool_log_callback(script, output)
output = result
if tool_log_callback is not None:
tool_log_callback(script, output)
return output return output
def _truncate_tool_output(output: str) -> str: def _truncate_tool_output(output: str) -> str:
@@ -963,27 +949,22 @@ def _truncate_tool_output(output: str) -> str:
def _reread_file_items(file_items: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: 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. Re-reads file items from the filesystem if their modification times have changed.
Functional Purpose: Functional Purpose:
Iterates through context files, compares current filesystem mtime against cached mtime, 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 reads file contents if changes are detected, returning both the full refreshed set
and the subset of changed items. and the subset of changed items.
Parameters & Inputs: Parameters & Inputs: file_items (list[dict[str, Any]]): List of file dictionaries containing keys "path" and optionally "mtime", "content".
file_items (list[dict[str, Any]]): List of file dictionaries containing keys "path" and optionally "mtime", "content".
Returns: Returns: tuple[list[dict[str, Any]], list[dict[str, Any]]]: A tuple containing (refreshed_items, changed_items).
tuple[list[dict[str, Any]], list[dict[str, Any]]]: A tuple containing (refreshed_items, changed_items).
Immediate-Mode DAG / Thread Context: Immediate-Mode DAG / Thread Context:
Called by: _send_gemini Called by: _send_gemini
Calls: pathlib.Path.stat, pathlib.Path.read_text Calls: pathlib.Path.stat, pathlib.Path.read_text
SSDL: SSDL: `o-> [I:get_mtime] -> [B:changed?] -> [I:read_file] -> [T:diff_text]`
`o-> [I:get_mtime] -> [B:changed?] -> [I:read_file] -> [T:diff_text]`
Thread Boundaries: Thread Boundaries: Runs synchronously in the caller thread. Does synchronous blocking file system I/O.
Runs synchronously in the caller thread. Does synchronous blocking file system I/O.
""" """
refreshed: list[dict[str, Any]] = [] refreshed: list[dict[str, Any]] = []
changed: list[dict[str, Any]] = [] changed: list[dict[str, Any]] = []
@@ -1061,16 +1042,11 @@ def _build_file_diff_text(changed_items: list[dict[str, Any]]) -> str:
old_lines = old_content.splitlines(keepends=True) old_lines = old_content.splitlines(keepends=True)
diff = difflib.unified_diff(old_lines, new_lines, fromfile=str(path), tofile=str(path), lineterm="") diff = difflib.unified_diff(old_lines, new_lines, fromfile=str(path), tofile=str(path), lineterm="")
diff_text = "\n".join(diff) diff_text = "\n".join(diff)
if diff_text: if diff_text: parts.append(f"### `{path}` (diff)\n\n```diff\n{diff_text}\n```")
parts.append(f"### `{path}` (diff)\n\n```diff\n{diff_text}\n```") else: parts.append(f"### `{path}` (no changes detected)")
else:
parts.append(f"### `{path}` (no changes detected)")
return "\n\n---\n\n".join(parts) return "\n\n---\n\n".join(parts)
def _build_deepseek_tools() -> list[dict[str, Any]]: def _build_deepseek_tools() -> list[dict[str, Any]]:
"""
[C: tests/test_tool_access_exclusion.py:test_build_deepseek_tools_excludes_disabled]
"""
raw_tools: list[dict[str, Any]] = [] raw_tools: list[dict[str, Any]] = []
for spec in mcp_client.get_tool_schemas(): for spec in mcp_client.get_tool_schemas():
if _agent_tools.get(spec["name"], True): if _agent_tools.get(spec["name"], True):
@@ -1123,17 +1099,12 @@ def _get_deepseek_tools() -> list[dict[str, Any]]:
return _CACHED_DEEPSEEK_TOOLS return _CACHED_DEEPSEEK_TOOLS
def _content_block_to_dict(block: Any) -> dict[str, Any]: def _content_block_to_dict(block: Any) -> dict[str, Any]:
if isinstance(block, dict): if isinstance(block, dict): return block
return block if hasattr(block, "model_dump"): return cast(dict[str, Any], block.model_dump())
if hasattr(block, "model_dump"): if hasattr(block, "to_dict"): return cast(dict[str, Any], block.to_dict())
return cast(dict[str, Any], block.model_dump())
if hasattr(block, "to_dict"):
return cast(dict[str, Any], block.to_dict())
block_type = getattr(block, "type", None) block_type = getattr(block, "type", None)
if block_type == "text": if block_type == "text": return {"type": "text", "text": block.text}
return {"type": "text", "text": block.text} if block_type == "tool_use": return {"type": "tool_use", "id": getattr(block, "id"), "name": getattr(block, "name"), "input": getattr(block, "input")}
if block_type == "tool_use":
return {"type": "tool_use", "id": getattr(block, "id"), "name": getattr(block, "name"), "input": getattr(block, "input")}
return {"type": "text", "text": str(block)} return {"type": "text", "text": str(block)}
#endregion: File Context Building #endregion: File Context Building
@@ -1147,8 +1118,7 @@ _FILE_REFRESH_MARKER: str = _project_context_marker if _project_context_marker.s
def _estimate_message_tokens(msg: dict[str, Any]) -> int: def _estimate_message_tokens(msg: dict[str, Any]) -> int:
cached = msg.get("_est_tokens") cached = msg.get("_est_tokens")
if cached is not None: if cached is not None: return cast(int, cached)
return cast(int, cached)
total_chars = 0 total_chars = 0
content = msg.get("content", "") content = msg.get("content", "")
if isinstance(content, str): if isinstance(content, str):
@@ -1207,9 +1177,6 @@ def _strip_stale_file_refreshes(history: list[dict[str, Any]]) -> None:
_invalidate_token_estimate(msg) _invalidate_token_estimate(msg)
def _chunk_text(text: str, chunk_size: int) -> list[str]: def _chunk_text(text: str, chunk_size: int) -> list[str]:
"""
[C: src/rag_engine.py:RAGEngine._chunk_code, src/rag_engine.py:RAGEngine.index_file]
"""
return [text[i:i + chunk_size] for i in range(0, len(text), chunk_size)] return [text[i:i + chunk_size] for i in range(0, len(text), chunk_size)]
def _build_chunked_context_blocks(md_content: str) -> list[dict[str, Any]]: def _build_chunked_context_blocks(md_content: str) -> list[dict[str, Any]]:
@@ -1232,8 +1199,7 @@ def _strip_cache_controls(history: list[dict[str, Any]]) -> None:
def _add_history_cache_breakpoint(history: list[dict[str, Any]]) -> None: def _add_history_cache_breakpoint(history: list[dict[str, Any]]) -> None:
user_indices = [i for i, m in enumerate(history) if m.get("role") == "user"] user_indices = [i for i, m in enumerate(history) if m.get("role") == "user"]
if len(user_indices) < 2: if len(user_indices) < 2: return
return
target_idx = user_indices[-2] target_idx = user_indices[-2]
content = history[target_idx].get("content") content = history[target_idx].get("content")
if isinstance(content, list) and content: if isinstance(content, list) and content:
@@ -1255,8 +1221,7 @@ def _list_anthropic_models() -> list[str]:
creds = _load_credentials() creds = _load_credentials()
client = anthropic.Anthropic(api_key=creds["anthropic"]["api_key"]) client = anthropic.Anthropic(api_key=creds["anthropic"]["api_key"])
models: list[str] = [] models: list[str] = []
for m in client.models.list(): for m in client.models.list(): models.append(m.id)
models.append(m.id)
return sorted(models) return sorted(models)
except Exception as exc: except Exception as exc:
raise _classify_anthropic_error(exc) from exc raise _classify_anthropic_error(exc) from exc
@@ -1267,15 +1232,14 @@ def _ensure_anthropic_client() -> None:
if _anthropic_client is None: if _anthropic_client is None:
creds = _load_credentials() creds = _load_credentials()
_anthropic_client = anthropic.Anthropic( _anthropic_client = anthropic.Anthropic(
api_key=creds["anthropic"]["api_key"], api_key = creds["anthropic"]["api_key"],
default_headers={"anthropic-beta": "prompt-caching-2024-07-31"} default_headers = {"anthropic-beta": "prompt-caching-2024-07-31"}
) )
def _trim_anthropic_history(system_blocks: list[dict[str, Any]], history: list[dict[str, Any]]) -> int: def _trim_anthropic_history(system_blocks: list[dict[str, Any]], history: list[dict[str, Any]]) -> int:
_strip_stale_file_refreshes(history) _strip_stale_file_refreshes(history)
est = _estimate_prompt_tokens(system_blocks, history) est = _estimate_prompt_tokens(system_blocks, history)
if est <= _ANTHROPIC_MAX_PROMPT_TOKENS: if est <= _ANTHROPIC_MAX_PROMPT_TOKENS: return 0
return 0
dropped = 0 dropped = 0
while len(history) > 3 and est > _ANTHROPIC_MAX_PROMPT_TOKENS: while len(history) > 3 and est > _ANTHROPIC_MAX_PROMPT_TOKENS:
if history[1].get("role") == "assistant" and len(history) > 2 and history[2].get("role") == "user": if history[1].get("role") == "assistant" and len(history) > 2 and history[2].get("role") == "user":
@@ -1301,11 +1265,9 @@ def _trim_anthropic_history(system_blocks: list[dict[str, Any]], history: list[d
return dropped return dropped
def _repair_anthropic_history(history: list[dict[str, Any]]) -> None: def _repair_anthropic_history(history: list[dict[str, Any]]) -> None:
if not history: if not history: return
return
last = history[-1] last = history[-1]
if last.get("role") != "assistant": if last.get("role") != "assistant": return
return
content = last.get("content", []) content = last.get("content", [])
tool_use_ids: list[str] = [] tool_use_ids: list[str] = []
for block in content: for block in content:
@@ -1326,10 +1288,18 @@ def _repair_anthropic_history(history: list[dict[str, Any]]) -> None:
], ],
}) })
def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_items: list[dict[str, Any]] | None = None, discussion_history: str = "", pre_tool_callback: Optional[Callable[[str, str, Optional[Callable[[str], str]]], Optional[str]]] = None, qa_callback: Optional[Callable[[str], str]] = None, stream_callback: Optional[Callable[[str], None]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Result[str]: def _send_anthropic(
md_content: str,
user_message: str,
base_dir: str,
file_items: list[dict[str, Any]] | None = None,
discussion_history: str = "",
pre_tool_callback: Optional[Callable[[str, str, Optional[Callable[[str], str]]], Optional[str]]] = None,
qa_callback: Optional[Callable[[str], str]] = None,
stream_callback: Optional[Callable[[str], None]] = None,
patch_callback: Optional[Callable[[str, str], Optional[str]]] = None
) -> Result[str]:
""" """
[C: src/ai_server.py:_handle_send]
Functional Purpose: Functional Purpose:
Sends requests to Anthropic models, managing conversation history, prompt caching, token limits, and executing tool loops. Sends requests to Anthropic models, managing conversation history, prompt caching, token limits, and executing tool loops.
Parameters & Inputs: Parameters & Inputs:
@@ -1369,8 +1339,7 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
if _history_trunc_limit > 0 and isinstance(t_content, str) and len(t_content) > _history_trunc_limit: if _history_trunc_limit > 0 and isinstance(t_content, str) and len(t_content) > _history_trunc_limit:
block["content"] = t_content[:_history_trunc_limit] + "\n\n... [TRUNCATED BY SYSTEM TO SAVE TOKENS. Original output was too large.]" block["content"] = t_content[:_history_trunc_limit] + "\n\n... [TRUNCATED BY SYSTEM TO SAVE TOKENS. Original output was too large.]"
modified = True modified = True
if modified: if modified: _invalidate_token_estimate(msg)
_invalidate_token_estimate(msg)
_strip_cache_controls(_anthropic_history) _strip_cache_controls(_anthropic_history)
_repair_anthropic_history(_anthropic_history) _repair_anthropic_history(_anthropic_history)
_anthropic_history.append({"role": "user", "content": user_content}) _anthropic_history.append({"role": "user", "content": user_content})
@@ -1397,13 +1366,13 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
assert _anthropic_client is not None assert _anthropic_client is not None
if stream_callback: if stream_callback:
with _anthropic_client.messages.stream( with _anthropic_client.messages.stream(
model=_model, model = _model,
max_tokens=_max_tokens, max_tokens = _max_tokens,
temperature=_temperature, temperature = _temperature,
top_p=_top_p, top_p = _top_p,
system=cast(Iterable[anthropic.types.TextBlockParam], system_blocks), system = cast(Iterable[anthropic.types.TextBlockParam], system_blocks),
tools=cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()), tools = cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()),
messages=cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)), messages = cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)),
) as stream: ) as stream:
for event in stream: for event in stream:
if isinstance(event, anthropic.types.ContentBlockDeltaEvent) and event.delta.type == "text_delta": if isinstance(event, anthropic.types.ContentBlockDeltaEvent) and event.delta.type == "text_delta":
@@ -1411,13 +1380,13 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
response = stream.get_final_message() response = stream.get_final_message()
else: else:
response = _anthropic_client.messages.create( response = _anthropic_client.messages.create(
model=_model, model = _model,
max_tokens=_max_tokens, max_tokens = _max_tokens,
temperature=_temperature, temperature = _temperature,
top_p=_top_p, top_p = _top_p,
system=cast(Iterable[anthropic.types.TextBlockParam], system_blocks), system = cast(Iterable[anthropic.types.TextBlockParam], system_blocks),
tools=cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()), tools = cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()),
messages=cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)), messages = cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)),
) )
serialised_content = [_content_block_to_dict(b) for b in response.content] serialised_content = [_content_block_to_dict(b) for b in response.content]
_anthropic_history.append({ _anthropic_history.append({
@@ -1438,10 +1407,8 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
usage_dict["output_tokens"] = response.usage.output_tokens usage_dict["output_tokens"] = response.usage.output_tokens
cache_creation = getattr(response.usage, "cache_creation_input_tokens", None) cache_creation = getattr(response.usage, "cache_creation_input_tokens", None)
cache_read = getattr(response.usage, "cache_read_input_tokens", None) cache_read = getattr(response.usage, "cache_read_input_tokens", None)
if cache_creation is not None: if cache_creation is not None: usage_dict["cache_creation_input_tokens"] = cache_creation
usage_dict["cache_creation_input_tokens"] = cache_creation if cache_read is not None: usage_dict["cache_read_input_tokens"] = cache_read
if cache_read is not None:
usage_dict["cache_read_input_tokens"] = cache_read
events.emit("response_received", payload={"provider": "anthropic", "model": _model, "usage": usage_dict, "round": round_idx}) events.emit("response_received", payload={"provider": "anthropic", "model": _model, "usage": usage_dict, "round": round_idx})
_append_comms("IN", "response", { _append_comms("IN", "response", {
"round": round_idx, "round": round_idx,
@@ -1450,10 +1417,8 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
"tool_calls": tool_use_blocks, "tool_calls": tool_use_blocks,
"usage": usage_dict, "usage": usage_dict,
}) })
if response.stop_reason != "tool_use" or not tool_use_blocks: if response.stop_reason != "tool_use" or not tool_use_blocks: break
break if round_idx > MAX_TOOL_ROUNDS: break
if round_idx > MAX_TOOL_ROUNDS:
break
# Execute tools concurrently # Execute tools concurrently
try: try:
@@ -1522,12 +1487,8 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
#region: Gemini Provider #region: Gemini Provider
def get_gemini_cache_stats() -> dict[str, Any]: def get_gemini_cache_stats() -> dict[str, Any]:
"""
[C: src/app_controller.py:AppController._recalculate_session_usage, src/app_controller.py:AppController._update_cached_stats, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_gemini_metrics.py:test_get_gemini_cache_stats_with_mock_client]
"""
_ensure_gemini_client() _ensure_gemini_client()
if not _gemini_client: if not _gemini_client: return {"cache_count": 0, "total_size_bytes": 0, "cached_files": []}
return {"cache_count": 0, "total_size_bytes": 0, "cached_files": []}
caches_iterator = _gemini_client.caches.list() caches_iterator = _gemini_client.caches.list()
caches = list(caches_iterator) caches = list(caches_iterator)
total_size_bytes = sum(getattr(c, 'size_bytes', 0) for c in caches) total_size_bytes = sum(getattr(c, 'size_bytes', 0) for c in caches)
@@ -1554,18 +1515,13 @@ def _list_gemini_models(api_key: str) -> list[str]:
models: list[str] = [] models: list[str] = []
for m in client.models.list(): for m in client.models.list():
name = m.name name = m.name
if name and name.startswith("models/"): if name and name.startswith("models/"): name = name[len("models/"):]
name = name[len("models/"):] if name and "gemini" in name.lower(): models.append(name)
if name and "gemini" in name.lower():
models.append(name)
return sorted(models) return sorted(models)
except Exception as exc: except Exception as exc:
raise _classify_gemini_error(exc) from exc raise _classify_gemini_error(exc) from exc
def _ensure_gemini_client() -> None: def _ensure_gemini_client() -> None:
"""
[C: src/rag_engine.py:GeminiEmbeddingProvider.embed]
"""
global _gemini_client global _gemini_client
genai = _require_warmed("google.genai") genai = _require_warmed("google.genai")
if _gemini_client is None: if _gemini_client is None:
@@ -1574,12 +1530,9 @@ def _ensure_gemini_client() -> None:
def _get_gemini_history_list(chat: Any | None) -> list[Any]: def _get_gemini_history_list(chat: Any | None) -> list[Any]:
if not chat: return [] if not chat: return []
if hasattr(chat, "_history"): if hasattr(chat, "_history"): return cast(list[Any], chat._history)
return cast(list[Any], chat._history) if hasattr(chat, "history"): return cast(list[Any], chat.history)
if hasattr(chat, "history"): if hasattr(chat, "get_history"): return cast(list[Any], chat.get_history())
return cast(list[Any], chat.history)
if hasattr(chat, "get_history"):
return cast(list[Any], chat.get_history())
return [] return []
def _send_gemini(md_content: str, user_message: str, base_dir: str, def _send_gemini(md_content: str, user_message: str, base_dir: str,
@@ -1589,14 +1542,13 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str,
qa_callback: Optional[Callable[[str], str]] = None, qa_callback: Optional[Callable[[str], str]] = None,
enable_tools: bool = True, enable_tools: bool = True,
stream_callback: Optional[Callable[[str], None]] = None, stream_callback: Optional[Callable[[str], None]] = None,
patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Result[str]: patch_callback: Optional[Callable[[str, str], Optional[str]]] = None
) -> Result[str]:
""" """
[C: src/ai_server.py:_handle_send, tests/test_tier4_interceptor.py:test_gemini_provider_passes_qa_callback_to_run_script]
Functional Purpose: Sends requests to Gemini via google-genai SDK, handling context caching, chat history, and tools. Functional Purpose: Sends requests to Gemini via google-genai SDK, handling context caching, chat history, and tools.
Parameters & Inputs: md_content, user_message, base_dir, file_items, discussion_history, callbacks, enable_tools. Parameters & Inputs: md_content, user_message, base_dir, file_items, discussion_history, callbacks, enable_tools.
Immediate-Mode DAG / Thread Context: Called by: send; Calls: _ensure_gemini_client, client.caches.create, client.chats.create, run_with_tool_loop Immediate-Mode DAG / Thread Context: Called by: send; Calls: _ensure_gemini_client, client.caches.create, client.chats.create, run_with_tool_loop
SSDL: SSDL: [I:_ensure_gemini_client] -> [B:Cache Changed?] -> [I:client.caches.create] -> [I:client.chats.create] -> [T:Result]
[I:_ensure_gemini_client] -> [B:Cache Changed?] -> [I:client.caches.create] -> [I:client.chats.create] -> [T:Result]
Thread Boundaries: Runs on caller thread (typically an async worker thread). Thread Boundaries: Runs on caller thread (typically an async worker thread).
""" """
global _gemini_chat, _gemini_cache, _gemini_cache_md_hash, _gemini_cache_created_at, _gemini_cached_file_paths global _gemini_chat, _gemini_cache, _gemini_cache_md_hash, _gemini_cache_created_at, _gemini_cached_file_paths
@@ -1626,6 +1578,7 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str,
elapsed = time.time() - _gemini_cache_created_at elapsed = time.time() - _gemini_cache_created_at
if elapsed > _GEMINI_CACHE_TTL * 0.9: if elapsed > _GEMINI_CACHE_TTL * 0.9:
old_history = list(_get_gemini_history_list(_gemini_chat)) if _get_gemini_history_list(_gemini_chat) else [] old_history = list(_get_gemini_history_list(_gemini_chat)) if _get_gemini_history_list(_gemini_chat) else []
#TODO(Ed): Review(Exception)
try: _gemini_client.caches.delete(name=_gemini_cache.name) try: _gemini_client.caches.delete(name=_gemini_cache.name)
except Exception as e: _append_comms("OUT", "request", {"message": f"[CACHE DELETE WARN] {e}"}) except Exception as e: _append_comms("OUT", "request", {"message": f"[CACHE DELETE WARN] {e}"})
_gemini_chat = None _gemini_chat = None
@@ -1635,12 +1588,12 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str,
_append_comms("OUT", "request", {"message": f"[CACHE TTL] Rebuilding cache (expired after {int(elapsed)}s)..."}) _append_comms("OUT", "request", {"message": f"[CACHE TTL] Rebuilding cache (expired after {int(elapsed)}s)..."})
if not _gemini_chat: if not _gemini_chat:
chat_config = types.GenerateContentConfig( chat_config = types.GenerateContentConfig(
system_instruction=sys_instr, system_instruction = sys_instr,
tools=cast(Any, tools_decl), tools = cast(Any, tools_decl),
temperature=_temperature, temperature = _temperature,
top_p=_top_p, top_p = _top_p,
max_output_tokens=_max_tokens, max_output_tokens = _max_tokens,
safety_settings=[types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH)] safety_settings = [types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH)]
) )
should_cache = False should_cache = False
try: try: