code-puppy 0.0.348__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 (87) hide show
  1. code_puppy/agents/__init__.py +8 -0
  2. code_puppy/agents/agent_manager.py +272 -1
  3. code_puppy/agents/agent_pack_leader.py +383 -0
  4. code_puppy/agents/agent_qa_kitten.py +12 -7
  5. code_puppy/agents/agent_terminal_qa.py +323 -0
  6. code_puppy/agents/base_agent.py +11 -8
  7. code_puppy/agents/event_stream_handler.py +101 -8
  8. code_puppy/agents/pack/__init__.py +34 -0
  9. code_puppy/agents/pack/bloodhound.py +304 -0
  10. code_puppy/agents/pack/husky.py +321 -0
  11. code_puppy/agents/pack/retriever.py +393 -0
  12. code_puppy/agents/pack/shepherd.py +348 -0
  13. code_puppy/agents/pack/terrier.py +287 -0
  14. code_puppy/agents/pack/watchdog.py +367 -0
  15. code_puppy/agents/subagent_stream_handler.py +276 -0
  16. code_puppy/api/__init__.py +13 -0
  17. code_puppy/api/app.py +169 -0
  18. code_puppy/api/main.py +21 -0
  19. code_puppy/api/pty_manager.py +446 -0
  20. code_puppy/api/routers/__init__.py +12 -0
  21. code_puppy/api/routers/agents.py +36 -0
  22. code_puppy/api/routers/commands.py +217 -0
  23. code_puppy/api/routers/config.py +74 -0
  24. code_puppy/api/routers/sessions.py +232 -0
  25. code_puppy/api/templates/terminal.html +361 -0
  26. code_puppy/api/websocket.py +154 -0
  27. code_puppy/callbacks.py +73 -0
  28. code_puppy/chatgpt_codex_client.py +53 -0
  29. code_puppy/claude_cache_client.py +294 -41
  30. code_puppy/command_line/add_model_menu.py +13 -4
  31. code_puppy/command_line/agent_menu.py +662 -0
  32. code_puppy/command_line/core_commands.py +89 -112
  33. code_puppy/command_line/model_picker_completion.py +3 -20
  34. code_puppy/command_line/model_settings_menu.py +21 -3
  35. code_puppy/config.py +145 -70
  36. code_puppy/gemini_model.py +706 -0
  37. code_puppy/http_utils.py +6 -3
  38. code_puppy/messaging/__init__.py +15 -0
  39. code_puppy/messaging/messages.py +27 -0
  40. code_puppy/messaging/queue_console.py +1 -1
  41. code_puppy/messaging/rich_renderer.py +36 -1
  42. code_puppy/messaging/spinner/__init__.py +20 -2
  43. code_puppy/messaging/subagent_console.py +461 -0
  44. code_puppy/model_factory.py +50 -16
  45. code_puppy/model_switching.py +63 -0
  46. code_puppy/model_utils.py +27 -24
  47. code_puppy/models.json +12 -12
  48. code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
  49. code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
  50. code_puppy/plugins/antigravity_oauth/transport.py +236 -45
  51. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
  52. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
  53. code_puppy/plugins/claude_code_oauth/utils.py +4 -1
  54. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  55. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  56. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  57. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  58. code_puppy/pydantic_patches.py +52 -0
  59. code_puppy/status_display.py +6 -2
  60. code_puppy/tools/__init__.py +37 -1
  61. code_puppy/tools/agent_tools.py +83 -33
  62. code_puppy/tools/browser/__init__.py +37 -0
  63. code_puppy/tools/browser/browser_control.py +6 -6
  64. code_puppy/tools/browser/browser_interactions.py +21 -20
  65. code_puppy/tools/browser/browser_locators.py +9 -9
  66. code_puppy/tools/browser/browser_manager.py +316 -0
  67. code_puppy/tools/browser/browser_navigation.py +7 -7
  68. code_puppy/tools/browser/browser_screenshot.py +78 -140
  69. code_puppy/tools/browser/browser_scripts.py +15 -13
  70. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  71. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  72. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  73. code_puppy/tools/browser/terminal_tools.py +525 -0
  74. code_puppy/tools/command_runner.py +292 -101
  75. code_puppy/tools/common.py +176 -1
  76. code_puppy/tools/display.py +84 -0
  77. code_puppy/tools/subagent_context.py +158 -0
  78. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
  79. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
  80. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
  81. code_puppy/prompts/codex_system_prompt.md +0 -310
  82. code_puppy/tools/browser/camoufox_manager.py +0 -235
  83. code_puppy/tools/browser/vqa_agent.py +0 -90
  84. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
  85. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
  86. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
  87. {code_puppy-0.0.348.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
+
49
73
 
50
- class AntigravityModel(GoogleModel):
51
- """Custom GoogleModel that correctly handles Claude thinking signatures via Antigravity."""
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 = []
108
+
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,44 +242,52 @@ 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
- raise RuntimeError(
219
- f"Antigravity API Error {response.status_code}: {response.text}"
220
- )
257
+ error_text = response.text
258
+ if response.status_code == 400 and _is_signature_error(error_text):
259
+ logger.warning(
260
+ "Received 400 signature error. Backfilling with bypass signatures and retrying. Error: %s",
261
+ error_text[:200],
262
+ )
263
+ _backfill_thought_signatures(messages)
264
+
265
+ # Re-map messages
266
+ system_instruction, contents = await self._map_messages(
267
+ messages, model_request_parameters
268
+ )
269
+
270
+ # Update body
271
+ body["contents"] = contents
272
+ if system_instruction:
273
+ body["systemInstruction"] = system_instruction
274
+
275
+ # Retry request
276
+ response = await client.post(url, json=body)
277
+ if response.status_code != 200:
278
+ raise RuntimeError(
279
+ f"Antigravity API Error {response.status_code}: {response.text}"
280
+ )
281
+ else:
282
+ raise RuntimeError(
283
+ f"Antigravity API Error {response.status_code}: {error_text}"
284
+ )
221
285
 
222
286
  data = response.json()
223
287
 
224
288
  # Extract candidates
225
289
  candidates = data.get("candidates", [])
226
290
  if not candidates:
227
- # Handle empty response or safety block?
228
291
  return ModelResponse(
229
292
  parts=[TextPart(content="")],
230
293
  model_name=self._model_name,
@@ -259,31 +322,13 @@ class AntigravityModel(GoogleModel):
259
322
  model_request_parameters: ModelRequestParameters,
260
323
  run_context: RunContext[Any] | None = None,
261
324
  ) -> AsyncIterator[StreamedResponse]:
262
- """Override request_stream to use streaming with proper signature handling."""
263
- # Prepare request
264
- model_settings, model_request_parameters = self.prepare_request(
265
- model_settings, model_request_parameters
266
- )
267
-
325
+ """Override request_stream for streaming with signature handling."""
268
326
  system_instruction, contents = await self._map_messages(
269
327
  messages, model_request_parameters
270
328
  )
271
329
 
272
330
  # Build generation config
273
- gen_config: dict[str, Any] = {}
274
- if model_settings:
275
- if (
276
- hasattr(model_settings, "temperature")
277
- and model_settings.temperature is not None
278
- ):
279
- gen_config["temperature"] = model_settings.temperature
280
- if hasattr(model_settings, "top_p") and model_settings.top_p is not None:
281
- gen_config["topP"] = model_settings.top_p
282
- if (
283
- hasattr(model_settings, "max_tokens")
284
- and model_settings.max_tokens is not None
285
- ):
286
- gen_config["maxOutputTokens"] = model_settings.max_tokens
331
+ gen_config = self._build_generation_config(model_settings)
287
332
 
288
333
  # Build request body
289
334
  body: dict[str, Any] = {"contents": contents}
@@ -294,48 +339,66 @@ class AntigravityModel(GoogleModel):
294
339
 
295
340
  # Add tools
296
341
  if model_request_parameters.function_tools:
297
- funcs = []
298
- for t in model_request_parameters.function_tools:
299
- funcs.append(
300
- {
301
- "name": t.name,
302
- "description": t.description,
303
- "parameters": t.parameters_json_schema,
304
- }
305
- )
306
- body["tools"] = [{"functionDeclarations": funcs}]
342
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
307
343
 
308
344
  # Get httpx client
309
- try:
310
- client = self.client._api_client._async_httpx_client
311
- except AttributeError:
312
- raise RuntimeError(
313
- "AntigravityModel requires access to the underlying httpx client"
314
- )
315
-
316
- # Use streaming endpoint
345
+ client = await self._get_client()
317
346
  url = f"/models/{self._model_name}:streamGenerateContent?alt=sse"
318
347
 
319
348
  # Create async generator for SSE events
320
349
  async def stream_chunks() -> AsyncIterator[dict[str, Any]]:
321
- async with client.stream("POST", url, json=body) as response:
322
- if response.status_code != 200:
323
- text = await response.aread()
324
- raise RuntimeError(
325
- f"Antigravity API Error {response.status_code}: {text.decode()}"
326
- )
350
+ retry_count = 0
351
+ nonlocal body # Allow modification for retry
352
+
353
+ while retry_count < 2:
354
+ should_retry = False
355
+ async with client.stream("POST", url, json=body) as response:
356
+ if response.status_code != 200:
357
+ text = await response.aread()
358
+ error_msg = text.decode()
359
+ if (
360
+ response.status_code == 400
361
+ and _is_signature_error(error_msg)
362
+ and retry_count == 0
363
+ ):
364
+ should_retry = True
365
+ else:
366
+ raise RuntimeError(
367
+ f"Antigravity API Error {response.status_code}: {error_msg}"
368
+ )
327
369
 
328
- async for line in response.aiter_lines():
329
- line = line.strip()
330
- if not line:
331
- continue
332
- if line.startswith("data: "):
333
- json_str = line[6:] # Remove 'data: ' prefix
334
- if json_str:
335
- try:
336
- yield json.loads(json_str)
337
- except json.JSONDecodeError:
370
+ if not should_retry:
371
+ async for line in response.aiter_lines():
372
+ line = line.strip()
373
+ if not line:
338
374
  continue
375
+ if line.startswith("data: "):
376
+ json_str = line[6:]
377
+ if json_str:
378
+ try:
379
+ yield json.loads(json_str)
380
+ except json.JSONDecodeError:
381
+ continue
382
+ return
383
+
384
+ # Handle retry outside the context manager
385
+ if should_retry:
386
+ logger.warning(
387
+ "Received 400 signature error in stream. Backfilling with bypass signatures and retrying."
388
+ )
389
+ _backfill_thought_signatures(messages)
390
+
391
+ # Re-map messages
392
+ system_instruction, contents = await self._map_messages(
393
+ messages, model_request_parameters
394
+ )
395
+
396
+ # Update body
397
+ body["contents"] = contents
398
+ if system_instruction:
399
+ body["systemInstruction"] = system_instruction
400
+
401
+ retry_count += 1
339
402
 
340
403
  # Create streaming response
341
404
  streamed = AntigravityStreamingResponse(
@@ -383,11 +446,9 @@ class AntigravityStreamingResponse(StreamedResponse):
383
446
  parts = content.get("parts", [])
384
447
 
385
448
  for part in parts:
386
- # Extract signature (for Gemini, it's on the functionCall part)
449
+ # Extract signature
387
450
  thought_signature = part.get("thoughtSignature")
388
451
  if thought_signature:
389
- # For Gemini: if this is a function call with signature,
390
- # the signature belongs to the previous thinking block
391
452
  if is_gemini and pending_signature is None:
392
453
  pending_signature = thought_signature
393
454
 
@@ -403,7 +464,6 @@ class AntigravityStreamingResponse(StreamedResponse):
403
464
  yield event
404
465
 
405
466
  # For Claude: signature is ON the thinking block itself
406
- # We need to explicitly set it after the part is created
407
467
  if thought_signature and not is_gemini:
408
468
  for existing_part in reversed(self._parts_manager._parts):
409
469
  if isinstance(existing_part, ThinkingPart):
@@ -428,13 +488,10 @@ class AntigravityStreamingResponse(StreamedResponse):
428
488
  elif part.get("functionCall"):
429
489
  fc = part["functionCall"]
430
490
 
431
- # For Gemini: the signature on a function call belongs to the
432
- # PREVIOUS thinking block. We need to retroactively set it.
491
+ # For Gemini: signature on function call belongs to previous thinking
433
492
  if is_gemini and thought_signature:
434
- # Find the most recent ThinkingPart and set its signature
435
493
  for existing_part in reversed(self._parts_manager._parts):
436
494
  if isinstance(existing_part, ThinkingPart):
437
- # Directly set the signature attribute
438
495
  object.__setattr__(
439
496
  existing_part, "signature", thought_signature
440
497
  )
@@ -444,7 +501,7 @@ class AntigravityStreamingResponse(StreamedResponse):
444
501
  vendor_part_id=uuid4(),
445
502
  tool_name=fc.get("name"),
446
503
  args=fc.get("args"),
447
- tool_call_id=fc.get("id") or _utils.generate_tool_call_id(),
504
+ tool_call_id=fc.get("id") or generate_tool_call_id(),
448
505
  )
449
506
  if event:
450
507
  yield event
@@ -462,13 +519,6 @@ class AntigravityStreamingResponse(StreamedResponse):
462
519
  return self._timestamp_val
463
520
 
464
521
 
465
- # Bypass signature for when no real thought signature is available.
466
- # Gemini API requires EVERY function call to have a thoughtSignature field.
467
- # When there's no thinking block or no signature was captured, we use this bypass.
468
- # This specific key is the official bypass token for Gemini 3 Pro.
469
- BYPASS_THOUGHT_SIGNATURE = "context_engineering_is_the_way_to_go"
470
-
471
-
472
522
  def _antigravity_content_model_response(
473
523
  m: ModelResponse, provider_name: str, model_name: str = ""
474
524
  ) -> ContentDict | None:
@@ -476,20 +526,13 @@ def _antigravity_content_model_response(
476
526
 
477
527
  Handles different signature protocols:
478
528
  - Claude models: signature goes ON the thinking block itself
479
- - Gemini models: signature goes on the NEXT part (function_call or text) after thinking
480
-
481
- IMPORTANT: For Gemini, EVERY function call MUST have a thoughtSignature field.
482
- If no real signature is available (no preceding ThinkingPart, or ThinkingPart
483
- had no signature), we use BYPASS_THOUGHT_SIGNATURE as a fallback.
529
+ - Gemini models: signature goes on the NEXT part after thinking
484
530
  """
485
531
  parts: list[PartDict] = []
486
532
 
487
- # Determine which protocol to use based on model name
488
533
  is_claude = "claude" in model_name.lower()
489
534
  is_gemini = "gemini" in model_name.lower()
490
535
 
491
- # For Gemini: save signature from ThinkingPart to attach to next part
492
- # Initialize to None - we'll use BYPASS_THOUGHT_SIGNATURE if still None when needed
493
536
  pending_signature: str | None = None
494
537
 
495
538
  for item in m.parts:
@@ -501,11 +544,7 @@ def _antigravity_content_model_response(
501
544
  )
502
545
  part["function_call"] = function_call
503
546
 
504
- # For Gemini: ALWAYS attach a thoughtSignature to function calls.
505
- # Use the real signature if available, otherwise use bypass.
506
- # NOTE: Do NOT clear pending_signature here! Multiple tool calls
507
- # in a row (e.g., parallel function calls) all need the same
508
- # signature from the preceding ThinkingPart.
547
+ # For Gemini: ALWAYS attach a thoughtSignature to function calls
509
548
  if is_gemini:
510
549
  part["thoughtSignature"] = (
511
550
  pending_signature
@@ -516,8 +555,6 @@ def _antigravity_content_model_response(
516
555
  elif isinstance(item, TextPart):
517
556
  part["text"] = item.content
518
557
 
519
- # For Gemini: attach pending signature to text part if available
520
- # Clear signature after text since text typically ends a response
521
558
  if is_gemini and pending_signature is not None:
522
559
  part["thoughtSignature"] = pending_signature
523
560
  pending_signature = None
@@ -527,32 +564,29 @@ def _antigravity_content_model_response(
527
564
  part["text"] = item.content
528
565
  part["thought"] = True
529
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.
530
570
  if item.signature:
531
571
  if is_claude:
532
- # Claude: signature goes ON the thinking block
572
+ # Claude expects signature ON the thinking block
533
573
  part["thoughtSignature"] = item.signature
534
574
  elif is_gemini:
535
- # Gemini: save signature for NEXT part
575
+ # Gemini expects signature on the NEXT part
536
576
  pending_signature = item.signature
537
577
  else:
538
- # Default: try both (put on thinking block)
539
578
  part["thoughtSignature"] = item.signature
540
579
  elif is_gemini:
541
- # ThinkingPart exists but has no signature - use bypass
542
- # This ensures subsequent tool calls still get a signature
543
580
  pending_signature = BYPASS_THOUGHT_SIGNATURE
544
581
 
545
582
  elif isinstance(item, BuiltinToolCallPart):
546
- # Skip code execution for now
547
583
  pass
548
584
 
549
585
  elif isinstance(item, BuiltinToolReturnPart):
550
- # Skip code execution result
551
586
  pass
552
587
 
553
588
  elif isinstance(item, FilePart):
554
589
  content = item.content
555
- # Ensure data is base64 string, not bytes
556
590
  data_val = content.data
557
591
  if isinstance(data_val, bytes):
558
592
  data_val = base64.b64encode(data_val).decode("utf-8")
@@ -574,25 +608,19 @@ def _antigravity_content_model_response(
574
608
 
575
609
 
576
610
  def _antigravity_process_response_from_parts(
577
- parts: list[Any], # dicts or objects
611
+ parts: list[Any],
578
612
  grounding_metadata: Any | None,
579
- model_name: GoogleModelName,
613
+ model_name: str,
580
614
  provider_name: str,
581
615
  usage: RequestUsage,
582
616
  vendor_id: str | None,
583
617
  vendor_details: dict[str, Any] | None = None,
584
618
  ) -> ModelResponse:
585
- """Custom response parser that extracts signatures from ThinkingParts.
586
-
587
- Handles different signature protocols:
588
- - Claude: signature is ON the thinking block
589
- - Gemini: signature is on the NEXT part after thinking (we associate it back)
590
- """
619
+ """Custom response parser that extracts signatures from ThinkingParts."""
591
620
  items: list[ModelResponsePart] = []
592
621
 
593
622
  is_gemini = "gemini" in str(model_name).lower()
594
623
 
595
- # Helper to get attribute from dict or object
596
624
  def get_attr(obj, attr):
597
625
  if isinstance(obj, dict):
598
626
  return obj.get(attr)
@@ -605,7 +633,6 @@ def _antigravity_process_response_from_parts(
605
633
  part, "thought_signature"
606
634
  )
607
635
 
608
- # Also check provider details
609
636
  pd = get_attr(part, "provider_details")
610
637
  if not thought_signature and pd:
611
638
  thought_signature = pd.get("thought_signature") or pd.get(
@@ -614,7 +641,6 @@ def _antigravity_process_response_from_parts(
614
641
 
615
642
  text = get_attr(part, "text")
616
643
  thought = get_attr(part, "thought")
617
- # API returns camelCase 'functionCall'
618
644
  function_call = get_attr(part, "functionCall") or get_attr(
619
645
  part, "function_call"
620
646
  )
@@ -632,7 +658,6 @@ def _antigravity_process_response_from_parts(
632
658
  if is_gemini:
633
659
  for i, pp in enumerate(parsed_parts):
634
660
  if pp["thought"] and not pp["signature"]:
635
- # Look at next part for signature
636
661
  if i + 1 < len(parsed_parts):
637
662
  next_sig = parsed_parts[i + 1].get("signature")
638
663
  if next_sig:
@@ -652,7 +677,7 @@ def _antigravity_process_response_from_parts(
652
677
  fc = pp["function_call"]
653
678
  fc_name = get_attr(fc, "name")
654
679
  fc_args = get_attr(fc, "args")
655
- fc_id = get_attr(fc, "id") or _utils.generate_tool_call_id()
680
+ fc_id = get_attr(fc, "id") or generate_tool_call_id()
656
681
 
657
682
  items.append(
658
683
  ToolCallPart(tool_name=fc_name, args=fc_args, tool_call_id=fc_id)
@@ -666,3 +691,12 @@ def _antigravity_process_response_from_parts(
666
691
  provider_details=vendor_details,
667
692
  provider_name=provider_name,
668
693
  )
694
+
695
+
696
+ def _backfill_thought_signatures(messages: list[ModelMessage]) -> None:
697
+ """Backfill all thinking parts with the bypass signature."""
698
+ for m in messages:
699
+ if isinstance(m, ModelResponse):
700
+ for part in m.parts:
701
+ if isinstance(part, ThinkingPart):
702
+ object.__setattr__(part, "signature", BYPASS_THOUGHT_SIGNATURE)
@@ -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":