codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,706 @@
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
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import json
11
+ import logging
12
+ from collections.abc import AsyncIterator
13
+ from contextlib import asynccontextmanager
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timezone
16
+ from typing import Any
17
+ from uuid import uuid4
18
+
19
+ from pydantic_ai._run_context import RunContext
20
+ from pydantic_ai.messages import (
21
+ BuiltinToolCallPart,
22
+ BuiltinToolReturnPart,
23
+ FilePart,
24
+ ModelMessage,
25
+ ModelRequest,
26
+ ModelResponse,
27
+ ModelResponsePart,
28
+ ModelResponseStreamEvent,
29
+ RetryPromptPart,
30
+ SystemPromptPart,
31
+ TextPart,
32
+ ThinkingPart,
33
+ ToolCallPart,
34
+ ToolReturnPart,
35
+ UserPromptPart,
36
+ )
37
+ from pydantic_ai.models import ModelRequestParameters, StreamedResponse
38
+ from pydantic_ai.settings import ModelSettings
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
48
+
49
+ logger = logging.getLogger(__name__)
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 = []
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}]
123
+
124
+ async def _map_messages(
125
+ self,
126
+ messages: list[ModelMessage],
127
+ model_request_parameters: ModelRequestParameters,
128
+ ) -> tuple[ContentDict | None, list[dict]]:
129
+ """Map messages to Gemini API format, preserving thinking signatures.
130
+
131
+ IMPORTANT: For Gemini with parallel function calls, the API expects:
132
+ - Model message: [FC1 + signature, FC2, ...] (all function calls together)
133
+ - User message: [FR1, FR2, ...] (all function responses together)
134
+
135
+ If messages are interleaved (FC1, FR1, FC2, FR2), the API returns 400.
136
+ This method merges consecutive same-role messages to fix this.
137
+ """
138
+ contents: list[dict] = []
139
+ system_parts: list[PartDict] = []
140
+
141
+ for m in messages:
142
+ if isinstance(m, ModelRequest):
143
+ message_parts: list[PartDict] = []
144
+
145
+ for part in m.parts:
146
+ if isinstance(part, SystemPromptPart):
147
+ system_parts.append({"text": part.content})
148
+ elif isinstance(part, UserPromptPart):
149
+ # Use parent's _map_user_prompt
150
+ mapped_parts = await self._map_user_prompt(part)
151
+ # Sanitize bytes to base64 for JSON serialization
152
+ for mp in mapped_parts:
153
+ if "inline_data" in mp and "data" in mp["inline_data"]:
154
+ data = mp["inline_data"]["data"]
155
+ if isinstance(data, bytes):
156
+ mp["inline_data"]["data"] = base64.b64encode(
157
+ data
158
+ ).decode("utf-8")
159
+ message_parts.extend(mapped_parts)
160
+ elif isinstance(part, ToolReturnPart):
161
+ message_parts.append(
162
+ {
163
+ "function_response": {
164
+ "name": part.tool_name,
165
+ "response": part.model_response_object(),
166
+ "id": part.tool_call_id,
167
+ }
168
+ }
169
+ )
170
+ elif isinstance(part, RetryPromptPart):
171
+ if part.tool_name is None:
172
+ message_parts.append({"text": part.model_response()})
173
+ else:
174
+ message_parts.append(
175
+ {
176
+ "function_response": {
177
+ "name": part.tool_name,
178
+ "response": {"error": part.model_response()},
179
+ "id": part.tool_call_id,
180
+ }
181
+ }
182
+ )
183
+ else:
184
+ assert_never(part)
185
+
186
+ if message_parts:
187
+ # Merge with previous user message if exists (for parallel function responses)
188
+ if contents and contents[-1].get("role") == "user":
189
+ contents[-1]["parts"].extend(message_parts)
190
+ else:
191
+ contents.append({"role": "user", "parts": message_parts})
192
+
193
+ elif isinstance(m, ModelResponse):
194
+ # Use custom helper for thinking signature handling
195
+ maybe_content = _antigravity_content_model_response(
196
+ m, self.system, self._model_name
197
+ )
198
+ if maybe_content:
199
+ # Merge with previous model message if exists (for parallel function calls)
200
+ if contents and contents[-1].get("role") == "model":
201
+ contents[-1]["parts"].extend(maybe_content["parts"])
202
+ else:
203
+ contents.append(maybe_content)
204
+ else:
205
+ assert_never(m)
206
+
207
+ # Google GenAI requires at least one part in the message.
208
+ if not contents:
209
+ contents = [{"role": "user", "parts": [{"text": ""}]}]
210
+
211
+ # Get any injected instructions
212
+ instructions = self._get_instructions(messages, model_request_parameters)
213
+ if instructions:
214
+ system_parts.insert(0, {"text": instructions})
215
+
216
+ system_instruction = (
217
+ ContentDict(role="user", parts=system_parts) if system_parts else None
218
+ )
219
+
220
+ return system_instruction, contents
221
+
222
+ async def request(
223
+ self,
224
+ messages: list[ModelMessage],
225
+ model_settings: ModelSettings | None,
226
+ model_request_parameters: ModelRequestParameters,
227
+ ) -> ModelResponse:
228
+ """Override request to handle Antigravity envelope and thinking signatures."""
229
+ system_instruction, contents = await self._map_messages(
230
+ messages, model_request_parameters
231
+ )
232
+
233
+ # Build generation config from model settings
234
+ gen_config = self._build_generation_config(model_settings)
235
+
236
+ # Build JSON body
237
+ body: dict[str, Any] = {
238
+ "contents": contents,
239
+ }
240
+ if gen_config:
241
+ body["generationConfig"] = gen_config
242
+ if system_instruction:
243
+ body["systemInstruction"] = system_instruction
244
+
245
+ # Serialize tools
246
+ if model_request_parameters.function_tools:
247
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
248
+
249
+ # Get httpx client
250
+ client = await self._get_client()
251
+ url = f"/models/{self._model_name}:generateContent"
252
+
253
+ # Send request
254
+ response = await client.post(url, json=body)
255
+
256
+ if response.status_code != 200:
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
+ )
285
+
286
+ data = response.json()
287
+
288
+ # Extract candidates
289
+ candidates = data.get("candidates", [])
290
+ if not candidates:
291
+ return ModelResponse(
292
+ parts=[TextPart(content="")],
293
+ model_name=self._model_name,
294
+ usage=RequestUsage(),
295
+ )
296
+
297
+ candidate = candidates[0]
298
+ content = candidate.get("content", {})
299
+ parts = content.get("parts", [])
300
+
301
+ # Extract usage
302
+ usage_meta = data.get("usageMetadata", {})
303
+ usage = RequestUsage(
304
+ input_tokens=usage_meta.get("promptTokenCount", 0),
305
+ output_tokens=usage_meta.get("candidatesTokenCount", 0),
306
+ )
307
+
308
+ return _antigravity_process_response_from_parts(
309
+ parts,
310
+ candidate.get("groundingMetadata"),
311
+ self._model_name,
312
+ self.system,
313
+ usage,
314
+ vendor_id=data.get("requestId"),
315
+ )
316
+
317
+ @asynccontextmanager
318
+ async def request_stream(
319
+ self,
320
+ messages: list[ModelMessage],
321
+ model_settings: ModelSettings | None,
322
+ model_request_parameters: ModelRequestParameters,
323
+ run_context: RunContext[Any] | None = None,
324
+ ) -> AsyncIterator[StreamedResponse]:
325
+ """Override request_stream for streaming with signature handling."""
326
+ system_instruction, contents = await self._map_messages(
327
+ messages, model_request_parameters
328
+ )
329
+
330
+ # Build generation config
331
+ gen_config = self._build_generation_config(model_settings)
332
+
333
+ # Build request body
334
+ body: dict[str, Any] = {"contents": contents}
335
+ if gen_config:
336
+ body["generationConfig"] = gen_config
337
+ if system_instruction:
338
+ body["systemInstruction"] = system_instruction
339
+
340
+ # Add tools
341
+ if model_request_parameters.function_tools:
342
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
343
+
344
+ # Get httpx client
345
+ client = await self._get_client()
346
+ url = f"/models/{self._model_name}:streamGenerateContent?alt=sse"
347
+
348
+ # Create async generator for SSE events
349
+ async def stream_chunks() -> AsyncIterator[dict[str, Any]]:
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
+ )
369
+
370
+ if not should_retry:
371
+ async for line in response.aiter_lines():
372
+ line = line.strip()
373
+ if not line:
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
402
+
403
+ # Create streaming response
404
+ streamed = AntigravityStreamingResponse(
405
+ model_request_parameters=model_request_parameters,
406
+ _chunks=stream_chunks(),
407
+ _model_name_str=self._model_name,
408
+ _provider_name_str=self.system,
409
+ )
410
+ yield streamed
411
+
412
+
413
+ @dataclass
414
+ class AntigravityStreamingResponse(StreamedResponse):
415
+ """Real streaming response that processes SSE chunks as they arrive."""
416
+
417
+ _chunks: AsyncIterator[dict[str, Any]]
418
+ _model_name_str: str
419
+ _provider_name_str: str = "google"
420
+
421
+ @property
422
+ def provider_url(self) -> str | None:
423
+ """Antigravity uses a custom proxy; no standard provider URL."""
424
+ return None
425
+
426
+ _timestamp_val: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
427
+
428
+ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
429
+ """Process streaming chunks and yield events."""
430
+ is_gemini = "gemini" in self._model_name_str.lower()
431
+ pending_signature: str | None = None
432
+
433
+ async for chunk in self._chunks:
434
+ # Extract usage from chunk
435
+ usage_meta = chunk.get("usageMetadata", {})
436
+ if usage_meta:
437
+ self._usage = RequestUsage(
438
+ input_tokens=usage_meta.get("promptTokenCount", 0),
439
+ output_tokens=usage_meta.get("candidatesTokenCount", 0),
440
+ )
441
+
442
+ # Extract response ID
443
+ if chunk.get("responseId"):
444
+ self.provider_response_id = chunk["responseId"]
445
+
446
+ candidates = chunk.get("candidates", [])
447
+ if not candidates:
448
+ continue
449
+
450
+ candidate = candidates[0]
451
+ content = candidate.get("content", {})
452
+ parts = content.get("parts", [])
453
+
454
+ for part in parts:
455
+ # Extract signature
456
+ thought_signature = part.get("thoughtSignature")
457
+ if thought_signature:
458
+ if is_gemini and pending_signature is None:
459
+ pending_signature = thought_signature
460
+
461
+ # Handle thought/thinking part
462
+ if part.get("thought") and part.get("text") is not None:
463
+ text = part["text"]
464
+
465
+ for event in self._parts_manager.handle_thinking_delta(
466
+ vendor_part_id=None,
467
+ content=text,
468
+ ):
469
+ yield event
470
+
471
+ # For Claude: signature is ON the thinking block itself
472
+ if thought_signature and not is_gemini:
473
+ for existing_part in reversed(self._parts_manager._parts):
474
+ if isinstance(existing_part, ThinkingPart):
475
+ object.__setattr__(
476
+ existing_part, "signature", thought_signature
477
+ )
478
+ break
479
+
480
+ # Handle regular text
481
+ elif part.get("text") is not None and not part.get("thought"):
482
+ text = part["text"]
483
+ if len(text) == 0:
484
+ continue
485
+ for event in self._parts_manager.handle_text_delta(
486
+ vendor_part_id=None,
487
+ content=text,
488
+ ):
489
+ yield event
490
+
491
+ # Handle function call
492
+ elif part.get("functionCall"):
493
+ fc = part["functionCall"]
494
+
495
+ # For Gemini: signature on function call belongs to previous thinking
496
+ if is_gemini and thought_signature:
497
+ for existing_part in reversed(self._parts_manager._parts):
498
+ if isinstance(existing_part, ThinkingPart):
499
+ object.__setattr__(
500
+ existing_part, "signature", thought_signature
501
+ )
502
+ break
503
+
504
+ event = self._parts_manager.handle_tool_call_delta(
505
+ vendor_part_id=uuid4(),
506
+ tool_name=fc.get("name"),
507
+ args=fc.get("args"),
508
+ tool_call_id=fc.get("id") or generate_tool_call_id(),
509
+ )
510
+ if event is not None:
511
+ yield event
512
+
513
+ @property
514
+ def model_name(self) -> str:
515
+ return self._model_name_str
516
+
517
+ @property
518
+ def provider_name(self) -> str | None:
519
+ return self._provider_name_str
520
+
521
+ @property
522
+ def timestamp(self) -> datetime:
523
+ return self._timestamp_val
524
+
525
+
526
+ def _antigravity_content_model_response(
527
+ m: ModelResponse, provider_name: str, model_name: str = ""
528
+ ) -> ContentDict | None:
529
+ """Custom serializer for Antigravity that preserves ThinkingPart signatures.
530
+
531
+ Handles different signature protocols:
532
+ - Claude models: signature goes ON the thinking block itself
533
+ - Gemini models: signature goes on the NEXT part after thinking
534
+ """
535
+ parts: list[PartDict] = []
536
+
537
+ is_claude = "claude" in model_name.lower()
538
+ is_gemini = "gemini" in model_name.lower()
539
+
540
+ pending_signature: str | None = None
541
+
542
+ for item in m.parts:
543
+ part: PartDict = {}
544
+
545
+ if isinstance(item, ToolCallPart):
546
+ function_call = FunctionCallDict(
547
+ name=item.tool_name, args=item.args_as_dict(), id=item.tool_call_id
548
+ )
549
+ part["function_call"] = function_call
550
+
551
+ # For Gemini: ALWAYS attach a thoughtSignature to function calls
552
+ if is_gemini:
553
+ part["thoughtSignature"] = (
554
+ pending_signature
555
+ if pending_signature is not None
556
+ else BYPASS_THOUGHT_SIGNATURE
557
+ )
558
+
559
+ elif isinstance(item, TextPart):
560
+ part["text"] = item.content
561
+
562
+ if is_gemini and pending_signature is not None:
563
+ part["thoughtSignature"] = pending_signature
564
+ pending_signature = None
565
+
566
+ elif isinstance(item, ThinkingPart):
567
+ if item.content:
568
+ part["text"] = item.content
569
+ part["thought"] = True
570
+
571
+ # Try to use original signature first. If the API rejects it
572
+ # (Gemini: "Corrupted thought signature", Claude: "thinking.signature: Field required"),
573
+ # we'll backfill with bypass signatures and retry.
574
+ if item.signature:
575
+ if is_claude:
576
+ # Claude expects signature ON the thinking block
577
+ part["thoughtSignature"] = item.signature
578
+ elif is_gemini:
579
+ # Gemini expects signature on the NEXT part
580
+ pending_signature = item.signature
581
+ else:
582
+ part["thoughtSignature"] = item.signature
583
+ elif is_gemini:
584
+ pending_signature = BYPASS_THOUGHT_SIGNATURE
585
+
586
+ elif isinstance(item, BuiltinToolCallPart):
587
+ pass
588
+
589
+ elif isinstance(item, BuiltinToolReturnPart):
590
+ pass
591
+
592
+ elif isinstance(item, FilePart):
593
+ content = item.content
594
+ data_val = content.data
595
+ if isinstance(data_val, bytes):
596
+ data_val = base64.b64encode(data_val).decode("utf-8")
597
+
598
+ inline_data_dict: BlobDict = {
599
+ "data": data_val,
600
+ "mime_type": content.media_type,
601
+ }
602
+ part["inline_data"] = inline_data_dict
603
+ else:
604
+ assert_never(item)
605
+
606
+ if part:
607
+ parts.append(part)
608
+
609
+ if not parts:
610
+ return None
611
+ return ContentDict(role="model", parts=parts)
612
+
613
+
614
+ def _antigravity_process_response_from_parts(
615
+ parts: list[Any],
616
+ grounding_metadata: Any | None,
617
+ model_name: str,
618
+ provider_name: str,
619
+ usage: RequestUsage,
620
+ vendor_id: str | None,
621
+ vendor_details: dict[str, Any] | None = None,
622
+ ) -> ModelResponse:
623
+ """Custom response parser that extracts signatures from ThinkingParts."""
624
+ items: list[ModelResponsePart] = []
625
+
626
+ is_gemini = "gemini" in str(model_name).lower()
627
+
628
+ def get_attr(obj, attr):
629
+ if isinstance(obj, dict):
630
+ return obj.get(attr)
631
+ return getattr(obj, attr, None)
632
+
633
+ # First pass: collect all parts and their signatures
634
+ parsed_parts = []
635
+ for part in parts:
636
+ thought_signature = get_attr(part, "thoughtSignature") or get_attr(
637
+ part, "thought_signature"
638
+ )
639
+
640
+ pd = get_attr(part, "provider_details")
641
+ if not thought_signature and pd:
642
+ thought_signature = pd.get("thought_signature") or pd.get(
643
+ "thoughtSignature"
644
+ )
645
+
646
+ text = get_attr(part, "text")
647
+ thought = get_attr(part, "thought")
648
+ function_call = get_attr(part, "functionCall") or get_attr(
649
+ part, "function_call"
650
+ )
651
+
652
+ parsed_parts.append(
653
+ {
654
+ "text": text,
655
+ "thought": thought,
656
+ "function_call": function_call,
657
+ "signature": thought_signature,
658
+ }
659
+ )
660
+
661
+ # Second pass: for Gemini, associate signatures from next parts with thinking blocks
662
+ if is_gemini:
663
+ for i, pp in enumerate(parsed_parts):
664
+ if pp["thought"] and not pp["signature"]:
665
+ if i + 1 < len(parsed_parts):
666
+ next_sig = parsed_parts[i + 1].get("signature")
667
+ if next_sig:
668
+ pp["signature"] = next_sig
669
+
670
+ # Third pass: create ModelResponsePart objects
671
+ for pp in parsed_parts:
672
+ if pp["text"] is not None:
673
+ if pp["thought"]:
674
+ items.append(
675
+ ThinkingPart(content=pp["text"], signature=pp["signature"])
676
+ )
677
+ else:
678
+ items.append(TextPart(content=pp["text"]))
679
+
680
+ elif pp["function_call"]:
681
+ fc = pp["function_call"]
682
+ fc_name = get_attr(fc, "name")
683
+ fc_args = get_attr(fc, "args")
684
+ fc_id = get_attr(fc, "id") or generate_tool_call_id()
685
+
686
+ items.append(
687
+ ToolCallPart(tool_name=fc_name, args=fc_args, tool_call_id=fc_id)
688
+ )
689
+
690
+ return ModelResponse(
691
+ parts=items,
692
+ model_name=model_name,
693
+ usage=usage,
694
+ provider_response_id=vendor_id,
695
+ provider_details=vendor_details,
696
+ provider_name=provider_name,
697
+ )
698
+
699
+
700
+ def _backfill_thought_signatures(messages: list[ModelMessage]) -> None:
701
+ """Backfill all thinking parts with the bypass signature."""
702
+ for m in messages:
703
+ if isinstance(m, ModelResponse):
704
+ for part in m.parts:
705
+ if isinstance(part, ThinkingPart):
706
+ object.__setattr__(part, "signature", BYPASS_THOUGHT_SIGNATURE)