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,754 @@
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
+ # Bypass thought signature for Gemini when no pending signature is available.
44
+ # This allows function calls to work with thinking models.
45
+ BYPASS_THOUGHT_SIGNATURE = "context_engineering_is_the_way_to_go"
46
+
47
+
48
+ def generate_tool_call_id() -> str:
49
+ """Generate a unique tool call ID."""
50
+ return str(uuid.uuid4())
51
+
52
+
53
+ def _flatten_union_to_object_gemini(union_items: list, defs: dict, resolve_fn) -> dict:
54
+ """Flatten a union of object types into a single object with all properties.
55
+
56
+ For discriminated unions like EditFilePayload, we merge all object types
57
+ into one with all properties (Gemini doesn't support anyOf/oneOf).
58
+ """
59
+ import copy as copy_module
60
+
61
+ merged_properties = {}
62
+ has_string_type = False
63
+
64
+ for item in union_items:
65
+ if not isinstance(item, dict):
66
+ continue
67
+
68
+ # Resolve $ref first
69
+ if "$ref" in item:
70
+ ref_path = item["$ref"]
71
+ ref_name = None
72
+ if ref_path.startswith("#/$defs/"):
73
+ ref_name = ref_path[8:]
74
+ elif ref_path.startswith("#/definitions/"):
75
+ ref_name = ref_path[14:]
76
+ if ref_name and ref_name in defs:
77
+ item = copy_module.deepcopy(defs[ref_name])
78
+ else:
79
+ continue
80
+
81
+ if item.get("type") == "string":
82
+ has_string_type = True
83
+ continue
84
+
85
+ if item.get("type") == "null":
86
+ continue
87
+
88
+ if item.get("type") == "object" or "properties" in item:
89
+ props = item.get("properties", {})
90
+ for prop_name, prop_schema in props.items():
91
+ if prop_name not in merged_properties:
92
+ merged_properties[prop_name] = resolve_fn(
93
+ copy_module.deepcopy(prop_schema)
94
+ )
95
+
96
+ if not merged_properties:
97
+ return {"type": "string"} if has_string_type else {"type": "object"}
98
+
99
+ return {
100
+ "type": "object",
101
+ "properties": merged_properties,
102
+ }
103
+
104
+
105
+ def _sanitize_schema_for_gemini(schema: dict) -> dict:
106
+ """Sanitize JSON schema for Gemini API compatibility.
107
+
108
+ Removes/transforms fields that Gemini doesn't support:
109
+ - $defs, definitions, $schema, $id
110
+ - additionalProperties
111
+ - $ref (inlined)
112
+ - anyOf/oneOf/allOf (flattened - Gemini doesn't support unions!)
113
+ - For unions of objects: merges into single object with all properties
114
+ - For simple unions (string | null): picks first non-null type
115
+ """
116
+ import copy
117
+
118
+ if not isinstance(schema, dict):
119
+ return schema
120
+
121
+ # Make a deep copy to avoid modifying original
122
+ schema = copy.deepcopy(schema)
123
+
124
+ # Extract $defs for reference resolution
125
+ defs = schema.pop("$defs", schema.pop("definitions", {}))
126
+
127
+ def resolve_refs(obj):
128
+ """Recursively resolve $ref references and clean schema."""
129
+ if isinstance(obj, dict):
130
+ # Handle anyOf/oneOf unions
131
+ for union_key in ["anyOf", "oneOf"]:
132
+ if union_key in obj:
133
+ union = obj[union_key]
134
+ if isinstance(union, list):
135
+ # Check if this is a complex union of objects
136
+ object_count = 0
137
+ has_refs = False
138
+ for item in union:
139
+ if isinstance(item, dict):
140
+ if "$ref" in item:
141
+ has_refs = True
142
+ object_count += 1
143
+ elif (
144
+ item.get("type") == "object" or "properties" in item
145
+ ):
146
+ object_count += 1
147
+
148
+ # If multiple objects or has refs, flatten to single object
149
+ if object_count > 1 or has_refs:
150
+ flattened = _flatten_union_to_object_gemini(
151
+ union, defs, resolve_refs
152
+ )
153
+ if "description" in obj:
154
+ flattened["description"] = obj["description"]
155
+ return flattened
156
+
157
+ # Simple union - pick first non-null type
158
+ for item in union:
159
+ if isinstance(item, dict) and item.get("type") != "null":
160
+ result = dict(item)
161
+ if "description" in obj:
162
+ result["description"] = obj["description"]
163
+ return resolve_refs(result)
164
+
165
+ # Handle allOf by merging all schemas
166
+ if "allOf" in obj:
167
+ all_of = obj["allOf"]
168
+ if isinstance(all_of, list):
169
+ merged = {}
170
+ merged_properties = {}
171
+ for item in all_of:
172
+ if isinstance(item, dict):
173
+ resolved_item = resolve_refs(item)
174
+ if "properties" in resolved_item:
175
+ merged_properties.update(
176
+ resolved_item.pop("properties")
177
+ )
178
+ merged.update(resolved_item)
179
+ if merged_properties:
180
+ merged["properties"] = merged_properties
181
+ for k, v in obj.items():
182
+ if k != "allOf":
183
+ merged[k] = v
184
+ return resolve_refs(merged)
185
+
186
+ # Check for $ref
187
+ if "$ref" in obj:
188
+ ref_path = obj["$ref"]
189
+ ref_name = None
190
+
191
+ # Parse ref like "#/$defs/SomeType" or "#/definitions/SomeType"
192
+ if ref_path.startswith("#/$defs/"):
193
+ ref_name = ref_path[8:]
194
+ elif ref_path.startswith("#/definitions/"):
195
+ ref_name = ref_path[14:]
196
+
197
+ if ref_name and ref_name in defs:
198
+ resolved = resolve_refs(copy.deepcopy(defs[ref_name]))
199
+ other_props = {k: v for k, v in obj.items() if k != "$ref"}
200
+ if other_props:
201
+ resolved.update(resolve_refs(other_props))
202
+ return resolved
203
+ else:
204
+ return {"type": "object"}
205
+
206
+ # Recursively process and transform
207
+ result = {}
208
+ for key, value in obj.items():
209
+ # Skip unsupported fields
210
+ if key in (
211
+ "$defs",
212
+ "definitions",
213
+ "$schema",
214
+ "$id",
215
+ "additionalProperties",
216
+ "default",
217
+ "examples",
218
+ "const",
219
+ "anyOf", # Skip any remaining union types
220
+ "oneOf",
221
+ "allOf",
222
+ ):
223
+ continue
224
+
225
+ result[key] = resolve_refs(value)
226
+ return result
227
+ elif isinstance(obj, list):
228
+ return [resolve_refs(item) for item in obj]
229
+ else:
230
+ return obj
231
+
232
+ return resolve_refs(schema)
233
+
234
+
235
+ class GeminiModel(Model):
236
+ """Standalone Model implementation for Google's Generative Language API.
237
+
238
+ Uses httpx directly instead of google-genai SDK.
239
+ """
240
+
241
+ def __init__(
242
+ self,
243
+ model_name: str,
244
+ api_key: str,
245
+ base_url: str = "https://generativelanguage.googleapis.com/v1beta",
246
+ http_client: httpx.AsyncClient | None = None,
247
+ ):
248
+ self._model_name = model_name
249
+ self.api_key = api_key
250
+ self._base_url = base_url.rstrip("/")
251
+ self._http_client = http_client
252
+ self._owns_client = http_client is None
253
+
254
+ @property
255
+ def model_name(self) -> str:
256
+ """Return the model name."""
257
+ return self._model_name
258
+
259
+ @property
260
+ def base_url(self) -> str:
261
+ """Return the base URL for the API."""
262
+ return self._base_url
263
+
264
+ @property
265
+ def system(self) -> str:
266
+ """Return the provider system identifier."""
267
+ return "google"
268
+
269
+ def _get_instructions(
270
+ self,
271
+ messages: list,
272
+ model_request_parameters,
273
+ ) -> str | None:
274
+ """Get additional instructions to prepend to system prompt.
275
+
276
+ This is a compatibility method for pydantic-ai interface.
277
+ Override in subclasses to inject custom instructions.
278
+ """
279
+ return None
280
+
281
+ def prepare_request(
282
+ self,
283
+ model_settings: ModelSettings | None,
284
+ model_request_parameters,
285
+ ) -> tuple:
286
+ """Prepare request by normalizing settings.
287
+
288
+ This is a compatibility method for pydantic-ai interface.
289
+ """
290
+ return model_settings, model_request_parameters
291
+
292
+ async def _get_client(self) -> httpx.AsyncClient:
293
+ """Get or create HTTP client."""
294
+ if self._http_client is None:
295
+ self._http_client = httpx.AsyncClient(timeout=180)
296
+ return self._http_client
297
+
298
+ async def _close_client(self) -> None:
299
+ """Close HTTP client if we own it."""
300
+ if self._owns_client and self._http_client is not None:
301
+ await self._http_client.aclose()
302
+ self._http_client = None
303
+
304
+ def _get_headers(self) -> dict[str, str]:
305
+ """Get HTTP headers for the request."""
306
+ return {
307
+ "Content-Type": "application/json",
308
+ "Accept": "application/json",
309
+ "x-goog-api-key": self.api_key,
310
+ }
311
+
312
+ async def _map_user_prompt(self, part: UserPromptPart) -> list[dict[str, Any]]:
313
+ """Map a user prompt part to Gemini format."""
314
+ parts = []
315
+
316
+ if isinstance(part.content, str):
317
+ parts.append({"text": part.content})
318
+ elif isinstance(part.content, list):
319
+ for item in part.content:
320
+ if isinstance(item, str):
321
+ parts.append({"text": item})
322
+ elif hasattr(item, "media_type") and hasattr(item, "data"):
323
+ # Handle file/image content
324
+ data = item.data
325
+ if isinstance(data, bytes):
326
+ data = base64.b64encode(data).decode("utf-8")
327
+ parts.append(
328
+ {
329
+ "inline_data": {
330
+ "mime_type": item.media_type,
331
+ "data": data,
332
+ }
333
+ }
334
+ )
335
+ else:
336
+ parts.append({"text": str(item)})
337
+ else:
338
+ parts.append({"text": str(part.content)})
339
+
340
+ return parts
341
+
342
+ async def _map_messages(
343
+ self,
344
+ messages: list[ModelMessage],
345
+ model_request_parameters: ModelRequestParameters,
346
+ ) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
347
+ """Map pydantic-ai messages to Gemini API format."""
348
+ contents: list[dict[str, Any]] = []
349
+ system_parts: list[dict[str, Any]] = []
350
+
351
+ for m in messages:
352
+ if isinstance(m, ModelRequest):
353
+ message_parts: list[dict[str, Any]] = []
354
+
355
+ for part in m.parts:
356
+ if isinstance(part, SystemPromptPart):
357
+ system_parts.append({"text": part.content})
358
+ elif isinstance(part, UserPromptPart):
359
+ mapped_parts = await self._map_user_prompt(part)
360
+ message_parts.extend(mapped_parts)
361
+ elif isinstance(part, ToolReturnPart):
362
+ message_parts.append(
363
+ {
364
+ "function_response": {
365
+ "name": part.tool_name,
366
+ "response": part.model_response_object(),
367
+ "id": part.tool_call_id,
368
+ }
369
+ }
370
+ )
371
+ elif isinstance(part, RetryPromptPart):
372
+ if part.tool_name is None:
373
+ message_parts.append({"text": part.model_response()})
374
+ else:
375
+ message_parts.append(
376
+ {
377
+ "function_response": {
378
+ "name": part.tool_name,
379
+ "response": {"error": part.model_response()},
380
+ "id": part.tool_call_id,
381
+ }
382
+ }
383
+ )
384
+
385
+ if message_parts:
386
+ # Merge with previous user message if exists
387
+ if contents and contents[-1].get("role") == "user":
388
+ contents[-1]["parts"].extend(message_parts)
389
+ else:
390
+ contents.append({"role": "user", "parts": message_parts})
391
+
392
+ elif isinstance(m, ModelResponse):
393
+ model_parts = self._map_model_response(m)
394
+ if model_parts:
395
+ # Merge with previous model message if exists
396
+ if contents and contents[-1].get("role") == "model":
397
+ contents[-1]["parts"].extend(model_parts["parts"])
398
+ else:
399
+ contents.append(model_parts)
400
+
401
+ # Ensure at least one content
402
+ if not contents:
403
+ contents = [{"role": "user", "parts": [{"text": ""}]}]
404
+
405
+ # Get any injected instructions
406
+ instructions = self._get_instructions(messages, model_request_parameters)
407
+ if instructions:
408
+ system_parts.insert(0, {"text": instructions})
409
+
410
+ # Build system instruction
411
+ system_instruction = None
412
+ if system_parts:
413
+ system_instruction = {"role": "user", "parts": system_parts}
414
+
415
+ return system_instruction, contents
416
+
417
+ def _map_model_response(self, m: ModelResponse) -> dict[str, Any] | None:
418
+ """Map a ModelResponse to Gemini content format.
419
+
420
+ For Gemini thinking models, we need to track thought signatures from
421
+ ThinkingParts and apply them to subsequent function_call parts.
422
+ """
423
+ parts: list[dict[str, Any]] = []
424
+ pending_signature: str | None = None
425
+
426
+ for item in m.parts:
427
+ if isinstance(item, ToolCallPart):
428
+ part_dict: dict[str, Any] = {
429
+ "function_call": {
430
+ "name": item.tool_name,
431
+ "args": item.args_as_dict(),
432
+ "id": item.tool_call_id,
433
+ }
434
+ }
435
+ # Gemini thinking models REQUIRE thoughtSignature on function calls
436
+ # Use pending signature from thinking or bypass signature
437
+ part_dict["thoughtSignature"] = (
438
+ pending_signature
439
+ if pending_signature is not None
440
+ else BYPASS_THOUGHT_SIGNATURE
441
+ )
442
+ parts.append(part_dict)
443
+ elif isinstance(item, TextPart):
444
+ part_dict = {"text": item.content}
445
+ # Apply pending signature to text parts too if present
446
+ if pending_signature is not None:
447
+ part_dict["thoughtSignature"] = pending_signature
448
+ pending_signature = None
449
+ parts.append(part_dict)
450
+ elif isinstance(item, ThinkingPart):
451
+ if item.content:
452
+ part_dict = {"text": item.content, "thought": True}
453
+ if item.signature:
454
+ part_dict["thoughtSignature"] = item.signature
455
+ # Store signature for subsequent parts
456
+ pending_signature = item.signature
457
+ else:
458
+ # No signature on thinking part, use bypass
459
+ pending_signature = BYPASS_THOUGHT_SIGNATURE
460
+ parts.append(part_dict)
461
+
462
+ if not parts:
463
+ return None
464
+ return {"role": "model", "parts": parts}
465
+
466
+ def _build_tools(self, tools: list[ToolDefinition]) -> list[dict[str, Any]]:
467
+ """Build tool definitions for the API."""
468
+ function_declarations = []
469
+
470
+ for tool in tools:
471
+ func_decl: dict[str, Any] = {
472
+ "name": tool.name,
473
+ "description": tool.description or "",
474
+ }
475
+ if tool.parameters_json_schema:
476
+ # Sanitize schema for Gemini compatibility
477
+ func_decl["parameters"] = _sanitize_schema_for_gemini(
478
+ tool.parameters_json_schema
479
+ )
480
+ function_declarations.append(func_decl)
481
+
482
+ return [{"functionDeclarations": function_declarations}]
483
+
484
+ def _build_generation_config(
485
+ self, model_settings: ModelSettings | None
486
+ ) -> dict[str, Any]:
487
+ """Build generation config from model settings."""
488
+ config: dict[str, Any] = {}
489
+
490
+ if model_settings:
491
+ # ModelSettings is a TypedDict, so use .get() for all access
492
+ temperature = model_settings.get("temperature")
493
+ if temperature is not None:
494
+ config["temperature"] = temperature
495
+
496
+ top_p = model_settings.get("top_p")
497
+ if top_p is not None:
498
+ config["topP"] = top_p
499
+
500
+ max_tokens = model_settings.get("max_tokens")
501
+ if max_tokens is not None:
502
+ config["maxOutputTokens"] = max_tokens
503
+
504
+ # Handle Gemini 3 Pro thinking settings
505
+ thinking_enabled = model_settings.get("thinking_enabled")
506
+ thinking_level = model_settings.get("thinking_level")
507
+
508
+ # Build thinkingConfig if thinking settings are present
509
+ if thinking_enabled is False:
510
+ # Disable thinking by not including thinkingConfig
511
+ pass
512
+ elif thinking_level is not None:
513
+ # Gemini 3 Pro uses thinkingLevel with values "low" or "high"
514
+ # includeThoughts=True is required to surface the thinking in the response
515
+ config["thinkingConfig"] = {
516
+ "thinkingLevel": thinking_level,
517
+ "includeThoughts": True,
518
+ }
519
+
520
+ return config
521
+
522
+ async def request(
523
+ self,
524
+ messages: list[ModelMessage],
525
+ model_settings: ModelSettings | None,
526
+ model_request_parameters: ModelRequestParameters,
527
+ ) -> ModelResponse:
528
+ """Make a non-streaming request to the Gemini API."""
529
+ system_instruction, contents = await self._map_messages(
530
+ messages, model_request_parameters
531
+ )
532
+
533
+ # Build request body
534
+ body: dict[str, Any] = {"contents": contents}
535
+
536
+ gen_config = self._build_generation_config(model_settings)
537
+ if gen_config:
538
+ body["generationConfig"] = gen_config
539
+ if system_instruction:
540
+ body["systemInstruction"] = system_instruction
541
+
542
+ # Add tools
543
+ if model_request_parameters.function_tools:
544
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
545
+
546
+ # Make request
547
+ client = await self._get_client()
548
+ url = f"{self._base_url}/models/{self._model_name}:generateContent"
549
+ headers = self._get_headers()
550
+
551
+ response = await client.post(url, json=body, headers=headers)
552
+
553
+ if response.status_code != 200:
554
+ raise RuntimeError(
555
+ f"Gemini API error {response.status_code}: {response.text}"
556
+ )
557
+
558
+ data = response.json()
559
+ return self._parse_response(data)
560
+
561
+ def _parse_response(self, data: dict[str, Any]) -> ModelResponse:
562
+ """Parse the Gemini API response."""
563
+ candidates = data.get("candidates", [])
564
+ if not candidates:
565
+ return ModelResponse(
566
+ parts=[TextPart(content="")],
567
+ model_name=self._model_name,
568
+ usage=RequestUsage(),
569
+ )
570
+
571
+ candidate = candidates[0]
572
+ content = candidate.get("content", {})
573
+ parts = content.get("parts", [])
574
+
575
+ response_parts: list[ModelResponsePart] = []
576
+
577
+ for part in parts:
578
+ if part.get("thought") and part.get("text") is not None:
579
+ # Thinking part
580
+ signature = part.get("thoughtSignature")
581
+ response_parts.append(
582
+ ThinkingPart(content=part["text"], signature=signature)
583
+ )
584
+ elif "text" in part:
585
+ response_parts.append(TextPart(content=part["text"]))
586
+ elif "functionCall" in part:
587
+ fc = part["functionCall"]
588
+ response_parts.append(
589
+ ToolCallPart(
590
+ tool_name=fc["name"],
591
+ args=fc.get("args", {}),
592
+ tool_call_id=fc.get("id") or generate_tool_call_id(),
593
+ )
594
+ )
595
+
596
+ # Extract usage
597
+ usage_meta = data.get("usageMetadata", {})
598
+ usage = RequestUsage(
599
+ input_tokens=usage_meta.get("promptTokenCount", 0),
600
+ output_tokens=usage_meta.get("candidatesTokenCount", 0),
601
+ )
602
+
603
+ return ModelResponse(
604
+ parts=response_parts,
605
+ model_name=self._model_name,
606
+ usage=usage,
607
+ provider_response_id=data.get("requestId"),
608
+ provider_name=self.system,
609
+ )
610
+
611
+ @asynccontextmanager
612
+ async def request_stream(
613
+ self,
614
+ messages: list[ModelMessage],
615
+ model_settings: ModelSettings | None,
616
+ model_request_parameters: ModelRequestParameters,
617
+ run_context: RunContext[Any] | None = None,
618
+ ) -> AsyncIterator[StreamedResponse]:
619
+ """Make a streaming request to the Gemini API."""
620
+ system_instruction, contents = await self._map_messages(
621
+ messages, model_request_parameters
622
+ )
623
+
624
+ # Build request body
625
+ body: dict[str, Any] = {"contents": contents}
626
+
627
+ gen_config = self._build_generation_config(model_settings)
628
+ if gen_config:
629
+ body["generationConfig"] = gen_config
630
+ if system_instruction:
631
+ body["systemInstruction"] = system_instruction
632
+
633
+ # Add tools
634
+ if model_request_parameters.function_tools:
635
+ body["tools"] = self._build_tools(model_request_parameters.function_tools)
636
+
637
+ # Make streaming request
638
+ client = await self._get_client()
639
+ url = (
640
+ f"{self._base_url}/models/{self._model_name}:streamGenerateContent?alt=sse"
641
+ )
642
+ headers = self._get_headers()
643
+
644
+ async def stream_chunks() -> AsyncIterator[dict[str, Any]]:
645
+ async with client.stream(
646
+ "POST", url, json=body, headers=headers
647
+ ) as response:
648
+ if response.status_code != 200:
649
+ text = await response.aread()
650
+ raise RuntimeError(
651
+ f"Gemini API error {response.status_code}: {text.decode()}"
652
+ )
653
+
654
+ async for line in response.aiter_lines():
655
+ line = line.strip()
656
+ if not line:
657
+ continue
658
+ if line.startswith("data: "):
659
+ json_str = line[6:]
660
+ if json_str:
661
+ try:
662
+ yield json.loads(json_str)
663
+ except json.JSONDecodeError:
664
+ continue
665
+
666
+ yield GeminiStreamingResponse(
667
+ model_request_parameters=model_request_parameters,
668
+ _chunks=stream_chunks(),
669
+ _model_name_str=self._model_name,
670
+ _provider_name_str=self.system,
671
+ _provider_url_str=self._base_url,
672
+ )
673
+
674
+
675
+ @dataclass
676
+ class GeminiStreamingResponse(StreamedResponse):
677
+ """Streaming response handler for Gemini API."""
678
+
679
+ _chunks: AsyncIterator[dict[str, Any]]
680
+ _model_name_str: str
681
+ _provider_name_str: str = "google"
682
+ _provider_url_str: str | None = None
683
+ _timestamp_val: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
684
+
685
+ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
686
+ """Process streaming chunks and yield events."""
687
+ async for chunk in self._chunks:
688
+ # Extract usage
689
+ usage_meta = chunk.get("usageMetadata", {})
690
+ if usage_meta:
691
+ self._usage = RequestUsage(
692
+ input_tokens=usage_meta.get("promptTokenCount", 0),
693
+ output_tokens=usage_meta.get("candidatesTokenCount", 0),
694
+ )
695
+
696
+ # Extract response ID
697
+ if chunk.get("responseId"):
698
+ self.provider_response_id = chunk["responseId"]
699
+
700
+ candidates = chunk.get("candidates", [])
701
+ if not candidates:
702
+ continue
703
+
704
+ candidate = candidates[0]
705
+ content = candidate.get("content", {})
706
+ parts = content.get("parts", [])
707
+
708
+ for part in parts:
709
+ # Handle thinking part
710
+ if part.get("thought") and part.get("text") is not None:
711
+ for event in self._parts_manager.handle_thinking_delta(
712
+ vendor_part_id=None,
713
+ content=part["text"],
714
+ ):
715
+ yield event
716
+
717
+ # Handle regular text
718
+ elif part.get("text") is not None and not part.get("thought"):
719
+ text = part["text"]
720
+ if len(text) == 0:
721
+ continue
722
+ for event in self._parts_manager.handle_text_delta(
723
+ vendor_part_id=None,
724
+ content=text,
725
+ ):
726
+ yield event
727
+
728
+ # Handle function call
729
+ elif part.get("functionCall"):
730
+ fc = part["functionCall"]
731
+ event = self._parts_manager.handle_tool_call_delta(
732
+ vendor_part_id=uuid.uuid4(),
733
+ tool_name=fc.get("name"),
734
+ args=fc.get("args"),
735
+ tool_call_id=fc.get("id") or generate_tool_call_id(),
736
+ )
737
+ if event is not None:
738
+ yield event
739
+
740
+ @property
741
+ def model_name(self) -> str:
742
+ return self._model_name_str
743
+
744
+ @property
745
+ def provider_name(self) -> str | None:
746
+ return self._provider_name_str
747
+
748
+ @property
749
+ def provider_url(self) -> str | None:
750
+ return self._provider_url_str
751
+
752
+ @property
753
+ def timestamp(self) -> datetime:
754
+ return self._timestamp_val