mpfaffenberger / mpfaffenberger/code_puppy
Gemini request plumbing duplicated: request() vs request_stream() build the same body in gemini_model.py, and gemini_code_assist.py forks _build_tools/_build_generation_config/_parse_response with real divergences
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 814
- Forks
- 278
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 76
Description
Severity: Medium (DRY; one divergence is a live bug)
1. Within gemini_model.py: request() and request_stream() duplicate body construction
GeminiModel.request() (gemini_model.py:522-560) and GeminiModel.request_stream() (612-649) each do the identical sequence — _map_messages → body = {"contents": ...} → conditionally attach generationConfig / systemInstruction / tools → _get_client() + url + _get_headers(). jscpd flags 528-544 ↔ 619-635 verbatim.
Divergence between the copies: only the streaming path adds toolConfig.functionCallingConfig (mode: AUTO, streamFunctionCallArguments: True). That may be intentional, but buried inside a 16-line clone it's indistinguishable from drift. Extract:
def _build_request_body(
self,
system_instruction, contents,
model_settings, model_request_parameters,
*, streaming: bool = False,
) -> dict[str, Any]: ...
so the intentional difference becomes one explicit if streaming: line and both endpoints share the rest.
2. Cross-file: gemini_code_assist.py re-implements the same helpers — and they've drifted into bugs
GeminiCodeAssistModel (gemini_code_assist.py) carries its own _build_tools (231-246), _build_generation_config (248-272), and _parse_response (274-310) that parallel GeminiModel's (gemini_model.py:466-482, 484-520, 561-610). The drift between copies is not cosmetic:
_build_generation_configin code_assist is dead code. It probeshasattr(model_settings, "temperature")— butModelSettingsis a TypedDict (verified:hasattr(ModelSettings(temperature=0.5), "temperature")→False). So temperature/top_p/max_tokens are silently never sent for Code Assist models. The gemini_model.py copy correctly uses.get()and additionally handlesthinking_level/thinkingConfig, which the fork lacks entirely._parse_responsedrift: gemini_model.py handlesThinkingPart(+thoughtSignature), preserves the API-supplied tool-call id (fc.get("id") or generate_tool_call_id()), tolerates empty candidates, and recordsprovider_response_id. The code_assist copy drops thinking parts, mintsstr(uuid.uuid4())for every tool-call id, and raisesRuntimeError("No candidates in response")where the other returns an empty TextPart._build_toolsdrift: gemini_model.py sanitizes schemas via_sanitize_schema_for_geminiand emits keyparameters; the fork emitsparametersJsonSchemaunsanitized — so any schema-compat fix made for vanilla Gemini silently doesn't apply to Code Assist.
Proposed extraction
A shared module (e.g. code_puppy/gemini_common.py) or a base class _GeminiWireFormat holding: _build_tools, _build_generation_config, _parse_candidate_parts (the per-part text/functionCall/thought loop + usageMetadata extraction), and _build_request_body. GeminiModel and GeminiCodeAssistModel keep only their genuine deltas: auth headers, endpoint URLs, the Code Assist {"model": ..., "project": ..., "request": ...} envelope/unwrap (data.get("response", data)), and the thoughtSignature injection on outgoing function calls.
Wire-format knowledge for one upstream API should live in exactly one place; today a Gemini API change means hunting through two files with three forked copies each.
(Related but distinct: #401 covers gemini_code_assist's StreamedResponse/model_name problems; #410 covers GeminiModel's client lifecycle. Neither covers this duplication.)
Filed by Zen Reviewer A (code-puppy-60635a) — DRY review round
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by comparing the named helpers and request paths in gemini_model.py with _build_tools, _build_generation_config, and _parse_response in gemini_code_assist.py. Trace how request() and request_stream() build bodies, then evaluate a shared wire-format layer while preserving the streaming flag, Code Assist envelope, authentication, and endpoint differences. Done means the duplicated helpers and listed behavioral divergences are consolidated without changing those genuine model-specific deltas.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai, backend-api-design
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100