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", ""),
)
declarations.append(types.FunctionDeclaration(
name=tool_def["name"],
description=tool_def["description"],
parameters=types.Schema(
type=types.Type.OBJECT,
properties=props,
required=params.get("required", []),
name = tool_def["name"],
description = tool_def["description"],
parameters = types.Schema(
type = types.Type.OBJECT,
properties = props,
required = params.get("required", []),
),
))
return types.Tool(function_declarations=declarations) if declarations else None
@@ -705,12 +705,9 @@ async def _execute_tool_calls_concurrently(
tier = get_current_tier()
tasks = []
for fc in calls:
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
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 == "anthropic":
name, args, call_id = cast(str, getattr(fc, "name")), cast(dict[str, Any], getattr(fc, "input")), cast(str, getattr(fc, "id"))
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
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 == "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":
tool_info = fc.get("function", {})
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:
with history_lock:
msg: dict[str, Any] = {"role": "assistant", "content": response.text or None}
if reasoning_content:
msg["reasoning_content"] = reasoning_content
if response.tool_calls:
msg["tool_calls"] = response.tool_calls
if reasoning_content: msg["reasoning_content"] = reasoning_content
if response.tool_calls: msg["tool_calls"] = response.tool_calls
history.append(msg)
if not response.tool_calls:
break
if on_pre_dispatch is not None:
_adjusted_calls = on_pre_dispatch(_round_idx, response.tool_calls)
else:
_adjusted_calls = response.tool_calls
if not response.tool_calls: break
if on_pre_dispatch is not None: _adjusted_calls = on_pre_dispatch(_round_idx, response.tool_calls)
else: _adjusted_calls = response.tool_calls
try:
loop = asyncio.get_running_loop()
results = asyncio.run_coroutine_threadsafe(
@@ -840,8 +832,7 @@ def run_with_tool_loop(
"tool_call_id": call_id,
"content": str(out) if out else "",
})
if trim_func is not None:
trim_func(history)
if trim_func is not None: trim_func(history)
return response_text
async def _execute_single_tool_call_async(
@@ -891,7 +882,7 @@ async def _execute_single_tool_call_async(
set_current_tier(tier)
out = ""
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
approval_mode = _tool_approval_modes.get(name, "ask")
@@ -906,10 +897,8 @@ async def _execute_single_tool_call_async(
elif pre_tool_callback:
# pre_tool_callback is synchronous and might block for HITL
res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback)
if res is None:
out = "USER REJECTED: tool execution cancelled"
else:
out = res
if res is None: out = "USER REJECTED: tool execution cancelled"
else: out = res
tool_executed = True
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:
return "ERROR: no confirmation handler registered"
result = confirm_and_run_callback(script, base_dir, qa_callback, patch_callback)
if result is None:
output = "USER REJECTED: command was not executed"
else:
output = result
if tool_log_callback is not None:
tool_log_callback(script, output)
if result is None: output = "USER REJECTED: command was not executed"
else: output = result
if tool_log_callback is not None: tool_log_callback(script, output)
return output
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]]]:
"""
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".
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).
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]`
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.
Thread Boundaries: Runs synchronously in the caller thread. Does synchronous blocking file system I/O.
"""
refreshed: 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)
diff = difflib.unified_diff(old_lines, new_lines, fromfile=str(path), tofile=str(path), lineterm="")
diff_text = "\n".join(diff)
if diff_text:
parts.append(f"### `{path}` (diff)\n\n```diff\n{diff_text}\n```")
else:
parts.append(f"### `{path}` (no changes detected)")
if diff_text: parts.append(f"### `{path}` (diff)\n\n```diff\n{diff_text}\n```")
else: parts.append(f"### `{path}` (no changes detected)")
return "\n\n---\n\n".join(parts)
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]] = []
for spec in mcp_client.get_tool_schemas():
if _agent_tools.get(spec["name"], True):
@@ -1123,17 +1099,12 @@ def _get_deepseek_tools() -> list[dict[str, Any]]:
return _CACHED_DEEPSEEK_TOOLS
def _content_block_to_dict(block: Any) -> dict[str, Any]:
if isinstance(block, dict):
return block
if hasattr(block, "model_dump"):
return cast(dict[str, Any], block.model_dump())
if hasattr(block, "to_dict"):
return cast(dict[str, Any], block.to_dict())
if isinstance(block, dict): return block
if hasattr(block, "model_dump"): 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)
if block_type == "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 == "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")}
return {"type": "text", "text": str(block)}
#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:
cached = msg.get("_est_tokens")
if cached is not None:
return cast(int, cached)
if cached is not None: return cast(int, cached)
total_chars = 0
content = msg.get("content", "")
if isinstance(content, str):
@@ -1207,9 +1177,6 @@ def _strip_stale_file_refreshes(history: list[dict[str, Any]]) -> None:
_invalidate_token_estimate(msg)
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)]
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:
user_indices = [i for i, m in enumerate(history) if m.get("role") == "user"]
if len(user_indices) < 2:
return
if len(user_indices) < 2: return
target_idx = user_indices[-2]
content = history[target_idx].get("content")
if isinstance(content, list) and content:
@@ -1255,8 +1221,7 @@ def _list_anthropic_models() -> list[str]:
creds = _load_credentials()
client = anthropic.Anthropic(api_key=creds["anthropic"]["api_key"])
models: list[str] = []
for m in client.models.list():
models.append(m.id)
for m in client.models.list(): models.append(m.id)
return sorted(models)
except Exception as exc:
raise _classify_anthropic_error(exc) from exc
@@ -1267,15 +1232,14 @@ def _ensure_anthropic_client() -> None:
if _anthropic_client is None:
creds = _load_credentials()
_anthropic_client = anthropic.Anthropic(
api_key=creds["anthropic"]["api_key"],
default_headers={"anthropic-beta": "prompt-caching-2024-07-31"}
api_key = creds["anthropic"]["api_key"],
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:
_strip_stale_file_refreshes(history)
est = _estimate_prompt_tokens(system_blocks, history)
if est <= _ANTHROPIC_MAX_PROMPT_TOKENS:
return 0
if est <= _ANTHROPIC_MAX_PROMPT_TOKENS: return 0
dropped = 0
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":
@@ -1301,11 +1265,9 @@ def _trim_anthropic_history(system_blocks: list[dict[str, Any]], history: list[d
return dropped
def _repair_anthropic_history(history: list[dict[str, Any]]) -> None:
if not history:
return
if not history: return
last = history[-1]
if last.get("role") != "assistant":
return
if last.get("role") != "assistant": return
content = last.get("content", [])
tool_use_ids: list[str] = []
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:
Sends requests to Anthropic models, managing conversation history, prompt caching, token limits, and executing tool loops.
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:
block["content"] = t_content[:_history_trunc_limit] + "\n\n... [TRUNCATED BY SYSTEM TO SAVE TOKENS. Original output was too large.]"
modified = True
if modified:
_invalidate_token_estimate(msg)
if modified: _invalidate_token_estimate(msg)
_strip_cache_controls(_anthropic_history)
_repair_anthropic_history(_anthropic_history)
_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
if stream_callback:
with _anthropic_client.messages.stream(
model=_model,
max_tokens=_max_tokens,
temperature=_temperature,
top_p=_top_p,
system=cast(Iterable[anthropic.types.TextBlockParam], system_blocks),
tools=cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()),
messages=cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)),
model = _model,
max_tokens = _max_tokens,
temperature = _temperature,
top_p = _top_p,
system = cast(Iterable[anthropic.types.TextBlockParam], system_blocks),
tools = cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()),
messages = cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)),
) as stream:
for event in stream:
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()
else:
response = _anthropic_client.messages.create(
model=_model,
max_tokens=_max_tokens,
temperature=_temperature,
top_p=_top_p,
system=cast(Iterable[anthropic.types.TextBlockParam], system_blocks),
tools=cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()),
messages=cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)),
model = _model,
max_tokens = _max_tokens,
temperature = _temperature,
top_p = _top_p,
system = cast(Iterable[anthropic.types.TextBlockParam], system_blocks),
tools = cast(Iterable[anthropic.types.ToolParam], _get_anthropic_tools()),
messages = cast(Iterable[anthropic.types.MessageParam], _strip_private_keys(_anthropic_history)),
)
serialised_content = [_content_block_to_dict(b) for b in response.content]
_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
cache_creation = getattr(response.usage, "cache_creation_input_tokens", None)
cache_read = getattr(response.usage, "cache_read_input_tokens", None)
if cache_creation is not None:
usage_dict["cache_creation_input_tokens"] = cache_creation
if cache_read is not None:
usage_dict["cache_read_input_tokens"] = cache_read
if cache_creation is not None: usage_dict["cache_creation_input_tokens"] = cache_creation
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})
_append_comms("IN", "response", {
"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,
"usage": usage_dict,
})
if response.stop_reason != "tool_use" or not tool_use_blocks:
break
if round_idx > MAX_TOOL_ROUNDS:
break
if response.stop_reason != "tool_use" or not tool_use_blocks: break
if round_idx > MAX_TOOL_ROUNDS: break
# Execute tools concurrently
try:
@@ -1522,12 +1487,8 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
#region: Gemini Provider
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()
if not _gemini_client:
return {"cache_count": 0, "total_size_bytes": 0, "cached_files": []}
if not _gemini_client: return {"cache_count": 0, "total_size_bytes": 0, "cached_files": []}
caches_iterator = _gemini_client.caches.list()
caches = list(caches_iterator)
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] = []
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)
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 as exc:
raise _classify_gemini_error(exc) from exc
def _ensure_gemini_client() -> None:
"""
[C: src/rag_engine.py:GeminiEmbeddingProvider.embed]
"""
global _gemini_client
genai = _require_warmed("google.genai")
if _gemini_client is None:
@@ -1574,12 +1530,9 @@ def _ensure_gemini_client() -> None:
def _get_gemini_history_list(chat: Any | None) -> list[Any]:
if not chat: return []
if hasattr(chat, "_history"):
return cast(list[Any], chat._history)
if hasattr(chat, "history"):
return cast(list[Any], chat.history)
if hasattr(chat, "get_history"):
return cast(list[Any], chat.get_history())
if hasattr(chat, "_history"): return cast(list[Any], chat._history)
if hasattr(chat, "history"): return cast(list[Any], chat.history)
if hasattr(chat, "get_history"): return cast(list[Any], chat.get_history())
return []
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,
enable_tools: bool = True,
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.
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
SSDL:
[I:_ensure_gemini_client] -> [B:Cache Changed?] -> [I:client.caches.create] -> [I:client.chats.create] -> [T:Result]
SSDL: [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).
"""
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
if elapsed > _GEMINI_CACHE_TTL * 0.9:
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)
except Exception as e: _append_comms("OUT", "request", {"message": f"[CACHE DELETE WARN] {e}"})
_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)..."})
if not _gemini_chat:
chat_config = types.GenerateContentConfig(
system_instruction=sys_instr,
tools=cast(Any, tools_decl),
temperature=_temperature,
top_p=_top_p,
max_output_tokens=_max_tokens,
safety_settings=[types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH)]
system_instruction = sys_instr,
tools = cast(Any, tools_decl),
temperature = _temperature,
top_p = _top_p,
max_output_tokens = _max_tokens,
safety_settings = [types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_ONLY_HIGH)]
)
should_cache = False
try: