Private
Public Access
ai client pass (in gemini region)
This commit is contained in:
+86
-133
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user