refactor(agent/gemini,acp): hoist missing-key error, sentinel-based stream advance

This commit is contained in:
Teknium
2026-09-02 20:00:38 -07:00
parent 2526ab9807
commit 0d6ed9cd69
2 changed files with 13 additions and 18 deletions
+3 -7
View File
@@ -312,10 +312,8 @@ class CopilotACPClient:
creationflags=windows_hide_flags(),
)
except FileNotFoundError as exc:
raise RuntimeError(
f"Could not start Copilot ACP command '{self._acp_command}'. Install GitHub Copilot CLI or set "
"HERMES_COPILOT_ACP_COMMAND/COPILOT_CLI_PATH."
) from exc
raise RuntimeError(f"Could not start Copilot ACP command '{self._acp_command}'. Install GitHub Copilot CLI or set "
"HERMES_COPILOT_ACP_COMMAND/COPILOT_CLI_PATH.") from exc
if proc.stdin is None or proc.stdout is None:
proc.kill()
raise RuntimeError("Copilot ACP process did not expose stdin/stdout pipes.")
@@ -384,9 +382,7 @@ class CopilotACPClient:
else:
logger.warning("Copilot ACP does not offer model %r; using the session default.", requested_model)
except Exception as exc:
logger.warning(
"Copilot ACP model selection for %r failed; continuing with the session default: %s", requested_model, exc
)
logger.warning("Copilot ACP model selection for %r failed; continuing with the session default: %s", requested_model, exc)
text_parts: list[str] = []
reasoning_parts: list[str] = []
prompt = {"sessionId": session_id, "prompt": [{"type": "text", "text": prompt_text}]}
+10 -11
View File
@@ -60,11 +60,17 @@ _INTERRUPTED_RESPONSE_PLACEHOLDER = "[The previous response was interrupted befo
# Cross-provider tool_calls (e.g. fallback from xAI/Anthropic) carry no Gemini thoughtSignature;
# without this sentinel Gemini 3 thinking models reject replayed history with 400 INVALID_ARGUMENT.
_SKIP_SIGNATURE = "skip_thought_signature_validator"
_END = object() # stream-exhausted marker for _advance_stream_iterator
_TOOL_CHOICE_MODES = {"auto": "AUTO", "required": "ANY", "none": "NONE"}
_FINISH_REASON_MAP = {
"STOP": "stop", "MAX_TOKENS": "length", "SAFETY": "content_filter", "RECITATION": "content_filter", "OTHER": "stop",
}
_HTTP_ERROR_CODES = {401: "gemini_unauthorized", 429: "gemini_rate_limited", 404: "gemini_model_not_found"}
_MISSING_KEY_ERROR = (
"Gemini native client requires an API key, but none was provided. Set GOOGLE_API_KEY or GEMINI_API_KEY in your "
"environment / ~/.hermes/.env (get one at https://aistudio.google.com/app/apikey), or run `hermes setup` to "
"configure the Google provider."
)
def bare_gemini_model_id(model: str) -> str:
@@ -613,16 +619,11 @@ class GeminiNativeClient:
timeout: Any = None, http_client: Optional[httpx.Client] = None, **_: Any,
) -> None:
if not (api_key or "").strip():
raise RuntimeError(
"Gemini native client requires an API key, but none was provided. Set GOOGLE_API_KEY or GEMINI_API_KEY in your "
"environment / ~/.hermes/.env (get one at https://aistudio.google.com/app/apikey), or run `hermes setup` to "
"configure the Google provider."
)
self.api_key = api_key
raise RuntimeError(_MISSING_KEY_ERROR)
self.api_key, self.is_closed = api_key, False
self.base_url = (base_url or DEFAULT_GEMINI_BASE_URL).rstrip("/").removesuffix("/openai")
self._default_headers = dict(default_headers or {})
self.chat = SimpleNamespace(completions=SimpleNamespace(create=self._create_chat_completion))
self.is_closed = False
self._http = http_client or httpx.Client(timeout=timeout or httpx.Timeout(connect=15.0, read=600.0, write=30.0, pool=30.0))
def close(self) -> None:
@@ -636,10 +637,8 @@ class GeminiNativeClient:
@staticmethod
def _advance_stream_iterator(iterator: Iterator[_GeminiStreamChunk]) -> tuple[bool, Optional[_GeminiStreamChunk]]:
try:
return False, next(iterator)
except StopIteration:
return True, None
chunk = next(iterator, _END)
return (True, None) if chunk is _END else (False, chunk)
def _create_chat_completion(
self, *, model: str = "gemini-3.7-flash", messages: Optional[List[Dict[str, Any]]] = None, stream: bool = False,