code-puppy 0.0.361__py3-none-any.whl → 0.0.372__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. code_puppy/agents/__init__.py +6 -0
  2. code_puppy/agents/agent_manager.py +223 -1
  3. code_puppy/agents/base_agent.py +2 -12
  4. code_puppy/chatgpt_codex_client.py +53 -0
  5. code_puppy/claude_cache_client.py +45 -7
  6. code_puppy/command_line/add_model_menu.py +13 -4
  7. code_puppy/command_line/agent_menu.py +662 -0
  8. code_puppy/command_line/core_commands.py +4 -112
  9. code_puppy/command_line/model_picker_completion.py +3 -20
  10. code_puppy/command_line/model_settings_menu.py +21 -3
  11. code_puppy/config.py +79 -8
  12. code_puppy/gemini_model.py +706 -0
  13. code_puppy/http_utils.py +6 -3
  14. code_puppy/model_factory.py +50 -16
  15. code_puppy/model_switching.py +63 -0
  16. code_puppy/model_utils.py +1 -52
  17. code_puppy/models.json +12 -12
  18. code_puppy/plugins/antigravity_oauth/antigravity_model.py +128 -165
  19. code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
  20. code_puppy/plugins/antigravity_oauth/transport.py +235 -45
  21. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
  22. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
  23. code_puppy/plugins/claude_code_oauth/utils.py +4 -1
  24. code_puppy/pydantic_patches.py +52 -0
  25. code_puppy/tools/agent_tools.py +3 -3
  26. code_puppy/tools/browser/__init__.py +1 -1
  27. code_puppy/tools/browser/browser_control.py +1 -1
  28. code_puppy/tools/browser/browser_interactions.py +1 -1
  29. code_puppy/tools/browser/browser_locators.py +1 -1
  30. code_puppy/tools/browser/{camoufox_manager.py → browser_manager.py} +29 -110
  31. code_puppy/tools/browser/browser_navigation.py +1 -1
  32. code_puppy/tools/browser/browser_screenshot.py +1 -1
  33. code_puppy/tools/browser/browser_scripts.py +1 -1
  34. {code_puppy-0.0.361.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
  35. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/METADATA +5 -6
  36. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/RECORD +40 -38
  37. code_puppy/prompts/codex_system_prompt.md +0 -310
  38. {code_puppy-0.0.361.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
  39. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
  40. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
  41. {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,9 @@
1
+ """AntigravityModel - extends GeminiModel with thinking signature handling.
2
+
3
+ This model handles the special Antigravity envelope format and preserves
4
+ Claude thinking signatures for Gemini 3 models.
5
+ """
6
+
1
7
  from __future__ import annotations
2
8
 
3
9
  import base64
@@ -19,6 +25,7 @@ from pydantic_ai.messages import (
19
25
  ModelRequest,
20
26
  ModelResponse,
21
27
  ModelResponsePart,
28
+ ModelResponseStreamEvent,
22
29
  RetryPromptPart,
23
30
  SystemPromptPart,
24
31
  TextPart,
@@ -27,35 +34,99 @@ from pydantic_ai.messages import (
27
34
  ToolReturnPart,
28
35
  UserPromptPart,
29
36
  )
30
- from typing_extensions import assert_never
31
-
32
- # Define types locally if needed to avoid import errors
33
- try:
34
- from pydantic_ai.messages import BlobDict, ContentDict, FunctionCallDict, PartDict
35
- except ImportError:
36
- ContentDict = dict[str, Any]
37
- PartDict = dict[str, Any]
38
- FunctionCallDict = dict[str, Any]
39
- BlobDict = dict[str, Any]
40
-
41
- from pydantic_ai.messages import ModelResponseStreamEvent
42
37
  from pydantic_ai.models import ModelRequestParameters, StreamedResponse
43
- from pydantic_ai.models.google import GoogleModel, GoogleModelName, _utils
44
38
  from pydantic_ai.settings import ModelSettings
45
39
  from pydantic_ai.usage import RequestUsage
40
+ from typing_extensions import assert_never
41
+
42
+ from code_puppy.gemini_model import (
43
+ GeminiModel,
44
+ generate_tool_call_id,
45
+ )
46
+ from code_puppy.model_utils import _load_antigravity_prompt
47
+ from code_puppy.plugins.antigravity_oauth.transport import _inline_refs
46
48
 
47
49
  logger = logging.getLogger(__name__)
48
50
 
51
+ # Type aliases for clarity
52
+ ContentDict = dict[str, Any]
53
+ PartDict = dict[str, Any]
54
+ FunctionCallDict = dict[str, Any]
55
+ BlobDict = dict[str, Any]
56
+
57
+ # Bypass signature for when no real thought signature is available.
58
+ BYPASS_THOUGHT_SIGNATURE = "context_engineering_is_the_way_to_go"
59
+
60
+
61
+ def _is_signature_error(error_text: str) -> bool:
62
+ """Check if the error is a thought signature error that can be retried.
63
+
64
+ Detects both:
65
+ - Gemini: "Corrupted thought signature"
66
+ - Claude: "thinking.signature: Field required" or similar
67
+ """
68
+ return (
69
+ "Corrupted thought signature" in error_text
70
+ or "thinking.signature" in error_text
71
+ )
72
+
73
+
74
+ class AntigravityModel(GeminiModel):
75
+ """Custom GeminiModel that correctly handles Claude thinking signatures via Antigravity.
76
+
77
+ This model extends GeminiModel and adds:
78
+ - Proper thoughtSignature handling for both Gemini and Claude models
79
+ - Backfill logic for corrupted thought signatures
80
+ - Special message merging for parallel function calls
81
+ """
82
+
83
+ def _get_instructions(
84
+ self,
85
+ messages: list,
86
+ model_request_parameters,
87
+ ) -> str | None:
88
+ """Return the Antigravity system prompt.
89
+
90
+ The Antigravity endpoint expects requests to include the special
91
+ Antigravity identity prompt in the systemInstruction field.
92
+ """
93
+ return _load_antigravity_prompt()
94
+
95
+ def _is_claude_model(self) -> bool:
96
+ """Check if this is a Claude model (vs Gemini)."""
97
+ return "claude" in self.model_name.lower()
98
+
99
+ def _build_tools(self, tools: list) -> list[dict]:
100
+ """Build tool definitions with model-appropriate schema handling.
101
+
102
+ Both Gemini and Claude require simplified union types in function schemas:
103
+ - Neither supports anyOf/oneOf/allOf in function parameter schemas
104
+ - We simplify by picking the first non-null type from unions
105
+ """
106
+
107
+ function_declarations = []
49
108
 
50
- class AntigravityModel(GoogleModel):
51
- """Custom GoogleModel that correctly handles Claude thinking signatures via Antigravity."""
109
+ for tool in tools:
110
+ func_decl = {
111
+ "name": tool.name,
112
+ "description": tool.description or "",
113
+ }
114
+ if tool.parameters_json_schema:
115
+ # Simplify union types for all models (Gemini and Claude both need this)
116
+ func_decl["parameters"] = _inline_refs(
117
+ tool.parameters_json_schema,
118
+ simplify_unions=True, # Both Gemini and Claude need simplified unions
119
+ )
120
+ function_declarations.append(func_decl)
121
+
122
+ return [{"functionDeclarations": function_declarations}]
52
123
 
53
124
  async def _map_messages(
54
125
  self,
55
126
  messages: list[ModelMessage],
56
127
  model_request_parameters: ModelRequestParameters,
57
128
  ) -> tuple[ContentDict | None, list[dict]]:
58
- """Map messages to Google GenAI format, preserving thinking signatures.
129
+ """Map messages to Gemini API format, preserving thinking signatures.
59
130
 
60
131
  IMPORTANT: For Gemini with parallel function calls, the API expects:
61
132
  - Model message: [FC1 + signature, FC2, ...] (all function calls together)
@@ -120,8 +191,7 @@ class AntigravityModel(GoogleModel):
120
191
  contents.append({"role": "user", "parts": message_parts})
121
192
 
122
193
  elif isinstance(m, ModelResponse):
123
- # USE CUSTOM HELPER HERE
124
- # Pass model name so we can handle Claude vs Gemini signature placement
194
+ # Use custom helper for thinking signature handling
125
195
  maybe_content = _antigravity_content_model_response(
126
196
  m, self.system, self._model_name
127
197
  )
@@ -138,8 +208,11 @@ class AntigravityModel(GoogleModel):
138
208
  if not contents:
139
209
  contents = [{"role": "user", "parts": [{"text": ""}]}]
140
210
 
141
- if instructions := self._get_instructions(messages, model_request_parameters):
211
+ # Get any injected instructions
212
+ instructions = self._get_instructions(messages, model_request_parameters)
213
+ if instructions:
142
214
  system_parts.insert(0, {"text": instructions})
215
+
143
216
  system_instruction = (
144
217
  ContentDict(role="user", parts=system_parts) if system_parts else None
145
218
  )
@@ -152,33 +225,15 @@ class AntigravityModel(GoogleModel):
152
225
  model_settings: ModelSettings | None,
153
226
  model_request_parameters: ModelRequestParameters,
154
227
  ) -> ModelResponse:
155
- """Override request to use direct HTTP calls, bypassing google-genai validation."""
156
- # Prepare request (normalizes settings)
157
- model_settings, model_request_parameters = self.prepare_request(
158
- model_settings, model_request_parameters
159
- )
160
-
228
+ """Override request to handle Antigravity envelope and thinking signatures."""
161
229
  system_instruction, contents = await self._map_messages(
162
230
  messages, model_request_parameters
163
231
  )
164
232
 
165
233
  # Build generation config from model settings
166
- gen_config: dict[str, Any] = {}
167
- if model_settings:
168
- if (
169
- hasattr(model_settings, "temperature")
170
- and model_settings.temperature is not None
171
- ):
172
- gen_config["temperature"] = model_settings.temperature
173
- if hasattr(model_settings, "top_p") and model_settings.top_p is not None:
174
- gen_config["topP"] = model_settings.top_p
175
- if (
176
- hasattr(model_settings, "max_tokens")
177
- and model_settings.max_tokens is not None
178
- ):
179
- gen_config["maxOutputTokens"] = model_settings.max_tokens
180
-
181
- # Build JSON body manually to ensure thoughtSignature is preserved
234
+ gen_config = self._build_generation_config(model_settings)
235
+
236
+ # Build JSON body
182
237
  body: dict[str, Any] = {
183
238
  "contents": contents,
184
239
  }
@@ -187,43 +242,23 @@ class AntigravityModel(GoogleModel):
187
242
  if system_instruction:
188
243
  body["systemInstruction"] = system_instruction
189
244
 
190
- # Serialize tools manually
245
+ # Serialize tools
191
246
  if model_request_parameters.function_tools:
192
- funcs = []
193
- for t in model_request_parameters.function_tools:
194
- funcs.append(
195
- {
196
- "name": t.name,
197
- "description": t.description,
198
- "parameters": t.parameters_json_schema,
199
- }
200
- )
201
- body["tools"] = [{"functionDeclarations": funcs}]
202
-
203
- # Use the http_client from the google-genai client directly
204
- # This bypasses google-genai library's strict validation/serialization
205
- # Path: self.client._api_client._async_httpx_client
206
- try:
207
- client = self.client._api_client._async_httpx_client
208
- except AttributeError:
209
- raise RuntimeError(
210
- "AntigravityModel requires access to the underlying httpx client"
211
- )
247
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
248
+
249
+ # Get httpx client
250
+ client = await self._get_client()
212
251
  url = f"/models/{self._model_name}:generateContent"
213
252
 
214
253
  # Send request
215
254
  response = await client.post(url, json=body)
216
255
 
217
256
  if response.status_code != 200:
218
- # Check for corrupted thought signature error and retry
219
- # Error 400: { error: { code: 400, message: Corrupted thought signature., status: INVALID_ARGUMENT } }
220
257
  error_text = response.text
221
- if (
222
- response.status_code == 400
223
- and "Corrupted thought signature" in error_text
224
- ):
258
+ if response.status_code == 400 and _is_signature_error(error_text):
225
259
  logger.warning(
226
- "Received 400 Corrupted thought signature. Backfilling signatures and retrying."
260
+ "Received 400 signature error. Backfilling with bypass signatures and retrying. Error: %s",
261
+ error_text[:200],
227
262
  )
228
263
  _backfill_thought_signatures(messages)
229
264
 
@@ -239,7 +274,6 @@ class AntigravityModel(GoogleModel):
239
274
 
240
275
  # Retry request
241
276
  response = await client.post(url, json=body)
242
- # Check error again after retry
243
277
  if response.status_code != 200:
244
278
  raise RuntimeError(
245
279
  f"Antigravity API Error {response.status_code}: {response.text}"
@@ -254,7 +288,6 @@ class AntigravityModel(GoogleModel):
254
288
  # Extract candidates
255
289
  candidates = data.get("candidates", [])
256
290
  if not candidates:
257
- # Handle empty response or safety block?
258
291
  return ModelResponse(
259
292
  parts=[TextPart(content="")],
260
293
  model_name=self._model_name,
@@ -289,31 +322,13 @@ class AntigravityModel(GoogleModel):
289
322
  model_request_parameters: ModelRequestParameters,
290
323
  run_context: RunContext[Any] | None = None,
291
324
  ) -> AsyncIterator[StreamedResponse]:
292
- """Override request_stream to use streaming with proper signature handling."""
293
- # Prepare request
294
- model_settings, model_request_parameters = self.prepare_request(
295
- model_settings, model_request_parameters
296
- )
297
-
325
+ """Override request_stream for streaming with signature handling."""
298
326
  system_instruction, contents = await self._map_messages(
299
327
  messages, model_request_parameters
300
328
  )
301
329
 
302
330
  # Build generation config
303
- gen_config: dict[str, Any] = {}
304
- if model_settings:
305
- if (
306
- hasattr(model_settings, "temperature")
307
- and model_settings.temperature is not None
308
- ):
309
- gen_config["temperature"] = model_settings.temperature
310
- if hasattr(model_settings, "top_p") and model_settings.top_p is not None:
311
- gen_config["topP"] = model_settings.top_p
312
- if (
313
- hasattr(model_settings, "max_tokens")
314
- and model_settings.max_tokens is not None
315
- ):
316
- gen_config["maxOutputTokens"] = model_settings.max_tokens
331
+ gen_config = self._build_generation_config(model_settings)
317
332
 
318
333
  # Build request body
319
334
  body: dict[str, Any] = {"contents": contents}
@@ -324,31 +339,17 @@ class AntigravityModel(GoogleModel):
324
339
 
325
340
  # Add tools
326
341
  if model_request_parameters.function_tools:
327
- funcs = []
328
- for t in model_request_parameters.function_tools:
329
- funcs.append(
330
- {
331
- "name": t.name,
332
- "description": t.description,
333
- "parameters": t.parameters_json_schema,
334
- }
335
- )
336
- body["tools"] = [{"functionDeclarations": funcs}]
342
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
337
343
 
338
344
  # Get httpx client
339
- try:
340
- client = self.client._api_client._async_httpx_client
341
- except AttributeError:
342
- raise RuntimeError(
343
- "AntigravityModel requires access to the underlying httpx client"
344
- )
345
-
346
- # Use streaming endpoint
345
+ client = await self._get_client()
347
346
  url = f"/models/{self._model_name}:streamGenerateContent?alt=sse"
348
347
 
349
348
  # Create async generator for SSE events
350
349
  async def stream_chunks() -> AsyncIterator[dict[str, Any]]:
351
350
  retry_count = 0
351
+ nonlocal body # Allow modification for retry
352
+
352
353
  while retry_count < 2:
353
354
  should_retry = False
354
355
  async with client.stream("POST", url, json=body) as response:
@@ -357,7 +358,7 @@ class AntigravityModel(GoogleModel):
357
358
  error_msg = text.decode()
358
359
  if (
359
360
  response.status_code == 400
360
- and "Corrupted thought signature" in error_msg
361
+ and _is_signature_error(error_msg)
361
362
  and retry_count == 0
362
363
  ):
363
364
  should_retry = True
@@ -372,7 +373,7 @@ class AntigravityModel(GoogleModel):
372
373
  if not line:
373
374
  continue
374
375
  if line.startswith("data: "):
375
- json_str = line[6:] # Remove 'data: ' prefix
376
+ json_str = line[6:]
376
377
  if json_str:
377
378
  try:
378
379
  yield json.loads(json_str)
@@ -383,7 +384,7 @@ class AntigravityModel(GoogleModel):
383
384
  # Handle retry outside the context manager
384
385
  if should_retry:
385
386
  logger.warning(
386
- "Received 400 Corrupted thought signature in stream. Backfilling and retrying."
387
+ "Received 400 signature error in stream. Backfilling with bypass signatures and retrying."
387
388
  )
388
389
  _backfill_thought_signatures(messages)
389
390
 
@@ -392,7 +393,7 @@ class AntigravityModel(GoogleModel):
392
393
  messages, model_request_parameters
393
394
  )
394
395
 
395
- # Update body in place
396
+ # Update body
396
397
  body["contents"] = contents
397
398
  if system_instruction:
398
399
  body["systemInstruction"] = system_instruction
@@ -445,11 +446,9 @@ class AntigravityStreamingResponse(StreamedResponse):
445
446
  parts = content.get("parts", [])
446
447
 
447
448
  for part in parts:
448
- # Extract signature (for Gemini, it's on the functionCall part)
449
+ # Extract signature
449
450
  thought_signature = part.get("thoughtSignature")
450
451
  if thought_signature:
451
- # For Gemini: if this is a function call with signature,
452
- # the signature belongs to the previous thinking block
453
452
  if is_gemini and pending_signature is None:
454
453
  pending_signature = thought_signature
455
454
 
@@ -465,7 +464,6 @@ class AntigravityStreamingResponse(StreamedResponse):
465
464
  yield event
466
465
 
467
466
  # For Claude: signature is ON the thinking block itself
468
- # We need to explicitly set it after the part is created
469
467
  if thought_signature and not is_gemini:
470
468
  for existing_part in reversed(self._parts_manager._parts):
471
469
  if isinstance(existing_part, ThinkingPart):
@@ -490,13 +488,10 @@ class AntigravityStreamingResponse(StreamedResponse):
490
488
  elif part.get("functionCall"):
491
489
  fc = part["functionCall"]
492
490
 
493
- # For Gemini: the signature on a function call belongs to the
494
- # PREVIOUS thinking block. We need to retroactively set it.
491
+ # For Gemini: signature on function call belongs to previous thinking
495
492
  if is_gemini and thought_signature:
496
- # Find the most recent ThinkingPart and set its signature
497
493
  for existing_part in reversed(self._parts_manager._parts):
498
494
  if isinstance(existing_part, ThinkingPart):
499
- # Directly set the signature attribute
500
495
  object.__setattr__(
501
496
  existing_part, "signature", thought_signature
502
497
  )
@@ -506,7 +501,7 @@ class AntigravityStreamingResponse(StreamedResponse):
506
501
  vendor_part_id=uuid4(),
507
502
  tool_name=fc.get("name"),
508
503
  args=fc.get("args"),
509
- tool_call_id=fc.get("id") or _utils.generate_tool_call_id(),
504
+ tool_call_id=fc.get("id") or generate_tool_call_id(),
510
505
  )
511
506
  if event:
512
507
  yield event
@@ -524,13 +519,6 @@ class AntigravityStreamingResponse(StreamedResponse):
524
519
  return self._timestamp_val
525
520
 
526
521
 
527
- # Bypass signature for when no real thought signature is available.
528
- # Gemini API requires EVERY function call to have a thoughtSignature field.
529
- # When there's no thinking block or no signature was captured, we use this bypass.
530
- # This specific key is the official bypass token for Gemini 3 Pro.
531
- BYPASS_THOUGHT_SIGNATURE = "context_engineering_is_the_way_to_go"
532
-
533
-
534
522
  def _antigravity_content_model_response(
535
523
  m: ModelResponse, provider_name: str, model_name: str = ""
536
524
  ) -> ContentDict | None:
@@ -538,20 +526,13 @@ def _antigravity_content_model_response(
538
526
 
539
527
  Handles different signature protocols:
540
528
  - Claude models: signature goes ON the thinking block itself
541
- - Gemini models: signature goes on the NEXT part (function_call or text) after thinking
542
-
543
- IMPORTANT: For Gemini, EVERY function call MUST have a thoughtSignature field.
544
- If no real signature is available (no preceding ThinkingPart, or ThinkingPart
545
- had no signature), we use BYPASS_THOUGHT_SIGNATURE as a fallback.
529
+ - Gemini models: signature goes on the NEXT part after thinking
546
530
  """
547
531
  parts: list[PartDict] = []
548
532
 
549
- # Determine which protocol to use based on model name
550
533
  is_claude = "claude" in model_name.lower()
551
534
  is_gemini = "gemini" in model_name.lower()
552
535
 
553
- # For Gemini: save signature from ThinkingPart to attach to next part
554
- # Initialize to None - we'll use BYPASS_THOUGHT_SIGNATURE if still None when needed
555
536
  pending_signature: str | None = None
556
537
 
557
538
  for item in m.parts:
@@ -563,11 +544,7 @@ def _antigravity_content_model_response(
563
544
  )
564
545
  part["function_call"] = function_call
565
546
 
566
- # For Gemini: ALWAYS attach a thoughtSignature to function calls.
567
- # Use the real signature if available, otherwise use bypass.
568
- # NOTE: Do NOT clear pending_signature here! Multiple tool calls
569
- # in a row (e.g., parallel function calls) all need the same
570
- # signature from the preceding ThinkingPart.
547
+ # For Gemini: ALWAYS attach a thoughtSignature to function calls
571
548
  if is_gemini:
572
549
  part["thoughtSignature"] = (
573
550
  pending_signature
@@ -578,8 +555,6 @@ def _antigravity_content_model_response(
578
555
  elif isinstance(item, TextPart):
579
556
  part["text"] = item.content
580
557
 
581
- # For Gemini: attach pending signature to text part if available
582
- # Clear signature after text since text typically ends a response
583
558
  if is_gemini and pending_signature is not None:
584
559
  part["thoughtSignature"] = pending_signature
585
560
  pending_signature = None
@@ -589,32 +564,29 @@ def _antigravity_content_model_response(
589
564
  part["text"] = item.content
590
565
  part["thought"] = True
591
566
 
567
+ # Try to use original signature first. If the API rejects it
568
+ # (Gemini: "Corrupted thought signature", Claude: "thinking.signature: Field required"),
569
+ # we'll backfill with bypass signatures and retry.
592
570
  if item.signature:
593
571
  if is_claude:
594
- # Claude: signature goes ON the thinking block
572
+ # Claude expects signature ON the thinking block
595
573
  part["thoughtSignature"] = item.signature
596
574
  elif is_gemini:
597
- # Gemini: save signature for NEXT part
575
+ # Gemini expects signature on the NEXT part
598
576
  pending_signature = item.signature
599
577
  else:
600
- # Default: try both (put on thinking block)
601
578
  part["thoughtSignature"] = item.signature
602
579
  elif is_gemini:
603
- # ThinkingPart exists but has no signature - use bypass
604
- # This ensures subsequent tool calls still get a signature
605
580
  pending_signature = BYPASS_THOUGHT_SIGNATURE
606
581
 
607
582
  elif isinstance(item, BuiltinToolCallPart):
608
- # Skip code execution for now
609
583
  pass
610
584
 
611
585
  elif isinstance(item, BuiltinToolReturnPart):
612
- # Skip code execution result
613
586
  pass
614
587
 
615
588
  elif isinstance(item, FilePart):
616
589
  content = item.content
617
- # Ensure data is base64 string, not bytes
618
590
  data_val = content.data
619
591
  if isinstance(data_val, bytes):
620
592
  data_val = base64.b64encode(data_val).decode("utf-8")
@@ -636,25 +608,19 @@ def _antigravity_content_model_response(
636
608
 
637
609
 
638
610
  def _antigravity_process_response_from_parts(
639
- parts: list[Any], # dicts or objects
611
+ parts: list[Any],
640
612
  grounding_metadata: Any | None,
641
- model_name: GoogleModelName,
613
+ model_name: str,
642
614
  provider_name: str,
643
615
  usage: RequestUsage,
644
616
  vendor_id: str | None,
645
617
  vendor_details: dict[str, Any] | None = None,
646
618
  ) -> ModelResponse:
647
- """Custom response parser that extracts signatures from ThinkingParts.
648
-
649
- Handles different signature protocols:
650
- - Claude: signature is ON the thinking block
651
- - Gemini: signature is on the NEXT part after thinking (we associate it back)
652
- """
619
+ """Custom response parser that extracts signatures from ThinkingParts."""
653
620
  items: list[ModelResponsePart] = []
654
621
 
655
622
  is_gemini = "gemini" in str(model_name).lower()
656
623
 
657
- # Helper to get attribute from dict or object
658
624
  def get_attr(obj, attr):
659
625
  if isinstance(obj, dict):
660
626
  return obj.get(attr)
@@ -667,7 +633,6 @@ def _antigravity_process_response_from_parts(
667
633
  part, "thought_signature"
668
634
  )
669
635
 
670
- # Also check provider details
671
636
  pd = get_attr(part, "provider_details")
672
637
  if not thought_signature and pd:
673
638
  thought_signature = pd.get("thought_signature") or pd.get(
@@ -676,7 +641,6 @@ def _antigravity_process_response_from_parts(
676
641
 
677
642
  text = get_attr(part, "text")
678
643
  thought = get_attr(part, "thought")
679
- # API returns camelCase 'functionCall'
680
644
  function_call = get_attr(part, "functionCall") or get_attr(
681
645
  part, "function_call"
682
646
  )
@@ -694,7 +658,6 @@ def _antigravity_process_response_from_parts(
694
658
  if is_gemini:
695
659
  for i, pp in enumerate(parsed_parts):
696
660
  if pp["thought"] and not pp["signature"]:
697
- # Look at next part for signature
698
661
  if i + 1 < len(parsed_parts):
699
662
  next_sig = parsed_parts[i + 1].get("signature")
700
663
  if next_sig:
@@ -714,7 +677,7 @@ def _antigravity_process_response_from_parts(
714
677
  fc = pp["function_call"]
715
678
  fc_name = get_attr(fc, "name")
716
679
  fc_args = get_attr(fc, "args")
717
- fc_id = get_attr(fc, "id") or _utils.generate_tool_call_id()
680
+ fc_id = get_attr(fc, "id") or generate_tool_call_id()
718
681
 
719
682
  items.append(
720
683
  ToolCallPart(tool_name=fc_name, args=fc_args, tool_call_id=fc_id)
@@ -10,8 +10,8 @@ from typing import Any, Dict, List, Optional, Tuple
10
10
  from urllib.parse import parse_qs, urlparse
11
11
 
12
12
  from code_puppy.callbacks import register_callback
13
- from code_puppy.config import set_model_name
14
13
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
14
+ from code_puppy.model_switching import set_model_and_reload_agent
15
15
 
16
16
  from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
17
17
  from .accounts import AccountManager
@@ -165,8 +165,16 @@ def _await_callback(context: Any) -> Optional[Tuple[str, str, str]]:
165
165
  return result.code, result.state, redirect_uri
166
166
 
167
167
 
168
- def _perform_authentication(add_account: bool = False) -> bool:
169
- """Run the OAuth authentication flow."""
168
+ def _perform_authentication(
169
+ add_account: bool = False,
170
+ reload_agent: bool = True,
171
+ ) -> bool:
172
+ """Run the OAuth authentication flow.
173
+
174
+ Args:
175
+ add_account: Whether to add a new account to the pool.
176
+ reload_agent: Whether to reload the current agent after auth.
177
+ """
170
178
  context = prepare_oauth_context()
171
179
  callback_result = _await_callback(context)
172
180
 
@@ -226,8 +234,8 @@ def _perform_authentication(add_account: bool = False) -> bool:
226
234
  else:
227
235
  emit_warning("Failed to configure models. Try running /antigravity-auth again.")
228
236
 
229
- # Reload agent
230
- reload_current_agent()
237
+ if reload_agent:
238
+ reload_current_agent()
231
239
  return True
232
240
 
233
241
 
@@ -378,9 +386,8 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
378
386
  "Existing tokens found. This will refresh your authentication."
379
387
  )
380
388
 
381
- if _perform_authentication():
382
- # Set a default model
383
- set_model_name("antigravity-gemini-3-pro-high")
389
+ if _perform_authentication(reload_agent=False):
390
+ set_model_and_reload_agent("antigravity-gemini-3-pro-high")
384
391
  return True
385
392
 
386
393
  if name == "antigravity-add":