refactor(agent/gemini,acp): async stream method, contextlib.suppress, compact headers
This commit is contained in:
@@ -6,6 +6,7 @@ as one prompt, collects text chunks, and returns the minimal OpenAI-client shape
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -82,13 +83,10 @@ def _resolve_args() -> list[str]:
|
||||
|
||||
|
||||
def _acp_supported(command: str, args: list[str]) -> bool | None:
|
||||
"""Tri-state probe: does ``command`` accept ``--acp``? A CLI without the flag
|
||||
(older releases, Claude Code v2.x) exits 1 with ``unknown option '--acp'`` and the
|
||||
parent would wait the full child timeout for stdout that never arrives.
|
||||
True = help advertises --acp; False = help ran cleanly without it (caller
|
||||
fast-fails); None = inconclusive (binary missing / --help failed → normal spawn
|
||||
error). Only probes when ``--acp`` is among ``args`` — a custom transport is the
|
||||
operator's business."""
|
||||
"""Tri-state ``--acp`` probe (a CLI without the flag exits 1 and the parent would wait the
|
||||
full child timeout for stdout that never arrives). True = help advertises --acp; False =
|
||||
help ran cleanly without it (caller fast-fails); None = inconclusive (binary missing /
|
||||
--help failed → normal spawn error). Skipped when ``--acp`` is not in ``args`` (custom transport)."""
|
||||
if "--acp" not in args:
|
||||
return True
|
||||
if (cached := _ACP_PROBE_CACHE.get(command)) is not None:
|
||||
@@ -112,8 +110,7 @@ def _resolve_home_dir() -> str:
|
||||
"""Stable HOME for child ACP processes; /tmp as a last resort so the child never starts HOME-less."""
|
||||
if home := os.environ.get("HOME", "").strip():
|
||||
return home
|
||||
expanded = os.path.expanduser("~")
|
||||
if expanded and expanded != "~":
|
||||
if (expanded := os.path.expanduser("~")) and expanded != "~":
|
||||
return expanded
|
||||
try:
|
||||
import pwd
|
||||
@@ -276,8 +273,8 @@ class CopilotACPClient:
|
||||
|
||||
def __init__(
|
||||
self, *, api_key: str | None = None, base_url: str | None = None, default_headers: dict[str, str] | None = None,
|
||||
acp_command: str | None = None, acp_args: list[str] | None = None, acp_cwd: str | None = None,
|
||||
command: str | None = None, args: list[str] | None = None, **_: Any,
|
||||
acp_command: str | None = None, acp_args: list[str] | None = None, acp_cwd: str | None = None, command: str | None = None,
|
||||
args: list[str] | None = None, **_: Any,
|
||||
):
|
||||
self.api_key = api_key or "copilot-acp"
|
||||
self.base_url = base_url or ACP_MARKER_BASE_URL
|
||||
@@ -300,10 +297,8 @@ class CopilotACPClient:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _create_chat_completion(
|
||||
self, *, model: str | None = None, messages: list[dict[str, Any]] | None = None, timeout: float | None = None,
|
||||
|
||||
@@ -237,10 +237,9 @@ def evaluate_credits_notices(
|
||||
return (to_show, to_clear)
|
||||
|
||||
|
||||
# Header contract: (field, header, kind[, default]). Each *-micros balance has a *-usd
|
||||
# twin holding the server's formatted USD string. micros: required int >= 0 ("signed":
|
||||
# may be negative). usd: required ^-?\d+\.\d{2}$. bool: optional "true"/"false" STRING.
|
||||
# Not in the table: subscription-limit-* (PAIRED/optional), tool-pool-micros (optional),
|
||||
# Header contract: (field, header, kind[, default]). micros: required int >= 0 ("signed": may be
|
||||
# negative); usd: the server's formatted string, ^-?\d+\.\d{2}$; bool: optional "true"/"false" STRING.
|
||||
# Handled inline below: subscription-limit-* (PAIRED/optional), tool-pool-micros (optional),
|
||||
# denominator-kind ("subscription_cap" | "none"), disabled-reason (omitted when null).
|
||||
_HEADER_FIELDS: tuple[tuple, ...] = (
|
||||
("remaining_micros", "x-nous-credits-remaining-micros", "micros"),
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"""OpenAI-compatible facade over Google AI Studio's native Gemini API.
|
||||
|
||||
The ``gemini`` provider keeps ``api_mode='chat_completions'`` so the agent loop
|
||||
stays OpenAI-shaped; this shim converts ``messages[]``/``tools[]`` requests into
|
||||
``models/{model}:generateContent`` payloads and converts responses back. Google's
|
||||
OpenAI-compat endpoint is brittle for the multi-turn tool loop (auth churn,
|
||||
tool-call replay quirks, thought-signature requirements); native is canonical.
|
||||
"""
|
||||
"""OpenAI-compatible facade over Google AI Studio's native Gemini API: the ``gemini``
|
||||
provider keeps ``api_mode='chat_completions'`` so the agent loop stays OpenAI-shaped,
|
||||
and this shim converts ``messages[]``/``tools[]`` into ``models/{model}:generateContent``
|
||||
payloads and responses back. Google's OpenAI-compat endpoint is brittle for the
|
||||
multi-turn tool loop (auth churn, tool-call replay, thought signatures); native is canonical."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -149,8 +146,7 @@ class GeminiAPIError(Exception):
|
||||
|
||||
def __init__(
|
||||
self, message: str, *, code: str = "gemini_api_error", status_code: Optional[int] = None,
|
||||
response: Optional[httpx.Response] = None, retry_after: Optional[float] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
response: Optional[httpx.Response] = None, retry_after: Optional[float] = None, details: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.code, self.status_code, self.response = code, status_code, response
|
||||
@@ -220,8 +216,8 @@ def _translate_tool_call_to_gemini(tool_call: Dict[str, Any], include_ids: bool
|
||||
except json.JSONDecodeError:
|
||||
args = {"_raw": args_raw}
|
||||
call: Dict[str, Any] = {"name": str(fn.get("name") or ""), "args": args if isinstance(args, dict) else {"_value": args}}
|
||||
if include_ids and _tool_call_id(tool_call):
|
||||
call["id"] = _tool_call_id(tool_call)
|
||||
if include_ids and (call_id := _tool_call_id(tool_call)):
|
||||
call["id"] = call_id
|
||||
return {"functionCall": call, "thoughtSignature": _tool_call_extra_signature(tool_call) or _SKIP_SIGNATURE}
|
||||
|
||||
|
||||
@@ -629,8 +625,8 @@ class GeminiNativeClient:
|
||||
"""Minimal OpenAI-SDK-compatible facade (``client.chat.completions.create(**kwargs)``)
|
||||
over Gemini's native REST API."""
|
||||
|
||||
# Declared for agent/auxiliary_client.py: already a complete client, never re-dispatched through
|
||||
# a wire adapter. (No HERMES_SKIP_ASYNC_WRAP — the async path has a real conversion, AsyncGeminiNativeClient.)
|
||||
# For agent/auxiliary_client.py: a complete client, never re-dispatched through a wire adapter.
|
||||
# (No HERMES_SKIP_ASYNC_WRAP — the async path has a real conversion, AsyncGeminiNativeClient.)
|
||||
HERMES_SKIP_TRANSPORT_WRAP = True
|
||||
|
||||
def __init__(
|
||||
@@ -658,10 +654,8 @@ class GeminiNativeClient:
|
||||
pass
|
||||
|
||||
def _headers(self) -> Dict[str, str]:
|
||||
return {
|
||||
"Content-Type": "application/json", "Accept": "application/json", "x-goog-api-key": self.api_key,
|
||||
"User-Agent": f"{_API_CLIENT} (gemini-native)", "X-Goog-Api-Client": _API_CLIENT, **self._default_headers,
|
||||
}
|
||||
return {"Content-Type": "application/json", "Accept": "application/json", "x-goog-api-key": self.api_key,
|
||||
"User-Agent": f"{_API_CLIENT} (gemini-native)", "X-Goog-Api-Client": _API_CLIENT, **self._default_headers}
|
||||
|
||||
@staticmethod
|
||||
def _advance_stream_iterator(iterator: Iterator[_GeminiStreamChunk]) -> tuple[bool, Optional[_GeminiStreamChunk]]:
|
||||
@@ -721,17 +715,14 @@ class AsyncGeminiNativeClient:
|
||||
|
||||
async def _create_chat_completion(self, **kwargs: Any) -> Any:
|
||||
result = await asyncio.to_thread(self._sync.chat.completions.create, **kwargs)
|
||||
if not kwargs.get("stream"):
|
||||
return result
|
||||
return self._async_stream(result) if kwargs.get("stream") else result
|
||||
|
||||
async def _async_stream() -> Any:
|
||||
while True:
|
||||
done, chunk = await asyncio.to_thread(self._sync._advance_stream_iterator, result)
|
||||
if done:
|
||||
return
|
||||
yield chunk
|
||||
|
||||
return _async_stream()
|
||||
async def _async_stream(self, iterator: Iterator[_GeminiStreamChunk]) -> Any:
|
||||
while True:
|
||||
done, chunk = await asyncio.to_thread(self._sync._advance_stream_iterator, iterator)
|
||||
if done:
|
||||
return
|
||||
yield chunk
|
||||
|
||||
async def close(self) -> None:
|
||||
await asyncio.to_thread(self._sync.close)
|
||||
|
||||
Reference in New Issue
Block a user