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.
- code_puppy/agents/__init__.py +8 -0
- code_puppy/agents/agent_manager.py +272 -1
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +11 -8
- code_puppy/agents/event_stream_handler.py +101 -8
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/chatgpt_codex_client.py +53 -0
- code_puppy/claude_cache_client.py +294 -41
- 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 +89 -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 +145 -70
- code_puppy/gemini_model.py +706 -0
- code_puppy/http_utils.py +6 -3
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +50 -16
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +27 -24
- code_puppy/models.json +12 -12
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
- code_puppy/plugins/antigravity_oauth/transport.py +236 -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/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +52 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +83 -33
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
- code_puppy/prompts/codex_system_prompt.md +0 -310
- code_puppy/tools/browser/camoufox_manager.py +0 -235
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
- {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(
|
|
51
|
-
"""Custom
|
|
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
|
|
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,44 +242,52 @@ 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
|
-
|
|
219
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
572
|
+
# Claude expects signature ON the thinking block
|
|
533
573
|
part["thoughtSignature"] = item.signature
|
|
534
574
|
elif is_gemini:
|
|
535
|
-
# Gemini
|
|
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],
|
|
611
|
+
parts: list[Any],
|
|
578
612
|
grounding_metadata: Any | None,
|
|
579
|
-
model_name:
|
|
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
|
|
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(
|
|
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":
|