tau-coding-agent 0.1.0__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 (283) hide show
  1. tau/__init__.py +0 -0
  2. tau/agent/__init__.py +11 -0
  3. tau/agent/prompt/__init__.py +10 -0
  4. tau/agent/prompt/builder.py +302 -0
  5. tau/agent/prompt/types.py +33 -0
  6. tau/agent/service.py +369 -0
  7. tau/agent/types.py +61 -0
  8. tau/auth/manager.py +247 -0
  9. tau/auth/storage.py +82 -0
  10. tau/auth/types.py +41 -0
  11. tau/builtins/__init__.py +4 -0
  12. tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
  13. tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
  14. tau/builtins/commands/__init__.py +41 -0
  15. tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  16. tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  17. tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
  18. tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
  19. tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
  20. tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
  21. tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
  22. tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
  23. tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
  24. tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
  25. tau/builtins/commands/clear.py +16 -0
  26. tau/builtins/commands/compact.py +28 -0
  27. tau/builtins/commands/reload.py +27 -0
  28. tau/builtins/commands/session.py +19 -0
  29. tau/builtins/extensions/footer/__init__.py +76 -0
  30. tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
  31. tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
  32. tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
  33. tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
  34. tau/builtins/extensions/footer/git.py +26 -0
  35. tau/builtins/extensions/footer/model.py +69 -0
  36. tau/builtins/extensions/footer/utils.py +44 -0
  37. tau/builtins/extensions/header/__init__.py +18 -0
  38. tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
  39. tau/builtins/models/__init__.py +0 -0
  40. tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
  41. tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
  42. tau/builtins/models/audio.py +43 -0
  43. tau/builtins/models/image.py +43 -0
  44. tau/builtins/models/text.py +482 -0
  45. tau/builtins/models/video.py +40 -0
  46. tau/builtins/prompts/commit.md +7 -0
  47. tau/builtins/prompts/docs.md +7 -0
  48. tau/builtins/prompts/explain.md +7 -0
  49. tau/builtins/prompts/fix.md +7 -0
  50. tau/builtins/prompts/refactor.md +7 -0
  51. tau/builtins/prompts/review.md +7 -0
  52. tau/builtins/prompts/test.md +7 -0
  53. tau/builtins/providers/__init__.py +0 -0
  54. tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  55. tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
  56. tau/builtins/providers/audio.py +10 -0
  57. tau/builtins/providers/image.py +9 -0
  58. tau/builtins/providers/text.py +33 -0
  59. tau/builtins/providers/video.py +6 -0
  60. tau/builtins/skills/code-review/SKILL.md +4 -0
  61. tau/builtins/skills/debug/SKILL.md +4 -0
  62. tau/builtins/skills/git-commit/SKILL.md +4 -0
  63. tau/builtins/themes/dark.yaml +1 -0
  64. tau/builtins/themes/light.yaml +46 -0
  65. tau/builtins/tools/__init__.py +73 -0
  66. tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  67. tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  68. tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
  69. tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
  70. tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
  71. tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
  72. tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
  73. tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
  74. tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
  75. tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
  76. tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
  77. tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
  78. tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
  79. tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
  80. tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
  81. tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
  82. tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
  83. tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
  84. tau/builtins/tools/edit.py +215 -0
  85. tau/builtins/tools/glob.py +112 -0
  86. tau/builtins/tools/grep.py +146 -0
  87. tau/builtins/tools/ls.py +135 -0
  88. tau/builtins/tools/read.py +122 -0
  89. tau/builtins/tools/terminal.py +150 -0
  90. tau/builtins/tools/write.py +105 -0
  91. tau/commands/__init__.py +10 -0
  92. tau/commands/registry.py +71 -0
  93. tau/commands/types.py +33 -0
  94. tau/console/__init__.py +0 -0
  95. tau/console/cli.py +266 -0
  96. tau/console/commands/__init__.py +0 -0
  97. tau/console/commands/auth.py +193 -0
  98. tau/console/commands/packages.py +104 -0
  99. tau/console/commands/update.py +76 -0
  100. tau/core/__init__.py +0 -0
  101. tau/core/registry.py +102 -0
  102. tau/engine/__init__.py +47 -0
  103. tau/engine/service.py +768 -0
  104. tau/engine/types.py +163 -0
  105. tau/extensions/__init__.py +28 -0
  106. tau/extensions/api.py +928 -0
  107. tau/extensions/context.py +462 -0
  108. tau/extensions/events.py +70 -0
  109. tau/extensions/loader.py +386 -0
  110. tau/extensions/runtime.py +184 -0
  111. tau/extensions/settings.py +137 -0
  112. tau/hooks/__init__.py +112 -0
  113. tau/hooks/engine.py +237 -0
  114. tau/hooks/inference.py +21 -0
  115. tau/hooks/runtime.py +126 -0
  116. tau/hooks/service.py +121 -0
  117. tau/hooks/session.py +117 -0
  118. tau/hooks/tui.py +61 -0
  119. tau/hooks/types.py +72 -0
  120. tau/inference/__init__.py +180 -0
  121. tau/inference/api/__init__.py +0 -0
  122. tau/inference/api/audio/__init__.py +0 -0
  123. tau/inference/api/audio/base.py +29 -0
  124. tau/inference/api/audio/builtins.py +15 -0
  125. tau/inference/api/audio/elevenlabs_audio.py +183 -0
  126. tau/inference/api/audio/gemini_audio.py +95 -0
  127. tau/inference/api/audio/openai_audio.py +159 -0
  128. tau/inference/api/audio/registry.py +15 -0
  129. tau/inference/api/audio/sarvam_audio.py +163 -0
  130. tau/inference/api/audio/service.py +103 -0
  131. tau/inference/api/audio/utils.py +47 -0
  132. tau/inference/api/image/__init__.py +0 -0
  133. tau/inference/api/image/base.py +17 -0
  134. tau/inference/api/image/builtins.py +8 -0
  135. tau/inference/api/image/gemini_image.py +77 -0
  136. tau/inference/api/image/openai_image.py +103 -0
  137. tau/inference/api/image/openrouter.py +144 -0
  138. tau/inference/api/image/registry.py +15 -0
  139. tau/inference/api/image/service.py +71 -0
  140. tau/inference/api/registry.py +82 -0
  141. tau/inference/api/text/__init__.py +0 -0
  142. tau/inference/api/text/anthropic_claude_code.py +222 -0
  143. tau/inference/api/text/anthropic_messages.py +196 -0
  144. tau/inference/api/text/base.py +40 -0
  145. tau/inference/api/text/builtins.py +19 -0
  146. tau/inference/api/text/gemini_generate.py +234 -0
  147. tau/inference/api/text/github_copilot_chat.py +172 -0
  148. tau/inference/api/text/google_antigravity.py +522 -0
  149. tau/inference/api/text/mistral_chat.py +284 -0
  150. tau/inference/api/text/ollama_chat.py +200 -0
  151. tau/inference/api/text/openai_codex_responses.py +497 -0
  152. tau/inference/api/text/openai_completions.py +227 -0
  153. tau/inference/api/text/openai_responses.py +235 -0
  154. tau/inference/api/text/registry.py +50 -0
  155. tau/inference/api/text/service.py +297 -0
  156. tau/inference/api/text/types.py +7 -0
  157. tau/inference/api/text/utils.py +228 -0
  158. tau/inference/api/video/__init__.py +0 -0
  159. tau/inference/api/video/base.py +26 -0
  160. tau/inference/api/video/builtins.py +7 -0
  161. tau/inference/api/video/fal_video.py +119 -0
  162. tau/inference/api/video/openrouter_video.py +142 -0
  163. tau/inference/api/video/registry.py +15 -0
  164. tau/inference/api/video/service.py +72 -0
  165. tau/inference/model/__init__.py +0 -0
  166. tau/inference/model/registry.py +102 -0
  167. tau/inference/model/types.py +65 -0
  168. tau/inference/provider/__init__.py +0 -0
  169. tau/inference/provider/oauth/__init__.py +35 -0
  170. tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
  171. tau/inference/provider/oauth/github_copilot.py +333 -0
  172. tau/inference/provider/oauth/google_antigravity.py +258 -0
  173. tau/inference/provider/oauth/openai_codex.py +309 -0
  174. tau/inference/provider/oauth/pkce.py +14 -0
  175. tau/inference/provider/oauth/types.py +46 -0
  176. tau/inference/provider/oauth/utils.py +154 -0
  177. tau/inference/provider/registry.py +141 -0
  178. tau/inference/provider/types.py +114 -0
  179. tau/inference/types.py +549 -0
  180. tau/inference/utils.py +219 -0
  181. tau/message/__init__.py +0 -0
  182. tau/message/types.py +482 -0
  183. tau/message/utils.py +178 -0
  184. tau/packages/__init__.py +11 -0
  185. tau/packages/manager.py +190 -0
  186. tau/packages/types.py +20 -0
  187. tau/packages/utils.py +67 -0
  188. tau/prompts/expand.py +58 -0
  189. tau/prompts/loader.py +69 -0
  190. tau/prompts/registry.py +45 -0
  191. tau/prompts/types.py +24 -0
  192. tau/rpc/__init__.py +8 -0
  193. tau/rpc/mode.py +783 -0
  194. tau/rpc/types.py +252 -0
  195. tau/runtime/service.py +759 -0
  196. tau/runtime/types.py +303 -0
  197. tau/session/branch_summarization.py +312 -0
  198. tau/session/compaction.py +646 -0
  199. tau/session/manager.py +652 -0
  200. tau/session/types.py +188 -0
  201. tau/session/utils.py +233 -0
  202. tau/settings/manager.py +1077 -0
  203. tau/settings/paths.py +150 -0
  204. tau/settings/storage.py +63 -0
  205. tau/settings/types.py +173 -0
  206. tau/settings/utils.py +25 -0
  207. tau/skills/loader.py +91 -0
  208. tau/skills/registry.py +70 -0
  209. tau/skills/types.py +25 -0
  210. tau/themes/loader.py +238 -0
  211. tau/themes/registry.py +108 -0
  212. tau/themes/types.py +19 -0
  213. tau/tool/__init__.py +3 -0
  214. tau/tool/registry.py +117 -0
  215. tau/tool/render.py +21 -0
  216. tau/tool/types.py +244 -0
  217. tau/trust/__init__.py +13 -0
  218. tau/trust/manager.py +80 -0
  219. tau/trust/types.py +14 -0
  220. tau/trust/utils.py +72 -0
  221. tau/tui/__init__.py +54 -0
  222. tau/tui/agent_hooks.py +346 -0
  223. tau/tui/ansi.py +330 -0
  224. tau/tui/app.py +540 -0
  225. tau/tui/autocomplete.py +33 -0
  226. tau/tui/capabilities.py +119 -0
  227. tau/tui/commands/__init__.py +3 -0
  228. tau/tui/commands/appearance.py +498 -0
  229. tau/tui/commands/auth.py +232 -0
  230. tau/tui/commands/context.py +38 -0
  231. tau/tui/commands/misc.py +82 -0
  232. tau/tui/commands/model.py +118 -0
  233. tau/tui/commands/session.py +464 -0
  234. tau/tui/component.py +268 -0
  235. tau/tui/components/__init__.py +0 -0
  236. tau/tui/components/autocomplete_manager.py +267 -0
  237. tau/tui/components/autocomplete_picker.py +143 -0
  238. tau/tui/components/box.py +90 -0
  239. tau/tui/components/command_palette.py +144 -0
  240. tau/tui/components/dynamic_border.py +19 -0
  241. tau/tui/components/file_picker.py +233 -0
  242. tau/tui/components/image.py +181 -0
  243. tau/tui/components/inline_selector.py +71 -0
  244. tau/tui/components/layout.py +1194 -0
  245. tau/tui/components/message_list.py +692 -0
  246. tau/tui/components/modal.py +97 -0
  247. tau/tui/components/model_palette.py +204 -0
  248. tau/tui/components/picker_overlay.py +174 -0
  249. tau/tui/components/prompt_overlay.py +236 -0
  250. tau/tui/components/resume_modal.py +372 -0
  251. tau/tui/components/select_list.py +222 -0
  252. tau/tui/components/settings_modal.py +274 -0
  253. tau/tui/components/settings_schema.py +203 -0
  254. tau/tui/components/spinner.py +119 -0
  255. tau/tui/components/text_input.py +396 -0
  256. tau/tui/components/text_prompt.py +82 -0
  257. tau/tui/components/tree_select_list.py +580 -0
  258. tau/tui/components/trust_screen.py +97 -0
  259. tau/tui/diff.py +114 -0
  260. tau/tui/fuzzy.py +99 -0
  261. tau/tui/input.py +496 -0
  262. tau/tui/input_handler.py +716 -0
  263. tau/tui/keybindings.py +87 -0
  264. tau/tui/markdown.py +286 -0
  265. tau/tui/message_renderers.py +31 -0
  266. tau/tui/overlay.py +326 -0
  267. tau/tui/renderer.py +378 -0
  268. tau/tui/terminal.py +499 -0
  269. tau/tui/theme.py +148 -0
  270. tau/tui/tui.py +544 -0
  271. tau/tui/ui_context.py +768 -0
  272. tau/tui/utils.py +20 -0
  273. tau/utils/__init__.py +0 -0
  274. tau/utils/http_proxy.py +221 -0
  275. tau/utils/image_processing.py +172 -0
  276. tau/utils/secrets.py +59 -0
  277. tau/utils/version_check.py +60 -0
  278. tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
  279. tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
  280. tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
  281. tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  282. tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  283. tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,522 @@
