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.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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)
|