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.
- code_puppy/agents/__init__.py +6 -0
- code_puppy/agents/agent_manager.py +223 -1
- code_puppy/agents/base_agent.py +2 -12
- code_puppy/chatgpt_codex_client.py +53 -0
- code_puppy/claude_cache_client.py +45 -7
- code_puppy/command_line/add_model_menu.py +13 -4
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/core_commands.py +4 -112
- code_puppy/command_line/model_picker_completion.py +3 -20
- code_puppy/command_line/model_settings_menu.py +21 -3
- code_puppy/config.py +79 -8
- code_puppy/gemini_model.py +706 -0
- code_puppy/http_utils.py +6 -3
- code_puppy/model_factory.py +50 -16
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +1 -52
- code_puppy/models.json +12 -12
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +128 -165
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
- code_puppy/plugins/antigravity_oauth/transport.py +235 -45
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
- code_puppy/plugins/claude_code_oauth/utils.py +4 -1
- code_puppy/pydantic_patches.py +52 -0
- code_puppy/tools/agent_tools.py +3 -3
- code_puppy/tools/browser/__init__.py +1 -1
- code_puppy/tools/browser/browser_control.py +1 -1
- code_puppy/tools/browser/browser_interactions.py +1 -1
- code_puppy/tools/browser/browser_locators.py +1 -1
- code_puppy/tools/browser/{camoufox_manager.py → browser_manager.py} +29 -110
- code_puppy/tools/browser/browser_navigation.py +1 -1
- code_puppy/tools/browser/browser_screenshot.py +1 -1
- code_puppy/tools/browser/browser_scripts.py +1 -1
- {code_puppy-0.0.361.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
- {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/METADATA +5 -6
- {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/RECORD +40 -38
- code_puppy/prompts/codex_system_prompt.md +0 -310
- {code_puppy-0.0.361.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.361.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
245
|
+
# Serialize tools
|
|
191
246
|
if model_request_parameters.function_tools:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:]
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
572
|
+
# Claude expects signature ON the thinking block
|
|
595
573
|
part["thoughtSignature"] = item.signature
|
|
596
574
|
elif is_gemini:
|
|
597
|
-
# Gemini
|
|
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],
|
|
611
|
+
parts: list[Any],
|
|
640
612
|
grounding_metadata: Any | None,
|
|
641
|
-
model_name:
|
|
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
|
|
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(
|
|
169
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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":
|