code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,602 @@
1
+ """Standalone Gemini Model for pydantic_ai - no google-genai dependency.
2
+
3
+ This module provides a custom Model implementation that uses Google's
4
+ Generative Language API directly via httpx, without the bloated google-genai
5
+ SDK dependency.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import json
12
+ import logging
13
+ import uuid
14
+ from collections.abc import AsyncIterator
15
+ from contextlib import asynccontextmanager
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime, timezone
18
+ from typing import Any
19
+
20
+ import httpx
21
+ from pydantic_ai._run_context import RunContext
22
+ from pydantic_ai.messages import (
23
+ ModelMessage,
24
+ ModelRequest,
25
+ ModelResponse,
26
+ ModelResponsePart,
27
+ ModelResponseStreamEvent,
28
+ RetryPromptPart,
29
+ SystemPromptPart,
30
+ TextPart,
31
+ ThinkingPart,
32
+ ToolCallPart,
33
+ ToolReturnPart,
34
+ UserPromptPart,
35
+ )
36
+ from pydantic_ai.models import Model, ModelRequestParameters, StreamedResponse
37
+ from pydantic_ai.settings import ModelSettings
38
+ from pydantic_ai.tools import ToolDefinition
39
+ from pydantic_ai.usage import RequestUsage
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ def generate_tool_call_id() -> str:
45
+ """Generate a unique tool call ID."""
46
+ return str(uuid.uuid4())
47
+
48
+
49
+ def _sanitize_schema_for_gemini(schema: dict) -> dict:
50
+ """Sanitize JSON schema for Gemini API compatibility.
51
+
52
+ Removes/transforms fields that Gemini doesn't support:
53
+ - $defs, definitions, $schema, $id
54
+ - additionalProperties
55
+ - $ref (inlined)
56
+ - anyOf/oneOf/allOf (converted to any_of/one_of/all_of)
57
+ """
58
+ import copy
59
+
60
+ if not isinstance(schema, dict):
61
+ return schema
62
+
63
+ # Make a deep copy to avoid modifying original
64
+ schema = copy.deepcopy(schema)
65
+
66
+ # Extract $defs for reference resolution
67
+ defs = schema.pop("$defs", schema.pop("definitions", {}))
68
+
69
+ def resolve_refs(obj):
70
+ """Recursively resolve $ref references and clean schema."""
71
+ if isinstance(obj, dict):
72
+ # Check for $ref
73
+ if "$ref" in obj:
74
+ ref_path = obj["$ref"]
75
+ ref_name = None
76
+
77
+ # Parse ref like "#/$defs/SomeType" or "#/definitions/SomeType"
78
+ if ref_path.startswith("#/$defs/"):
79
+ ref_name = ref_path[8:]
80
+ elif ref_path.startswith("#/definitions/"):
81
+ ref_name = ref_path[14:]
82
+
83
+ if ref_name and ref_name in defs:
84
+ resolved = resolve_refs(copy.deepcopy(defs[ref_name]))
85
+ other_props = {k: v for k, v in obj.items() if k != "$ref"}
86
+ if other_props:
87
+ resolved.update(resolve_refs(other_props))
88
+ return resolved
89
+ else:
90
+ return {"type": "object"}
91
+
92
+ # Recursively process and transform
93
+ result = {}
94
+ for key, value in obj.items():
95
+ # Skip unsupported fields
96
+ if key in (
97
+ "$defs",
98
+ "definitions",
99
+ "$schema",
100
+ "$id",
101
+ "additionalProperties",
102
+ "default",
103
+ "examples",
104
+ "const",
105
+ ):
106
+ continue
107
+
108
+ # Transform union types for Gemini
109
+ new_key = key
110
+ if key == "anyOf":
111
+ new_key = "any_of"
112
+ elif key == "oneOf":
113
+ new_key = "one_of"
114
+ elif key == "allOf":
115
+ new_key = "all_of"
116
+
117
+ result[new_key] = resolve_refs(value)
118
+ return result
119
+ elif isinstance(obj, list):
120
+ return [resolve_refs(item) for item in obj]
121
+ else:
122
+ return obj
123
+
124
+ return resolve_refs(schema)
125
+
126
+
127
+ class GeminiModel(Model):
128
+ """Standalone Model implementation for Google's Generative Language API.
129
+
130
+ Uses httpx directly instead of google-genai SDK.
131
+ """
132
+
133
+ def __init__(
134
+ self,
135
+ model_name: str,
136
+ api_key: str,
137
+ base_url: str = "https://generativelanguage.googleapis.com/v1beta",
138
+ http_client: httpx.AsyncClient | None = None,
139
+ ):
140
+ self._model_name = model_name
141
+ self.api_key = api_key
142
+ self._base_url = base_url.rstrip("/")
143
+ self._http_client = http_client
144
+ self._owns_client = http_client is None
145
+
146
+ @property
147
+ def model_name(self) -> str:
148
+ """Return the model name."""
149
+ return self._model_name
150
+
151
+ @property
152
+ def base_url(self) -> str:
153
+ """Return the base URL for the API."""
154
+ return self._base_url
155
+
156
+ @property
157
+ def system(self) -> str:
158
+ """Return the provider system identifier."""
159
+ return "google"
160
+
161
+ def _get_instructions(
162
+ self,
163
+ messages: list,
164
+ model_request_parameters,
165
+ ) -> str | None:
166
+ """Get additional instructions to prepend to system prompt.
167
+
168
+ This is a compatibility method for pydantic-ai interface.
169
+ Override in subclasses to inject custom instructions.
170
+ """
171
+ return None
172
+
173
+ def prepare_request(
174
+ self,
175
+ model_settings: ModelSettings | None,
176
+ model_request_parameters,
177
+ ) -> tuple:
178
+ """Prepare request by normalizing settings.
179
+
180
+ This is a compatibility method for pydantic-ai interface.
181
+ """
182
+ return model_settings, model_request_parameters
183
+
184
+ async def _get_client(self) -> httpx.AsyncClient:
185
+ """Get or create HTTP client."""
186
+ if self._http_client is None:
187
+ self._http_client = httpx.AsyncClient(timeout=180)
188
+ return self._http_client
189
+
190
+ async def _close_client(self) -> None:
191
+ """Close HTTP client if we own it."""
192
+ if self._owns_client and self._http_client is not None:
193
+ await self._http_client.aclose()
194
+ self._http_client = None
195
+
196
+ def _get_headers(self) -> dict[str, str]:
197
+ """Get HTTP headers for the request."""
198
+ return {
199
+ "Content-Type": "application/json",
200
+ "Accept": "application/json",
201
+ }
202
+
203
+ async def _map_user_prompt(self, part: UserPromptPart) -> list[dict[str, Any]]:
204
+ """Map a user prompt part to Gemini format."""
205
+ parts = []
206
+
207
+ if isinstance(part.content, str):
208
+ parts.append({"text": part.content})
209
+ elif isinstance(part.content, list):
210
+ for item in part.content:
211
+ if isinstance(item, str):
212
+ parts.append({"text": item})
213
+ elif hasattr(item, "media_type") and hasattr(item, "data"):
214
+ # Handle file/image content
215
+ data = item.data
216
+ if isinstance(data, bytes):
217
+ data = base64.b64encode(data).decode("utf-8")
218
+ parts.append(
219
+ {
220
+ "inline_data": {
221
+ "mime_type": item.media_type,
222
+ "data": data,
223
+ }
224
+ }
225
+ )
226
+ else:
227
+ parts.append({"text": str(item)})
228
+ else:
229
+ parts.append({"text": str(part.content)})
230
+
231
+ return parts
232
+
233
+ async def _map_messages(
234
+ self,
235
+ messages: list[ModelMessage],
236
+ model_request_parameters: ModelRequestParameters,
237
+ ) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
238
+ """Map pydantic-ai messages to Gemini API format."""
239
+ contents: list[dict[str, Any]] = []
240
+ system_parts: list[dict[str, Any]] = []
241
+
242
+ for m in messages:
243
+ if isinstance(m, ModelRequest):
244
+ message_parts: list[dict[str, Any]] = []
245
+
246
+ for part in m.parts:
247
+ if isinstance(part, SystemPromptPart):
248
+ system_parts.append({"text": part.content})
249
+ elif isinstance(part, UserPromptPart):
250
+ mapped_parts = await self._map_user_prompt(part)
251
+ message_parts.extend(mapped_parts)
252
+ elif isinstance(part, ToolReturnPart):
253
+ message_parts.append(
254
+ {
255
+ "function_response": {
256
+ "name": part.tool_name,
257
+ "response": part.model_response_object(),
258
+ "id": part.tool_call_id,
259
+ }
260
+ }
261
+ )
262
+ elif isinstance(part, RetryPromptPart):
263
+ if part.tool_name is None:
264
+ message_parts.append({"text": part.model_response()})
265
+ else:
266
+ message_parts.append(
267
+ {
268
+ "function_response": {
269
+ "name": part.tool_name,
270
+ "response": {"error": part.model_response()},
271
+ "id": part.tool_call_id,
272
+ }
273
+ }
274
+ )
275
+
276
+ if message_parts:
277
+ # Merge with previous user message if exists
278
+ if contents and contents[-1].get("role") == "user":
279
+ contents[-1]["parts"].extend(message_parts)
280
+ else:
281
+ contents.append({"role": "user", "parts": message_parts})
282
+
283
+ elif isinstance(m, ModelResponse):
284
+ model_parts = self._map_model_response(m)
285
+ if model_parts:
286
+ # Merge with previous model message if exists
287
+ if contents and contents[-1].get("role") == "model":
288
+ contents[-1]["parts"].extend(model_parts["parts"])
289
+ else:
290
+ contents.append(model_parts)
291
+
292
+ # Ensure at least one content
293
+ if not contents:
294
+ contents = [{"role": "user", "parts": [{"text": ""}]}]
295
+
296
+ # Get any injected instructions
297
+ instructions = self._get_instructions(messages, model_request_parameters)
298
+ if instructions:
299
+ system_parts.insert(0, {"text": instructions})
300
+
301
+ # Build system instruction
302
+ system_instruction = None
303
+ if system_parts:
304
+ system_instruction = {"role": "user", "parts": system_parts}
305
+
306
+ return system_instruction, contents
307
+
308
+ def _map_model_response(self, m: ModelResponse) -> dict[str, Any] | None:
309
+ """Map a ModelResponse to Gemini content format."""
310
+ parts: list[dict[str, Any]] = []
311
+
312
+ for item in m.parts:
313
+ if isinstance(item, ToolCallPart):
314
+ parts.append(
315
+ {
316
+ "function_call": {
317
+ "name": item.tool_name,
318
+ "args": item.args_as_dict(),
319
+ "id": item.tool_call_id,
320
+ }
321
+ }
322
+ )
323
+ elif isinstance(item, TextPart):
324
+ parts.append({"text": item.content})
325
+ elif isinstance(item, ThinkingPart):
326
+ if item.content:
327
+ part_dict: dict[str, Any] = {"text": item.content, "thought": True}
328
+ if item.signature:
329
+ part_dict["thoughtSignature"] = item.signature
330
+ parts.append(part_dict)
331
+
332
+ if not parts:
333
+ return None
334
+ return {"role": "model", "parts": parts}
335
+
336
+ def _build_tools(self, tools: list[ToolDefinition]) -> list[dict[str, Any]]:
337
+ """Build tool definitions for the API."""
338
+ function_declarations = []
339
+
340
+ for tool in tools:
341
+ func_decl: dict[str, Any] = {
342
+ "name": tool.name,
343
+ "description": tool.description or "",
344
+ }
345
+ if tool.parameters_json_schema:
346
+ # Sanitize schema for Gemini compatibility
347
+ func_decl["parameters"] = _sanitize_schema_for_gemini(
348
+ tool.parameters_json_schema
349
+ )
350
+ function_declarations.append(func_decl)
351
+
352
+ return [{"functionDeclarations": function_declarations}]
353
+
354
+ def _build_generation_config(
355
+ self, model_settings: ModelSettings | None
356
+ ) -> dict[str, Any]:
357
+ """Build generation config from model settings."""
358
+ config: dict[str, Any] = {}
359
+
360
+ if model_settings:
361
+ if (
362
+ hasattr(model_settings, "temperature")
363
+ and model_settings.temperature is not None
364
+ ):
365
+ config["temperature"] = model_settings.temperature
366
+ if hasattr(model_settings, "top_p") and model_settings.top_p is not None:
367
+ config["topP"] = model_settings.top_p
368
+ if (
369
+ hasattr(model_settings, "max_tokens")
370
+ and model_settings.max_tokens is not None
371
+ ):
372
+ config["maxOutputTokens"] = model_settings.max_tokens
373
+
374
+ return config
375
+
376
+ async def request(
377
+ self,
378
+ messages: list[ModelMessage],
379
+ model_settings: ModelSettings | None,
380
+ model_request_parameters: ModelRequestParameters,
381
+ ) -> ModelResponse:
382
+ """Make a non-streaming request to the Gemini API."""
383
+ system_instruction, contents = await self._map_messages(
384
+ messages, model_request_parameters
385
+ )
386
+
387
+ # Build request body
388
+ body: dict[str, Any] = {"contents": contents}
389
+
390
+ gen_config = self._build_generation_config(model_settings)
391
+ if gen_config:
392
+ body["generationConfig"] = gen_config
393
+ if system_instruction:
394
+ body["systemInstruction"] = system_instruction
395
+
396
+ # Add tools
397
+ if model_request_parameters.function_tools:
398
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
399
+
400
+ # Make request
401
+ client = await self._get_client()
402
+ url = f"{self._base_url}/models/{self._model_name}:generateContent?key={self.api_key}"
403
+ headers = self._get_headers()
404
+
405
+ response = await client.post(url, json=body, headers=headers)
406
+
407
+ if response.status_code != 200:
408
+ raise RuntimeError(
409
+ f"Gemini API error {response.status_code}: {response.text}"
410
+ )
411
+
412
+ data = response.json()
413
+ return self._parse_response(data)
414
+
415
+ def _parse_response(self, data: dict[str, Any]) -> ModelResponse:
416
+ """Parse the Gemini API response."""
417
+ candidates = data.get("candidates", [])
418
+ if not candidates:
419
+ return ModelResponse(
420
+ parts=[TextPart(content="")],
421
+ model_name=self._model_name,
422
+ usage=RequestUsage(),
423
+ )
424
+
425
+ candidate = candidates[0]
426
+ content = candidate.get("content", {})
427
+ parts = content.get("parts", [])
428
+
429
+ response_parts: list[ModelResponsePart] = []
430
+
431
+ for part in parts:
432
+ if part.get("thought") and part.get("text") is not None:
433
+ # Thinking part
434
+ signature = part.get("thoughtSignature")
435
+ response_parts.append(
436
+ ThinkingPart(content=part["text"], signature=signature)
437
+ )
438
+ elif "text" in part:
439
+ response_parts.append(TextPart(content=part["text"]))
440
+ elif "functionCall" in part:
441
+ fc = part["functionCall"]
442
+ response_parts.append(
443
+ ToolCallPart(
444
+ tool_name=fc["name"],
445
+ args=fc.get("args", {}),
446
+ tool_call_id=fc.get("id") or generate_tool_call_id(),
447
+ )
448
+ )
449
+
450
+ # Extract usage
451
+ usage_meta = data.get("usageMetadata", {})
452
+ usage = RequestUsage(
453
+ input_tokens=usage_meta.get("promptTokenCount", 0),
454
+ output_tokens=usage_meta.get("candidatesTokenCount", 0),
455
+ )
456
+
457
+ return ModelResponse(
458
+ parts=response_parts,
459
+ model_name=self._model_name,
460
+ usage=usage,
461
+ provider_response_id=data.get("requestId"),
462
+ provider_name=self.system,
463
+ )
464
+
465
+ @asynccontextmanager
466
+ async def request_stream(
467
+ self,
468
+ messages: list[ModelMessage],
469
+ model_settings: ModelSettings | None,
470
+ model_request_parameters: ModelRequestParameters,
471
+ run_context: RunContext[Any] | None = None,
472
+ ) -> AsyncIterator[StreamedResponse]:
473
+ """Make a streaming request to the Gemini API."""
474
+ system_instruction, contents = await self._map_messages(
475
+ messages, model_request_parameters
476
+ )
477
+
478
+ # Build request body
479
+ body: dict[str, Any] = {"contents": contents}
480
+
481
+ gen_config = self._build_generation_config(model_settings)
482
+ if gen_config:
483
+ body["generationConfig"] = gen_config
484
+ if system_instruction:
485
+ body["systemInstruction"] = system_instruction
486
+
487
+ # Add tools
488
+ if model_request_parameters.function_tools:
489
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
490
+
491
+ # Make streaming request
492
+ client = await self._get_client()
493
+ url = f"{self._base_url}/models/{self._model_name}:streamGenerateContent?alt=sse&key={self.api_key}"
494
+ headers = self._get_headers()
495
+
496
+ async def stream_chunks() -> AsyncIterator[dict[str, Any]]:
497
+ async with client.stream(
498
+ "POST", url, json=body, headers=headers
499
+ ) as response:
500
+ if response.status_code != 200:
501
+ text = await response.aread()
502
+ raise RuntimeError(
503
+ f"Gemini API error {response.status_code}: {text.decode()}"
504
+ )
505
+
506
+ async for line in response.aiter_lines():
507
+ line = line.strip()
508
+ if not line:
509
+ continue
510
+ if line.startswith("data: "):
511
+ json_str = line[6:]
512
+ if json_str:
513
+ try:
514
+ yield json.loads(json_str)
515
+ except json.JSONDecodeError:
516
+ continue
517
+
518
+ yield GeminiStreamingResponse(
519
+ model_request_parameters=model_request_parameters,
520
+ _chunks=stream_chunks(),
521
+ _model_name_str=self._model_name,
522
+ _provider_name_str=self.system,
523
+ )
524
+
525
+
526
+ @dataclass
527
+ class GeminiStreamingResponse(StreamedResponse):
528
+ """Streaming response handler for Gemini API."""
529
+
530
+ _chunks: AsyncIterator[dict[str, Any]]
531
+ _model_name_str: str
532
+ _provider_name_str: str = "google"
533
+ _timestamp_val: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
534
+
535
+ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
536
+ """Process streaming chunks and yield events."""
537
+ async for chunk in self._chunks:
538
+ # Extract usage
539
+ usage_meta = chunk.get("usageMetadata", {})
540
+ if usage_meta:
541
+ self._usage = RequestUsage(
542
+ input_tokens=usage_meta.get("promptTokenCount", 0),
543
+ output_tokens=usage_meta.get("candidatesTokenCount", 0),
544
+ )
545
+
546
+ # Extract response ID
547
+ if chunk.get("responseId"):
548
+ self.provider_response_id = chunk["responseId"]
549
+
550
+ candidates = chunk.get("candidates", [])
551
+ if not candidates:
552
+ continue
553
+
554
+ candidate = candidates[0]
555
+ content = candidate.get("content", {})
556
+ parts = content.get("parts", [])
557
+
558
+ for part in parts:
559
+ # Handle thinking part
560
+ if part.get("thought") and part.get("text") is not None:
561
+ event = self._parts_manager.handle_thinking_delta(
562
+ vendor_part_id=None,
563
+ content=part["text"],
564
+ )
565
+ if event:
566
+ yield event
567
+
568
+ # Handle regular text
569
+ elif part.get("text") is not None and not part.get("thought"):
570
+ text = part["text"]
571
+ if len(text) == 0:
572
+ continue
573
+ event = self._parts_manager.handle_text_delta(
574
+ vendor_part_id=None,
575
+ content=text,
576
+ )
577
+ if event:
578
+ yield event
579
+
580
+ # Handle function call
581
+ elif part.get("functionCall"):
582
+ fc = part["functionCall"]
583
+ event = self._parts_manager.handle_tool_call_delta(
584
+ vendor_part_id=uuid.uuid4(),
585
+ tool_name=fc.get("name"),
586
+ args=fc.get("args"),
587
+ tool_call_id=fc.get("id") or generate_tool_call_id(),
588
+ )
589
+ if event:
590
+ yield event
591
+
592
+ @property
593
+ def model_name(self) -> str:
594
+ return self._model_name_str
595
+
596
+ @property
597
+ def provider_name(self) -> str | None:
598
+ return self._provider_name_str
599
+
600
+ @property
601
+ def timestamp(self) -> datetime:
602
+ return self._timestamp_val