1
+ """
2
+ Google Antigravity API — SSE streaming via cloudcode-pa.googleapis.com.
3
+
4
+ Uses a Bearer token from Google OAuth to access Claude and Gemini models
5
+ through Google's Antigravity IDE quota.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ from tau.inference.api.text.utils import parse_tool_args
12
+ import os
13
+ from collections.abc import AsyncGenerator, AsyncIterator
14
+ from typing import Any, Optional, TYPE_CHECKING
15
+
16
+ import httpx
17
+
18
+ from tau.inference.api.text.base import BaseLLMAPI as BaseAPI
19
+ from tau.inference.api.text.types import APIResponse
20
+ from tau.inference.model.types import Model
21
+ from tau.inference.types import (
22
+ LLMContext, LLMEvent, LLMOptions, StopReason,
23
+ StartEvent, EndEvent, ErrorEvent,
24
+ TextStartEvent, TextDeltaEvent, TextEndEvent,
25
+ ThinkingStartEvent, ThinkingDeltaEvent, ThinkingEndEvent,
26
+ ToolCallStartEvent, ToolCallDeltaEvent, ToolCallEndEvent,
27
+ normalize_structured_response_format,
28
+ )
29
+ from tau.message.types import (
30
+ SystemMessage, UserMessage, AssistantMessage, ToolMessage,
31
+ TextContent, ImageContent, ThinkingContent, ToolCallContent, ToolResultContent,
32
+ )
33
+ if TYPE_CHECKING:
34
+ from tau.tool.types import Tool
35
+
36
+ __all__ = ["GoogleAntigravityAPI"]
37
+
38
+ _SKIP_KEYS = {"title", "$schema", "$defs", "default", "prefixItems", "maxItems", "minItems", "exclusiveMinimum", "exclusiveMaximum"}
39
+
40
+
41
+ def _resolve_schema(schema: dict[str, Any]) -> dict[str, Any]:
42
+ """Flatten Pydantic JSON schema for Gemini: resolve $ref/$defs, drop unsupported keys."""
43
+ defs = schema.get("$defs", {})
44
+
45
+ def _resolve(obj: Any) -> Any:
46
+ if not isinstance(obj, dict):
47
+ return obj if not isinstance(obj, list) else [_resolve(i) for i in obj]
48
+ if "$ref" in obj:
49
+ ref_name = obj["$ref"].rsplit("/", 1)[-1]
50
+ return _resolve(defs.get(ref_name, {}))
51
+ result: dict[str, Any] = {}
52
+ for k, v in obj.items():
53
+ if k in _SKIP_KEYS:
54
+ continue
55
+ if k == "anyOf" and isinstance(v, list):
56
+ non_null = [_resolve(s) for s in v if s != {"type": "null"}]
57
+ if len(non_null) == 1:
58
+ result.update(non_null[0])
59
+ else:
60
+ result[k] = non_null
61
+ elif isinstance(v, dict):
62
+ result[k] = _resolve(v)
63
+ elif isinstance(v, list):
64
+ result[k] = [_resolve(i) for i in v]
65
+ else:
66
+ result[k] = v
67
+
68
+ # Gemini requires `items` on every array type.
69
+ # Pydantic encodes tuple[int, int] as {type: array, prefixItems: [...]}
70
+ # which we drop above — fill in a generic integer items fallback.
71
+ if result.get("type") == "array" and "items" not in result:
72
+ prefix = obj.get("prefixItems")
73
+ if prefix and isinstance(prefix, list) and len(prefix) > 0:
74
+ # Use the type of the first element (all tuple coords are the same type)
75
+ result["items"] = _resolve(prefix[0])
76
+ else:
77
+ result["items"] = {"type": "string"}
78
+
79
+ return result
80
+
81
+ return _resolve(schema)
82
+
83
+ _DEFAULT_BASE_URL = "https://cloudcode-pa.googleapis.com"
84
+ _STREAM_PATH = "/v1internal:streamGenerateContent?alt=sse"
85
+ _LOAD_CODE_ASSIST_PATH = "/v1internal:loadCodeAssist"
86
+ _ONBOARD_USER_PATH = "/v1internal:onboardUser"
87
+ _ANTIGRAVITY_VERSION = "1.26.0"
88
+ _FALLBACK_PROJECT_ID = "rising-fact-p41fc"
89
+
90
+ _STOP_REASON: dict[str, StopReason] = {
91
+ "STOP": StopReason.Stop,
92
+ "MAX_TOKENS": StopReason.Length,
93
+ "SAFETY": StopReason.ContentFilter,
94
+ "RECITATION": StopReason.ContentFilter,
95
+ }
96
+
97
+
98
+ def _antigravity_headers(access_token: str) -> dict[str, str]:
99
+ platform = "WINDOWS" if os.name == "nt" else "MACOS"
100
+ arch = "windows/amd64" if os.name == "nt" else "darwin/arm64"
101
+ return {
102
+ "Authorization": f"Bearer {access_token}",
103
+ "Content-Type": "application/json",
104
+ "User-Agent": f"antigravity/{_ANTIGRAVITY_VERSION} {arch}",
105
+ "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
106
+ "Client-Metadata": json.dumps(
107
+ {"ideType": "ANTIGRAVITY", "platform": platform, "pluginType": "GEMINI"}
108
+ ),
109
+ "accept": "text/event-stream",
110
+ }
111
+
112
+
113
+ _METADATA = {
114
+ "ideType": "ANTIGRAVITY",
115
+ "platform": "PLATFORM_UNSPECIFIED",
116
+ "pluginType": "GEMINI",
117
+ }
118
+
119
+
120
+ def _extract_managed_project(payload: dict[str, Any]) -> Optional[str]:
121
+ project = payload.get("cloudaicompanionProject")
122
+ if isinstance(project, str) and project:
123
+ return project
124
+ if isinstance(project, dict) and isinstance(project.get("id"), str):
125
+ return project["id"]
126
+ return None
127
+
128
+
129
+ def _pick_default_tier(allowed: list[dict[str, Any]]) -> Optional[str]:
130
+ for tier in allowed:
131
+ if tier.get("isDefault") and tier.get("id"):
132
+ return tier["id"]
133
+ return allowed[0].get("id") if allowed else None
134
+
135
+
136
+ async def resolve_project_id(access_token: str, base_url: str = _DEFAULT_BASE_URL) -> str:
137
+ """Resolve the user's managed Cloud Code Assist project ID.
138
+
139
+ Calls loadCodeAssist; if no project is returned, calls onboardUser and
140
+ polls the returned Long Running Operation until done (up to 10×5s).
141
+ """
142
+ headers = _antigravity_headers(access_token)
143
+
144
+ async with httpx.AsyncClient(timeout=30.0) as client:
145
+ load_payload: dict[str, Any] = {}
146
+ try:
147
+ r = await client.post(
148
+ f"{base_url}{_LOAD_CODE_ASSIST_PATH}",
149
+ json={"metadata": _METADATA},
150
+ headers=headers,
151
+ )
152
+ if r.status_code == 200:
153
+ load_payload = r.json()
154
+ except Exception:
155
+ pass
156
+
157
+ existing = _extract_managed_project(load_payload)
158
+ if existing:
159
+ return existing
160
+
161
+ tier_id = _pick_default_tier(load_payload.get("allowedTiers") or []) or "free-tier"
162
+ body = {"tierId": tier_id, "metadata": _METADATA}
163
+
164
+ for _ in range(10):
165
+ try:
166
+ r = await client.post(
167
+ f"{base_url}{_ONBOARD_USER_PATH}",
168
+ json=body,
169
+ headers=headers,
170
+ )
171
+ if r.status_code != 200:
172
+ break
173
+ payload = r.json()
174
+ if payload.get("done"):
175
+ resp = payload.get("response") or {}
176
+ project = resp.get("cloudaicompanionProject") or {}
177
+ proj_id = project.get("id") if isinstance(project, dict) else None
178
+ if proj_id:
179
+ return proj_id
180
+ break
181
+ except Exception:
182
+ break
183
+ await asyncio.sleep(5)
184
+
185
+ return _FALLBACK_PROJECT_ID
186
+
187
+
188
+ def _messages_to_contents(
189
+ messages: list[LLMMessage],
190
+ ) -> tuple[str | None, list[dict[str, Any]]]:
191
+ system: str | None = None
192
+ raw: list[dict[str, Any]] = []
193
+
194
+ for msg in messages:
195
+ match msg:
196
+ case SystemMessage():
197
+ system = "\n".join(c.content for c in msg.contents if isinstance(c, TextContent))
198
+ case UserMessage():
199
+ parts: list[dict[str, Any]] = []
200
+ for item in msg.contents:
201
+ match item:
202
+ case TextContent():
203
+ parts.append({"text": item.content})
204
+ case ImageContent():
205
+ for b64, mime in item.to_base64():
206
+ parts.append({"inlineData": {"mimeType": mime or "image/png", "data": b64}})
207
+ if parts:
208
+ raw.append({"role": "user", "parts": parts})
209
+ case AssistantMessage():
210
+ parts = []
211
+ for item in msg.contents:
212
+ match item:
213
+ case TextContent():
214
+ parts.append({"text": item.content})
215
+ case ThinkingContent():
216
+ tp: dict[str, Any] = {"thought": True, "text": item.content}
217
+ if item.signature:
218
+ tp["thoughtSignature"] = item.signature
219
+ parts.append(tp)
220
+ case ToolCallContent():
221
+ fc_entry: dict[str, Any] = {
222
+ "functionCall": {
223
+ "name": item.name,
224
+ "args": item.args if isinstance(item.args, dict) else {},
225
+ }
226
+ }
227
+ sig = item.metadata.get("thoughtSignature") if item.metadata else None
228
+ if sig:
229
+ fc_entry["thoughtSignature"] = sig
230
+ parts.append(fc_entry)
231
+ if parts:
232
+ raw.append({"role": "model", "parts": parts})
233
+ case ToolMessage():
234
+ parts = []
235
+ for content in msg.contents:
236
+ if isinstance(content, ToolResultContent):
237
+ parts.append({
238
+ "functionResponse": {
239
+ "name": content.id,
240
+ "response": {"result": content.content}
241
+ }
242
+ })
243
+ if parts:
244
+ raw.append({"role": "user", "parts": parts})
245
+
246
+ # Merge consecutive same-role turns (Gemini requires strict alternation)
247
+ contents: list[dict[str, Any]] = []
248
+ for item in raw:
249
+ if contents and contents[-1]["role"] == item["role"]:
250
+ contents[-1]["parts"] = contents[-1]["parts"] + item["parts"]
251
+ else:
252
+ contents.append({"role": item["role"], "parts": list(item["parts"])})
253
+
254
+ return system, contents
255
+
256
+
257
+ def _response_schema(response_format: Any | None) -> dict[str, Any] | None:
258
+ structured = normalize_structured_response_format(response_format)
259
+ return structured.schema if structured is not None else None
260
+
261
+
262
+ _PROVIDER_ID = "google-antigravity"
263
+ _EXTRA_PROJECT_KEY = "project_id"
264
+
265
+
266
+ def _load_cached_project_id() -> Optional[str]:
267
+ try:
268
+ from tau.inference.api.text.service import LLM
269
+ from tau.auth.types import OAuthCredential as _Cred
270
+ cred = LLM._auth_store.get(_PROVIDER_ID)
271
+ if isinstance(cred, _Cred):
272
+ return cred.extra.get(_EXTRA_PROJECT_KEY) or None
273
+ except Exception:
274
+ pass
275
+ return None
276
+
277
+
278
+ def _persist_project_id(project_id: str) -> None:
279
+ try:
280
+ from tau.inference.api.text.service import LLM
281
+ from tau.auth.types import OAuthCredential as _Cred
282
+ cred = LLM._auth_store.get(_PROVIDER_ID)
283
+ if isinstance(cred, _Cred) and cred.extra.get(_EXTRA_PROJECT_KEY) != project_id:
284
+ cred.extra[_EXTRA_PROJECT_KEY] = project_id
285
+ LLM._auth_store.set(_PROVIDER_ID, cred)
286
+ except Exception:
287
+ pass
288
+
289
+
290
+ class GoogleAntigravityAPI(BaseAPI):
291
+ def __init__(self, options: LLMOptions) -> None:
292
+ super().__init__(options)
293
+ self._base_url = (options.base_url or _DEFAULT_BASE_URL).rstrip("/")
294
+ self._project_id: Optional[str] = (
295
+ (options.headers or {}).get("x-goog-user-project")
296
+ or _load_cached_project_id()
297
+ )
298
+
299
+ async def _ensure_project_id(self) -> str:
300
+ if not self._project_id:
301
+ resolved = await resolve_project_id(
302
+ self.options.api_key or "", self._base_url
303
+ )
304
+ self._project_id = resolved
305
+ if resolved and resolved != _FALLBACK_PROJECT_ID:
306
+ _persist_project_id(resolved)
307
+ return self._project_id
308
+
309
+ @staticmethod
310
+ def _tools_to_declarations(tools: list[Tool]) -> list[dict[str, Any]]:
311
+ """Convert Tool objects to Gemini functionDeclarations format."""
312
+ seen: set[str] = set()
313
+ decls = []
314
+ for t in tools:
315
+ if t.name in seen:
316
+ continue
317
+ seen.add(t.name)
318
+ raw = t.schema.model_json_schema() if t.schema else {}
319
+ schema = _resolve_schema(raw)
320
+ decls.append({"name": t.name, "description": t.description or "", "parameters": schema})
321
+ return decls
322
+
323
+ def _build_request_body(
324
+ self,
325
+ model: Model,
326
+ project: str,
327
+ system: str | None,
328
+ contents: list[dict[str, Any]],
329
+ tools: Optional[list[Tool]] = None,
330
+ response_format: Any | None = None,
331
+ ) -> dict[str, Any]:
332
+ generation_config: dict[str, Any] = {}
333
+ if self.options.temperature is not None:
334
+ generation_config["temperature"] = self.options.temperature
335
+ if self.options.max_tokens is not None:
336
+ generation_config["maxOutputTokens"] = self.options.max_tokens
337
+ schema = _response_schema(response_format)
338
+ if schema is not None:
339
+ generation_config["responseMimeType"] = "application/json"
340
+ generation_config["responseSchema"] = schema
341
+ if self.options.thinking_level is not None:
342
+ from tau.inference.types import ThinkingBudgets, ThinkingLevel as _TL
343
+ if self.options.thinking_level == _TL.Off:
344
+ generation_config["thinkingConfig"] = {"thinkingBudget": 0}
345
+ else:
346
+ budgets = self.options.thinking_budgets or ThinkingBudgets()
347
+ generation_config["thinkingConfig"] = {
348
+ "thinkingBudget": budgets.get(self.options.thinking_level),
349
+ "includeThoughts": True,
350
+ }
351
+
352
+ inner: dict[str, Any] = {"contents": contents}
353
+ if system:
354
+ inner["systemInstruction"] = {"parts": [{"text": system}]}
355
+ if generation_config:
356
+ inner["generationConfig"] = generation_config
357
+ if tools:
358
+ decls = self._tools_to_declarations(tools)
359
+ if decls:
360
+ inner["tools"] = [{"functionDeclarations": decls}]
361
+
362
+ return {"model": model.id, "project": project, "request": inner}
363
+
364
+ async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
365
+ project = await self._ensure_project_id()
366
+ system, contents = _messages_to_contents(context.messages)
367
+ if context.system_prompt:
368
+ system = context.system_prompt
369
+ body = self._build_request_body(
370
+ model,
371
+ project,
372
+ system,
373
+ contents,
374
+ tools=context.tools or None,
375
+ response_format=context.response_format,
376
+ )
377
+ headers = _antigravity_headers(self.options.api_key or "")
378
+
379
+ if self.options.on_payload:
380
+ modified = self.options.on_payload(body)
381
+ if modified is not None:
382
+ body = modified
383
+
384
+ text_index = 0
385
+ thinking_index = 0
386
+ tool_index = 0
387
+ text_started = False
388
+ thinking_started = False
389
+ text_buf = ""
390
+ thinking_buf = ""
391
+ thinking_sig = ""
392
+
393
+ yield StartEvent()
394
+
395
+ done = False
396
+ try:
397
+ async with httpx.AsyncClient(timeout=self.options.timeout.total_seconds()) as client:
398
+ async with client.stream(
399
+ "POST",
400
+ f"{self._base_url}{_STREAM_PATH}",
401
+ headers=headers,
402
+ content=json.dumps(body),
403
+ ) as response:
404
+ if self.options.on_response:
405
+ self.options.on_response(APIResponse(response.status_code, dict(response.headers)))
406
+
407
+ if not response.is_success:
408
+ error_body = (await response.aread()).decode(errors="replace")
409
+ yield ErrorEvent(reason=StopReason.Abort, error=f"HTTP {response.status_code}: {error_body}")
410
+ return
411
+
412
+ # Use aiter_bytes() directly rather than the aiter_lines →
413
+ # aiter_text → aiter_bytes wrapper chain: one iterator with
414
+ # a single explicit finally:aclose() is easier to tear down
415
+ # deterministically than several nested ones. Deterministic
416
+ # cleanup of the whole generator chain on cancellation is
417
+ # handled by the aclosing() wrappers at the consuming sites
418
+ # (engine/service.py, inference/api/text/service.py); without
419
+ # those, an early exit leaves this generator suspended inside
420
+ # the httpx context managers and defers teardown to the GC
421
+ # asyncgen finalizer ("Task was destroyed but it is pending!").
422
+ _bytes = response.aiter_bytes()
423
+ line_buf = ""
424
+ try:
425
+ async for chunk in _bytes:
426
+ if self._cancelled():
427
+ yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
428
+ done = True
429
+ break
430
+ line_buf += chunk.decode("utf-8", errors="replace")
431
+ while "\n" in line_buf:
432
+ line, line_buf = line_buf.split("\n", 1)
433
+ line = line.rstrip("\r")
434
+ if not line.startswith("data: "):
435
+ continue
436
+ raw = line[6:].strip()
437
+ if not raw or raw == "[DONE]":
438
+ continue
439
+
440
+ try:
441
+ chunk_data = json.loads(raw)
442
+ except json.JSONDecodeError:
443
+ continue
444
+
445
+ # API wraps the response in a "response" key
446
+ if "response" in chunk_data:
447
+ chunk_data = chunk_data["response"]
448
+
449
+ candidates = chunk_data.get("candidates", [])
450
+ if not candidates:
451
+ continue
452
+
453
+ candidate = candidates[0]
454
+ content = candidate.get("content", {})
455
+ for part in content.get("parts", []):
456
+ if part.get("thought") and part.get("text"):
457
+ if not thinking_started:
458
+ yield ThinkingStartEvent(thinking=None)
459
+ thinking_started = True
460
+ delta = part["text"]
461
+ thinking_buf += delta
462
+ if part.get("thoughtSignature"):
463
+ thinking_sig = part["thoughtSignature"]
464
+ yield ThinkingDeltaEvent(thinking=ThinkingContent(content=delta))
465
+ elif part.get("thought") and part.get("thoughtSignature") and not part.get("text"):
466
+ # Gemini may send a signature-only thought part
467
+ thinking_sig = part["thoughtSignature"]
468
+ elif part.get("text"):
469
+ if thinking_started:
470
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf, signature=thinking_sig))
471
+ thinking_started = False
472
+ thinking_index += 1
473
+ thinking_buf = ""
474
+ thinking_sig = ""
475
+ if not text_started:
476
+ yield TextStartEvent(text=TextContent(content=""))
477
+ text_started = True
478
+ delta = part["text"]
479
+ text_buf += delta
480
+ yield TextDeltaEvent(text=TextContent(content=delta))
481
+ elif part.get("functionCall"):
482
+ fc = part["functionCall"]
483
+ name = fc.get("name", "") or fc.get("id", "")
484
+ args_raw = fc.get("args", {})
485
+ args = parse_tool_args(args_raw)
486
+ call_meta: dict[str, Any] = {}
487
+ part_sig = part.get("thoughtSignature")
488
+ if part_sig:
489
+ call_meta["thoughtSignature"] = part_sig
490
+ call_id = fc.get("id") or name
491
+ yield ToolCallStartEvent(tool_call=ToolCallContent(id=call_id, name=name))
492
+ yield ToolCallDeltaEvent(tool_call=ToolCallContent(id=call_id))
493
+ yield ToolCallEndEvent(tool_call=ToolCallContent(id=call_id, name=name, args=args, metadata=call_meta))
494
+ tool_index += 1
495
+
496
+ finish_reason = candidate.get("finishReason", "")
497
+ if finish_reason and finish_reason not in ("", "FINISH_REASON_UNSPECIFIED"):
498
+ if thinking_started:
499
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf, signature=thinking_sig))
500
+ thinking_index += 1
501
+ if text_started:
502
+ yield TextEndEvent(text=TextContent(content=text_buf))
503
+ text_index += 1
504
+ stop = StopReason.ToolCalls if tool_index > 0 else _STOP_REASON.get(finish_reason, StopReason.Stop)
505
+ yield EndEvent(reason=stop)
506
+ done = True
507
+ break
508
+ if done:
509
+ break
510
+ finally:
511
+ await _bytes.aclose()
512
+
513
+ except Exception as exc:
514
+ yield ErrorEvent(reason=StopReason.Abort, error=str(exc))
515
+ return
516
+
517
+ if not done:
518
+ if thinking_started:
519
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf, signature=thinking_sig))
520
+ if text_started:
521
+ yield TextEndEvent(text=TextContent(content=text_buf))
522
+ yield EndEvent(reason=StopReason.Stop)