Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds first-class support for Google’s Gemini native :generateContent REST API by introducing a dedicated adapter and wiring it into LLMClient via provider presets.
Changes:
- Added
GeminiAdapterto translate OpenAI-style{role, content}messages into Gemini’scontents/partsschema and map responses back. - Registered a new
"gemini"provider preset and initialized/used the adapter inLLMClient.from_rc_config()/_raw_call(). - Adjusted JSON-mode compatibility handling in the OpenAI-compatible path (adds
"apple"to the no-response_formatlist).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| researchclaw/llm/gemini_adapter.py | New Gemini native adapter implementing request/response translation and JSON-mode support. |
| researchclaw/llm/client.py | Wires the Gemini adapter into client initialization and request dispatch. |
| researchclaw/llm/init.py | Adds the "gemini" provider preset and documents it in the factory docstring. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try: | ||
| with urllib.request.urlopen(req, timeout=self.timeout_sec) as resp: | ||
| data = json.loads(resp.read()) | ||
| except urllib.error.HTTPError as exc: | ||
| # Attempt to extract detailed error from Gemini to aid debugging | ||
| try: | ||
| body_err = exc.read().decode("utf-8") | ||
| logger.error("Gemini API Error: %s", body_err) | ||
| except Exception: # noqa: BLE001 | ||
| pass | ||
| raise |
There was a problem hiding this comment.
In the HTTPError path, the adapter calls exc.read() to log the error body and then re-raises the same HTTPError. This consumes the underlying response stream, so LLMClient._call_with_retry will usually see an empty body on e.read(), which can break retry/transient-400 detection and the 403 “model not allowed” fallback logic. Capture the error body bytes once and re-raise a new urllib.error.HTTPError (or reset exc.fp) with an in-memory buffer so upstream can still read the body; also consider truncating what gets logged.
| gemini_finish = first_candidate.get("finishReason", "STOP") | ||
| if gemini_finish == "MAX_TOKENS": | ||
| finish_reason = "length" | ||
| else: | ||
| finish_reason = "stop" |
There was a problem hiding this comment.
finishReason mapping currently only distinguishes MAX_TOKENS vs everything else. Gemini also returns other reasons (e.g., SAFETY / RECITATION) that should not be reported as "stop" because downstream may treat this as a normal completion. Map these to a more appropriate OpenAI-style finish_reason (e.g., "content_filter") or surface the raw reason in a structured way.
| "gemini": { | ||
| "base_url": "https://generativelanguage.googleapis.com/v1beta", | ||
| }, |
There was a problem hiding this comment.
Adding a new provider preset + adapter path should come with unit coverage similar to tests/test_minimax_provider.py (preset registration, factory wiring, and a stubbed urlopen round-trip for GeminiAdapter). Without this, regressions in payload translation / URL construction may go unnoticed.
| elif provider == "gemini": | ||
| from .gemini_adapter import GeminiAdapter | ||
|
|
||
| client._gemini = GeminiAdapter( | ||
| original_base_url, original_api_key, config.timeout_sec | ||
| ) |
There was a problem hiding this comment.
LLMClient.preflight()’s 404 message uses self._endpoint_url(self.config.base_url), which is specific to OpenAI-compatible endpoints (/chat/completions or /responses). With the new Gemini provider, a 404 from GeminiAdapter will be reported with an incorrect endpoint URL, making debugging harder. Consider branching preflight error messaging when a native adapter (Gemini/Anthropic) is active to report the actual request URL or a provider-specific hint.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This PR introduces native support for Google's Gemini API endpoints.
Features