my-pi-agent 0.1.0
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.
- package/README.md +318 -0
- package/package.json +45 -0
- package/pyproject.toml +50 -0
- package/src/my_agent_core/__init__.py +123 -0
- package/src/my_agent_core/agent.py +441 -0
- package/src/my_agent_core/background.py +121 -0
- package/src/my_agent_core/context.py +505 -0
- package/src/my_agent_core/events.py +153 -0
- package/src/my_agent_core/extensions/__init__.py +9 -0
- package/src/my_agent_core/extensions/core.py +197 -0
- package/src/my_agent_core/hooks.py +130 -0
- package/src/my_agent_core/loop.py +709 -0
- package/src/my_agent_core/main.py +134 -0
- package/src/my_agent_core/memory.py +241 -0
- package/src/my_agent_core/message_queue.py +110 -0
- package/src/my_agent_core/plugins.py +212 -0
- package/src/my_agent_core/registry.py +186 -0
- package/src/my_agent_core/session/__init__.py +79 -0
- package/src/my_agent_core/session/entries.py +197 -0
- package/src/my_agent_core/session/jsonl.py +60 -0
- package/src/my_agent_core/session/memory.py +137 -0
- package/src/my_agent_core/session/session.py +400 -0
- package/src/my_agent_core/session/storage.py +245 -0
- package/src/my_agent_core/session/store.py +131 -0
- package/src/my_agent_core/session/tree.py +86 -0
- package/src/my_agent_core/skills.py +149 -0
- package/src/my_agent_core/subagent_tasks.py +170 -0
- package/src/my_agent_core/subagents.py +148 -0
- package/src/my_agent_core/task_store.py +248 -0
- package/src/my_agent_core/tool_history.py +189 -0
- package/src/my_agent_core/tools/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/task.py +30 -0
- package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
- package/src/my_agent_core/tools/core.py +239 -0
- package/src/my_agent_llm/__init__.py +45 -0
- package/src/my_agent_llm/auth/__init__.py +46 -0
- package/src/my_agent_llm/auth/antigravity.py +209 -0
- package/src/my_agent_llm/auth/manager.py +259 -0
- package/src/my_agent_llm/auth/quota.py +56 -0
- package/src/my_agent_llm/auth/schema.py +94 -0
- package/src/my_agent_llm/client.py +116 -0
- package/src/my_agent_llm/config.py +17 -0
- package/src/my_agent_llm/events.py +84 -0
- package/src/my_agent_llm/models.py +195 -0
- package/src/my_agent_llm/providers/__init__.py +4 -0
- package/src/my_agent_llm/providers/_base.py +94 -0
- package/src/my_agent_llm/providers/anthropic.py +298 -0
- package/src/my_agent_llm/providers/antigravity.py +480 -0
- package/src/my_agent_llm/providers/deepseek.py +196 -0
- package/src/my_agent_llm/providers/openai.py +364 -0
- package/src/my_agent_llm/providers/registry.py +16 -0
- package/src/my_agent_llm/stream.py +218 -0
- package/src/my_coding_agent/__init__.py +66 -0
- package/src/my_coding_agent/agent.py +208 -0
- package/src/my_coding_agent/cli.py +78 -0
- package/src/my_coding_agent/file_reference.py +80 -0
- package/src/my_coding_agent/macro.py +408 -0
- package/src/my_coding_agent/mcp.py +243 -0
- package/src/my_coding_agent/mutation_queue.py +37 -0
- package/src/my_coding_agent/paths.py +119 -0
- package/src/my_coding_agent/permissions.py +84 -0
- package/src/my_coding_agent/prompt.py +54 -0
- package/src/my_coding_agent/rpc_server.py +2817 -0
- package/src/my_coding_agent/settings.py +126 -0
- package/src/my_coding_agent/tools/__init__.py +55 -0
- package/src/my_coding_agent/tools/base.py +58 -0
- package/src/my_coding_agent/tools/bash.py +206 -0
- package/src/my_coding_agent/tools/edit.py +226 -0
- package/src/my_coding_agent/tools/find.py +118 -0
- package/src/my_coding_agent/tools/grep.py +177 -0
- package/src/my_coding_agent/tools/ls.py +112 -0
- package/src/my_coding_agent/tools/read.py +113 -0
- package/src/my_coding_agent/tools/write.py +72 -0
- package/tui/README.md +27 -0
- package/tui/bin/my-agent.js +98 -0
- package/tui/dist/app.d.ts +41 -0
- package/tui/dist/app.js +110 -0
- package/tui/dist/bridge/event-translator.d.ts +92 -0
- package/tui/dist/bridge/event-translator.js +216 -0
- package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
- package/tui/dist/bridge/kernel-bridge.js +132 -0
- package/tui/dist/client.d.ts +63 -0
- package/tui/dist/client.js +239 -0
- package/tui/dist/components/assistant-message.d.ts +19 -0
- package/tui/dist/components/assistant-message.js +90 -0
- package/tui/dist/components/compaction-summary-message.d.ts +19 -0
- package/tui/dist/components/compaction-summary-message.js +46 -0
- package/tui/dist/components/custom-editor.d.ts +18 -0
- package/tui/dist/components/custom-editor.js +56 -0
- package/tui/dist/components/dynamic-border.d.ts +9 -0
- package/tui/dist/components/dynamic-border.js +14 -0
- package/tui/dist/components/footer.d.ts +39 -0
- package/tui/dist/components/footer.js +199 -0
- package/tui/dist/components/header.d.ts +4 -0
- package/tui/dist/components/header.js +21 -0
- package/tui/dist/components/keys.d.ts +5 -0
- package/tui/dist/components/keys.js +12 -0
- package/tui/dist/components/login-selector.d.ts +26 -0
- package/tui/dist/components/login-selector.js +181 -0
- package/tui/dist/components/logout-selector.d.ts +19 -0
- package/tui/dist/components/logout-selector.js +88 -0
- package/tui/dist/components/model-selector.d.ts +40 -0
- package/tui/dist/components/model-selector.js +268 -0
- package/tui/dist/components/session-selector.d.ts +54 -0
- package/tui/dist/components/session-selector.js +393 -0
- package/tui/dist/components/settings-selector.d.ts +24 -0
- package/tui/dist/components/settings-selector.js +146 -0
- package/tui/dist/components/status-indicator.d.ts +25 -0
- package/tui/dist/components/status-indicator.js +60 -0
- package/tui/dist/components/theme-selector.d.ts +14 -0
- package/tui/dist/components/theme-selector.js +77 -0
- package/tui/dist/components/thinking-selector.d.ts +21 -0
- package/tui/dist/components/thinking-selector.js +128 -0
- package/tui/dist/components/tool-execution.d.ts +31 -0
- package/tui/dist/components/tool-execution.js +206 -0
- package/tui/dist/components/tree-selector.d.ts +40 -0
- package/tui/dist/components/tree-selector.js +173 -0
- package/tui/dist/components/user-message-selector.d.ts +21 -0
- package/tui/dist/components/user-message-selector.js +103 -0
- package/tui/dist/components/user-message.d.ts +5 -0
- package/tui/dist/components/user-message.js +15 -0
- package/tui/dist/index.d.ts +11 -0
- package/tui/dist/index.js +11 -0
- package/tui/dist/interactive/chat-viewport.d.ts +19 -0
- package/tui/dist/interactive/chat-viewport.js +41 -0
- package/tui/dist/interactive/components.d.ts +1 -0
- package/tui/dist/interactive/components.js +1 -0
- package/tui/dist/interactive/interactive-mode.d.ts +89 -0
- package/tui/dist/interactive/interactive-mode.js +1625 -0
- package/tui/dist/interactive/theme.d.ts +1 -0
- package/tui/dist/interactive/theme.js +1 -0
- package/tui/dist/interactive/tui-renderer.d.ts +8 -0
- package/tui/dist/interactive/tui-renderer.js +10 -0
- package/tui/dist/protocol.d.ts +78 -0
- package/tui/dist/protocol.js +1 -0
- package/tui/dist/theme/dark.json +54 -0
- package/tui/dist/theme/light.json +71 -0
- package/tui/dist/theme/theme.d.ts +20 -0
- package/tui/dist/theme/theme.js +86 -0
- package/tui/package.json +25 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""AuthManager: 管理 ~/.my-pi-agent/auth.json 的线程与进程安全凭据中心。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from filelock import FileLock # pyright: ignore[reportMissingImports]
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from my_agent_llm.auth.schema import (
|
|
16
|
+
ApiKeyCredential,
|
|
17
|
+
AuthStore,
|
|
18
|
+
OAuthCredential,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DEFAULT_ANTIGRAVITY_CLIENT_ID",
|
|
23
|
+
"DEFAULT_ANTIGRAVITY_CLIENT_SECRET",
|
|
24
|
+
"GOOGLE_TOKEN_URL",
|
|
25
|
+
"AuthManager",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
DEFAULT_ANTIGRAVITY_CLIENT_ID = os.environ.get(
|
|
29
|
+
"ANTIGRAVITY_CLIENT_ID",
|
|
30
|
+
"my-pi-agent-desktop-client-id",
|
|
31
|
+
)
|
|
32
|
+
DEFAULT_ANTIGRAVITY_CLIENT_SECRET = os.environ.get(
|
|
33
|
+
"ANTIGRAVITY_CLIENT_SECRET",
|
|
34
|
+
"my-pi-agent-desktop-client-secret",
|
|
35
|
+
)
|
|
36
|
+
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AuthManager:
|
|
40
|
+
"""产品级全局凭据管理器。
|
|
41
|
+
|
|
42
|
+
提供线程安全与跨进程文件锁保护的 auth.json 读写、Profile 调度与 OAuth 静默续期。
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
auth_path: Path | str | None = None,
|
|
48
|
+
lock_path: Path | str | None = None,
|
|
49
|
+
lock_timeout: float = 5.0,
|
|
50
|
+
) -> None:
|
|
51
|
+
if auth_path:
|
|
52
|
+
self.auth_path = Path(auth_path).resolve()
|
|
53
|
+
else:
|
|
54
|
+
home = Path(
|
|
55
|
+
os.environ.get("MY_AGENT_HOME")
|
|
56
|
+
or Path(os.environ.get("USERPROFILE") or os.environ.get("HOME") or "~").expanduser() / ".my-pi-agent"
|
|
57
|
+
).resolve()
|
|
58
|
+
self.auth_path = home / "auth.json"
|
|
59
|
+
|
|
60
|
+
if lock_path:
|
|
61
|
+
self.lock_path = Path(lock_path).resolve()
|
|
62
|
+
else:
|
|
63
|
+
self.lock_path = self.auth_path.with_name(f"{self.auth_path.name}.lock")
|
|
64
|
+
self.lock_timeout = lock_timeout
|
|
65
|
+
|
|
66
|
+
def load_store(self) -> AuthStore:
|
|
67
|
+
"""从 auth.json 加载并自动归一化格式。
|
|
68
|
+
|
|
69
|
+
支持标准的 AuthStore 结构以及简写平铺格式。如果文件不存在或损坏,优雅降级为空 AuthStore。
|
|
70
|
+
"""
|
|
71
|
+
if not self.auth_path.exists():
|
|
72
|
+
return AuthStore()
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
content = self.auth_path.read_text(encoding="utf-8")
|
|
76
|
+
data = json.loads(content)
|
|
77
|
+
if not isinstance(data, dict):
|
|
78
|
+
return AuthStore()
|
|
79
|
+
|
|
80
|
+
# 兼容扁平平铺简写格式:{"deepseek": {"type": "api_key", ...}}
|
|
81
|
+
if "providers" not in data:
|
|
82
|
+
normalized_providers: dict[str, Any] = {}
|
|
83
|
+
for prov, item in data.items():
|
|
84
|
+
if isinstance(item, dict) and "type" in item:
|
|
85
|
+
normalized_providers[prov] = {"default": item}
|
|
86
|
+
active_profiles = data.get("active_profiles")
|
|
87
|
+
if not isinstance(active_profiles, dict):
|
|
88
|
+
active_profiles = {}
|
|
89
|
+
version = data.get("version", 1)
|
|
90
|
+
return AuthStore(
|
|
91
|
+
version=version,
|
|
92
|
+
active_profiles=active_profiles,
|
|
93
|
+
providers=normalized_providers,
|
|
94
|
+
)
|
|
95
|
+
return AuthStore.model_validate(data)
|
|
96
|
+
except Exception:
|
|
97
|
+
return AuthStore()
|
|
98
|
+
|
|
99
|
+
def save_store(self, store: AuthStore) -> None:
|
|
100
|
+
"""以 0o600 权限与临时文件原子替换保存凭证。"""
|
|
101
|
+
self.auth_path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
tmp_path = self.auth_path.with_name(f"{self.auth_path.name}.tmp")
|
|
103
|
+
tmp_path.write_text(store.model_dump_json(indent=2), encoding="utf-8")
|
|
104
|
+
with contextlib.suppress(Exception):
|
|
105
|
+
tmp_path.chmod(0o600)
|
|
106
|
+
tmp_path.replace(self.auth_path)
|
|
107
|
+
|
|
108
|
+
def set_api_key(
|
|
109
|
+
self,
|
|
110
|
+
provider: str,
|
|
111
|
+
key: str,
|
|
112
|
+
profile: str = "default",
|
|
113
|
+
base_url: str | None = None,
|
|
114
|
+
env: dict[str, str] | None = None,
|
|
115
|
+
**kwargs: Any,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""安全设置指定提供商的 API Key 凭据。"""
|
|
118
|
+
self.lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
with FileLock(str(self.lock_path), timeout=self.lock_timeout):
|
|
120
|
+
store = self.load_store()
|
|
121
|
+
prov_dict = store.providers.setdefault(provider, {})
|
|
122
|
+
prov_dict[profile] = ApiKeyCredential(
|
|
123
|
+
key=key,
|
|
124
|
+
base_url=base_url,
|
|
125
|
+
env=env or {},
|
|
126
|
+
)
|
|
127
|
+
store.active_profiles.setdefault(provider, profile)
|
|
128
|
+
self.save_store(store)
|
|
129
|
+
|
|
130
|
+
def set_oauth(
|
|
131
|
+
self,
|
|
132
|
+
provider: str,
|
|
133
|
+
access: str,
|
|
134
|
+
refresh: str,
|
|
135
|
+
expires: int,
|
|
136
|
+
profile: str = "default",
|
|
137
|
+
project_id: str = "aicode-consumers",
|
|
138
|
+
email: str | None = None,
|
|
139
|
+
client_id: str | None = None,
|
|
140
|
+
client_secret: str | None = None,
|
|
141
|
+
**kwargs: Any,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""安全设置指定提供商的 OAuth 凭据。"""
|
|
144
|
+
self.lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
with FileLock(str(self.lock_path), timeout=self.lock_timeout):
|
|
146
|
+
store = self.load_store()
|
|
147
|
+
prov_dict = store.providers.setdefault(provider, {})
|
|
148
|
+
prov_dict[profile] = OAuthCredential(
|
|
149
|
+
access=access,
|
|
150
|
+
refresh=refresh,
|
|
151
|
+
expires=expires,
|
|
152
|
+
project_id=project_id,
|
|
153
|
+
email=email,
|
|
154
|
+
client_id=client_id,
|
|
155
|
+
client_secret=client_secret,
|
|
156
|
+
)
|
|
157
|
+
store.active_profiles.setdefault(provider, profile)
|
|
158
|
+
self.save_store(store)
|
|
159
|
+
|
|
160
|
+
def get_credential(self, provider: str, profile: str | None = None) -> ApiKeyCredential | OAuthCredential | None:
|
|
161
|
+
"""获取指定提供商在指定 Profile 下的凭证(缺省为 active profile 或 default)。"""
|
|
162
|
+
store = self.load_store()
|
|
163
|
+
profile_name = profile or store.active_profiles.get(provider, "default")
|
|
164
|
+
return store.providers.get(provider, {}).get(profile_name)
|
|
165
|
+
|
|
166
|
+
def remove_credential(self, provider: str, profile: str | None = None) -> bool:
|
|
167
|
+
"""安全删除指定提供商的凭据。
|
|
168
|
+
|
|
169
|
+
若未指定 profile,则删除该 provider 下的所有凭据及 active_profiles 映射;
|
|
170
|
+
若指定了 profile,则删除对应 Profile 的凭据;若删除后该 provider 无其他 Profile,
|
|
171
|
+
则同时清理其映射。
|
|
172
|
+
返回 True 表示成功删除,False 表示目标凭据不存在。
|
|
173
|
+
"""
|
|
174
|
+
self.lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
with FileLock(str(self.lock_path), timeout=self.lock_timeout):
|
|
176
|
+
store = self.load_store()
|
|
177
|
+
removed = False
|
|
178
|
+
if profile is None:
|
|
179
|
+
if provider in store.providers:
|
|
180
|
+
del store.providers[provider]
|
|
181
|
+
removed = True
|
|
182
|
+
if provider in store.active_profiles:
|
|
183
|
+
del store.active_profiles[provider]
|
|
184
|
+
removed = True
|
|
185
|
+
else:
|
|
186
|
+
if provider in store.providers and profile in store.providers[provider]:
|
|
187
|
+
del store.providers[provider][profile]
|
|
188
|
+
removed = True
|
|
189
|
+
if not store.providers[provider]:
|
|
190
|
+
del store.providers[provider]
|
|
191
|
+
if store.active_profiles.get(provider) == profile:
|
|
192
|
+
if provider in store.providers and store.providers[provider]:
|
|
193
|
+
store.active_profiles[provider] = next(iter(store.providers[provider]))
|
|
194
|
+
else:
|
|
195
|
+
store.active_profiles.pop(provider, None)
|
|
196
|
+
if removed:
|
|
197
|
+
self.save_store(store)
|
|
198
|
+
return removed
|
|
199
|
+
|
|
200
|
+
async def get_valid_token(self, provider: str, profile: str | None = None) -> str:
|
|
201
|
+
"""获取有效的调用 Token,自动完成 API Key 变量解析或 OAuth 静默刷新。"""
|
|
202
|
+
self.lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
with FileLock(str(self.lock_path), timeout=max(self.lock_timeout, 10.0)):
|
|
204
|
+
store = self.load_store()
|
|
205
|
+
profile_name = profile or store.active_profiles.get(provider, "default")
|
|
206
|
+
prov_dict = store.providers.get(provider, {})
|
|
207
|
+
cred = prov_dict.get(profile_name)
|
|
208
|
+
|
|
209
|
+
if not cred:
|
|
210
|
+
raise RuntimeError(f"未找到提供商 '{provider}' (Profile: {profile_name}) 的有效凭据,请先配置或登录。")
|
|
211
|
+
|
|
212
|
+
if isinstance(cred, ApiKeyCredential):
|
|
213
|
+
return cred.resolve_key()
|
|
214
|
+
|
|
215
|
+
if isinstance(cred, OAuthCredential):
|
|
216
|
+
if not cred.is_expired():
|
|
217
|
+
return cred.access
|
|
218
|
+
|
|
219
|
+
# 执行 OAuth 静默刷新
|
|
220
|
+
client_id = cred.client_id or DEFAULT_ANTIGRAVITY_CLIENT_ID
|
|
221
|
+
client_secret = cred.client_secret or DEFAULT_ANTIGRAVITY_CLIENT_SECRET
|
|
222
|
+
try:
|
|
223
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
224
|
+
resp = await client.post(
|
|
225
|
+
GOOGLE_TOKEN_URL,
|
|
226
|
+
data={
|
|
227
|
+
"client_id": client_id,
|
|
228
|
+
"client_secret": client_secret,
|
|
229
|
+
"refresh_token": cred.refresh,
|
|
230
|
+
"grant_type": "refresh_token",
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
if resp.status_code != 200:
|
|
234
|
+
raise RuntimeError(f"OAuth Token 自动刷新失败 ({resp.status_code}): {resp.text}")
|
|
235
|
+
data = resp.json()
|
|
236
|
+
except httpx.RequestError as exc:
|
|
237
|
+
raise RuntimeError(f"OAuth Token 自动刷新网络连接失败: {exc}") from exc
|
|
238
|
+
|
|
239
|
+
new_access = data["access_token"]
|
|
240
|
+
expires_in = data.get("expires_in", 3600)
|
|
241
|
+
try:
|
|
242
|
+
exp_seconds = int(expires_in)
|
|
243
|
+
now_ms = int(time.time() * 1000)
|
|
244
|
+
except (ValueError, TypeError):
|
|
245
|
+
exp_seconds = 3600
|
|
246
|
+
now_ms = 0
|
|
247
|
+
update_kwargs: dict[str, Any] = {
|
|
248
|
+
"access": new_access,
|
|
249
|
+
"expires": now_ms + (exp_seconds - 300) * 1000,
|
|
250
|
+
}
|
|
251
|
+
if "refresh_token" in data and data["refresh_token"]:
|
|
252
|
+
update_kwargs["refresh"] = data["refresh_token"]
|
|
253
|
+
|
|
254
|
+
updated_cred = cred.model_copy(update=update_kwargs)
|
|
255
|
+
prov_dict[profile_name] = updated_cred
|
|
256
|
+
self.save_store(store)
|
|
257
|
+
return new_access
|
|
258
|
+
|
|
259
|
+
raise RuntimeError(f"未知的凭据类型: {type(cred)}")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .antigravity import (
|
|
8
|
+
ANTIGRAVITY_USER_AGENT,
|
|
9
|
+
DEFAULT_ANTIGRAVITY_ENDPOINT,
|
|
10
|
+
AntigravityCredentials,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class QuotaBucket:
|
|
16
|
+
"""单个模型或操作类型的配额桶。"""
|
|
17
|
+
|
|
18
|
+
bucket_id: str
|
|
19
|
+
display_name: str
|
|
20
|
+
remaining_fraction: float
|
|
21
|
+
reset_time: str | None = None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def remaining_percent(self) -> int:
|
|
25
|
+
"""配额剩余百分比(0-100 整数)。"""
|
|
26
|
+
return int(round(max(0.0, min(1.0, self.remaining_fraction)) * 100))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def retrieve_user_quota_summary(creds: AntigravityCredentials) -> list[QuotaBucket]:
|
|
30
|
+
"""向 Google Cloud Code 网关查询当前用户各模型配额余量。"""
|
|
31
|
+
url = f"{DEFAULT_ANTIGRAVITY_ENDPOINT}/v1internal:retrieveUserQuotaSummary"
|
|
32
|
+
headers = {
|
|
33
|
+
"Authorization": f"Bearer {creds.access_token}",
|
|
34
|
+
"x-goog-user-project": creds.project_id,
|
|
35
|
+
"User-Agent": ANTIGRAVITY_USER_AGENT,
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
}
|
|
38
|
+
try:
|
|
39
|
+
res = httpx.post(url, headers=headers, json={}, timeout=10.0)
|
|
40
|
+
if res.status_code != 200:
|
|
41
|
+
return []
|
|
42
|
+
data = res.json()
|
|
43
|
+
buckets: list[QuotaBucket] = []
|
|
44
|
+
for group in data.get("groups", []):
|
|
45
|
+
for b in group.get("buckets", []):
|
|
46
|
+
buckets.append(
|
|
47
|
+
QuotaBucket(
|
|
48
|
+
bucket_id=b.get("bucketId", ""),
|
|
49
|
+
display_name=b.get("displayName", ""),
|
|
50
|
+
remaining_fraction=float(b.get("remainingFraction", 0.0)),
|
|
51
|
+
reset_time=b.get("resetTime"),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return buckets
|
|
55
|
+
except Exception:
|
|
56
|
+
return []
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""AuthStore 强类型凭证实体模型与模式定义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
from typing import Annotated, Literal, Union
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CredentialType",
|
|
15
|
+
"ApiKeyCredential",
|
|
16
|
+
"OAuthCredential",
|
|
17
|
+
"Credential",
|
|
18
|
+
"AuthStore",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CredentialType(str, Enum):
|
|
23
|
+
"""凭证类型枚举。"""
|
|
24
|
+
|
|
25
|
+
API_KEY = "api_key"
|
|
26
|
+
OAUTH = "oauth"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApiKeyCredential(BaseModel):
|
|
30
|
+
"""API Key 凭据模型。"""
|
|
31
|
+
|
|
32
|
+
type: Literal[CredentialType.API_KEY, "api_key"] = CredentialType.API_KEY
|
|
33
|
+
key: str
|
|
34
|
+
base_url: str | None = None
|
|
35
|
+
env: dict[str, str] = Field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
def resolve_key(self) -> str:
|
|
38
|
+
"""解析 API Key:处理字面量、!command 子进程输出与 $ENV_VAR/${ENV_VAR} 语法。"""
|
|
39
|
+
raw = self.key.strip()
|
|
40
|
+
if raw.startswith("!"):
|
|
41
|
+
cmd = raw[1:].strip()
|
|
42
|
+
if os.name == "nt":
|
|
43
|
+
parts = [
|
|
44
|
+
p.strip('"') if (p.startswith('"') and p.endswith('"')) else p
|
|
45
|
+
for p in shlex.split(cmd, posix=False)
|
|
46
|
+
]
|
|
47
|
+
else:
|
|
48
|
+
parts = shlex.split(cmd)
|
|
49
|
+
if not parts:
|
|
50
|
+
return ""
|
|
51
|
+
res = subprocess.run(parts, capture_output=True, text=True, check=True)
|
|
52
|
+
return res.stdout.strip()
|
|
53
|
+
if raw.startswith("$"):
|
|
54
|
+
var_name = raw[1:].strip("{}")
|
|
55
|
+
return os.environ.get(var_name, "")
|
|
56
|
+
return raw
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OAuthCredential(BaseModel):
|
|
60
|
+
"""OAuth 凭据模型(支持 Google Cloud Code / Antigravity)。"""
|
|
61
|
+
|
|
62
|
+
type: Literal[CredentialType.OAUTH, "oauth"] = CredentialType.OAUTH
|
|
63
|
+
access: str
|
|
64
|
+
refresh: str
|
|
65
|
+
expires: int = 0 # 毫秒时间戳,<=0 表示永久或不计算过期
|
|
66
|
+
project_id: str = "aicode-consumers"
|
|
67
|
+
email: str | None = None
|
|
68
|
+
client_id: str | None = None
|
|
69
|
+
client_secret: str | None = None
|
|
70
|
+
|
|
71
|
+
def is_expired(self, buffer_ms: int = 300_000) -> bool:
|
|
72
|
+
"""判定凭证是否已过期。
|
|
73
|
+
|
|
74
|
+
默认包含 5 分钟 (300,000ms) 缓冲期,保证在真正过期前完成自动刷新。
|
|
75
|
+
当 expires <= 0 时视为不计算过期。
|
|
76
|
+
"""
|
|
77
|
+
if self.expires <= 0:
|
|
78
|
+
return False
|
|
79
|
+
return self.expires <= int(time.time() * 1000) + buffer_ms
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# 使用 Pydantic 区分联合体,基于 `type` 字段精准反序列化
|
|
83
|
+
Credential = Annotated[
|
|
84
|
+
Union[ApiKeyCredential, OAuthCredential],
|
|
85
|
+
Field(discriminator="type"),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AuthStore(BaseModel):
|
|
90
|
+
"""全局统一凭证存储实体(映射 ~/.my-pi-agent/auth.json)。"""
|
|
91
|
+
|
|
92
|
+
version: int = 1
|
|
93
|
+
active_profiles: dict[str, str] = Field(default_factory=dict)
|
|
94
|
+
providers: dict[str, dict[str, Credential]] = Field(default_factory=dict)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# pyright: reportUnreachable=false
|
|
2
|
+
"""LLM 门面:按 provider 路由到对应实现,对外一套 API,只透传不碰 SDK。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import AsyncIterator, Iterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .config import Config # pyright: ignore[reportMissingImports]
|
|
10
|
+
from .events import StreamEvent # pyright: ignore[reportMissingImports]
|
|
11
|
+
from .models import ( # pyright: ignore[reportMissingImports]
|
|
12
|
+
Message,
|
|
13
|
+
Response,
|
|
14
|
+
StreamChunk,
|
|
15
|
+
)
|
|
16
|
+
from .providers import Provider # pyright: ignore[reportMissingImports]
|
|
17
|
+
from .providers.registry import (
|
|
18
|
+
PROVIDER_REGISTRY, # pyright: ignore[reportMissingImports]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LLM:
|
|
23
|
+
"""统一 LLM 客户端门面:严格接收强类型 Config 配置,多态路由至具体 Provider。"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: Config) -> None:
|
|
26
|
+
"""标准工程构造:严格接收 Config 实例。"""
|
|
27
|
+
if not isinstance(config, Config): # pyright: ignore[reportUnreachable]
|
|
28
|
+
raise TypeError(f"LLM expects a Config instance, got {type(config).__name__}") # pyright: ignore[reportUnreachable]
|
|
29
|
+
if config.provider not in PROVIDER_REGISTRY:
|
|
30
|
+
raise ValueError(f"Unknown provider '{config.provider}'. Available: {', '.join(sorted(PROVIDER_REGISTRY))}")
|
|
31
|
+
if not config.api_key and config.provider != "antigravity":
|
|
32
|
+
raise ValueError(f"No API key for provider: {config.provider}")
|
|
33
|
+
provider_cls = PROVIDER_REGISTRY[config.provider]
|
|
34
|
+
self._provider: Provider = provider_cls(config)
|
|
35
|
+
self.config = config
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def model(self) -> str:
|
|
39
|
+
"""当前配置的模型名。"""
|
|
40
|
+
if not self.config.model:
|
|
41
|
+
raise ValueError("No model specified. Pass model=... or set Config.model.")
|
|
42
|
+
return self.config.model
|
|
43
|
+
|
|
44
|
+
def _resolve_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
45
|
+
"""统一合并调用参数与全局 Config 中的默认采样配置。"""
|
|
46
|
+
opts = dict(kwargs)
|
|
47
|
+
if self.config.temperature is not None:
|
|
48
|
+
opts.setdefault("temperature", self.config.temperature)
|
|
49
|
+
if self.config.max_tokens is not None:
|
|
50
|
+
opts.setdefault("max_tokens", self.config.max_tokens)
|
|
51
|
+
return opts
|
|
52
|
+
|
|
53
|
+
def chat(
|
|
54
|
+
self,
|
|
55
|
+
messages: list[Message],
|
|
56
|
+
*,
|
|
57
|
+
tools: list[dict] | None = None,
|
|
58
|
+
model: str | None = None,
|
|
59
|
+
**kwargs: Any,
|
|
60
|
+
) -> Response:
|
|
61
|
+
"""同步对话:完整历史 + 可选工具。"""
|
|
62
|
+
opts = self._resolve_kwargs(kwargs)
|
|
63
|
+
return self._provider.chat(messages, model=model or self.model, tools=tools, **opts)
|
|
64
|
+
|
|
65
|
+
def stream(
|
|
66
|
+
self,
|
|
67
|
+
messages: list[Message],
|
|
68
|
+
*,
|
|
69
|
+
tools: list[dict] | None = None,
|
|
70
|
+
model: str | None = None,
|
|
71
|
+
**kwargs: Any,
|
|
72
|
+
) -> Iterator[StreamChunk]:
|
|
73
|
+
"""同步流式。"""
|
|
74
|
+
opts = self._resolve_kwargs(kwargs)
|
|
75
|
+
return self._provider.stream(messages, model=model or self.model, tools=tools, **opts)
|
|
76
|
+
|
|
77
|
+
async def achat(
|
|
78
|
+
self,
|
|
79
|
+
messages: list[Message],
|
|
80
|
+
*,
|
|
81
|
+
tools: list[dict] | None = None,
|
|
82
|
+
model: str | None = None,
|
|
83
|
+
**kwargs: Any,
|
|
84
|
+
) -> Response:
|
|
85
|
+
"""异步对话。"""
|
|
86
|
+
opts = self._resolve_kwargs(kwargs)
|
|
87
|
+
return await self._provider.achat(messages, model=model or self.model, tools=tools, **opts)
|
|
88
|
+
|
|
89
|
+
async def achat_stream(
|
|
90
|
+
self,
|
|
91
|
+
messages: list[Message],
|
|
92
|
+
*,
|
|
93
|
+
tools: list[dict] | None = None,
|
|
94
|
+
model: str | None = None,
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
) -> AsyncIterator[StreamChunk]:
|
|
97
|
+
"""异步流式。调用方直接 `async for chunk in llm.achat_stream(...)` 迭代,不 await。"""
|
|
98
|
+
opts = self._resolve_kwargs(kwargs)
|
|
99
|
+
async for chunk in self._provider.achat_stream(messages, model=model or self.model, tools=tools, **opts):
|
|
100
|
+
yield chunk
|
|
101
|
+
|
|
102
|
+
async def astream_events(
|
|
103
|
+
self,
|
|
104
|
+
messages: list[Message],
|
|
105
|
+
*,
|
|
106
|
+
tools: list[dict] | None = None,
|
|
107
|
+
model: str | None = None,
|
|
108
|
+
signal: Any | None = None,
|
|
109
|
+
**kwargs: Any,
|
|
110
|
+
) -> AsyncIterator[StreamEvent]:
|
|
111
|
+
"""异步高阶流式事件流:直接产出 StreamStartEvent/TextDeltaEvent/StreamDoneEvent/StreamErrorEvent。"""
|
|
112
|
+
opts = self._resolve_kwargs(kwargs)
|
|
113
|
+
async for ev in self._provider.astream_events(
|
|
114
|
+
messages, model=model or self.model, tools=tools, signal=signal, **opts
|
|
115
|
+
):
|
|
116
|
+
yield ev
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""LLM 客户端配置:集中校验、可复用、不可变。"""
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config(BaseModel):
|
|
6
|
+
"""统一配置:provider/model/api_key/采样参数/网络参数。"""
|
|
7
|
+
|
|
8
|
+
provider: str = "openai"
|
|
9
|
+
model: str | None = None
|
|
10
|
+
api_key: str | None = None
|
|
11
|
+
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
|
|
12
|
+
max_tokens: int | None = Field(default=None, ge=1)
|
|
13
|
+
timeout: int = Field(default=30, ge=1)
|
|
14
|
+
max_retries: int = Field(default=3, ge=0)
|
|
15
|
+
base_url: str | None = None
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(frozen=True)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""模型边界层高阶流式事件模型(对标 Tau provider_events)。
|
|
2
|
+
|
|
3
|
+
提供供应商中立的高阶流式生命周期事件,携带实时累积的 partial: Message 快照。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from my_agent_llm.models import Message, ToolCall
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"StreamDoneEvent",
|
|
14
|
+
"StreamErrorEvent",
|
|
15
|
+
"StreamEvent",
|
|
16
|
+
"StreamStartEvent",
|
|
17
|
+
"TextDeltaEvent",
|
|
18
|
+
"ThinkingDeltaEvent",
|
|
19
|
+
"ToolCallDeltaEvent",
|
|
20
|
+
"ToolCallDoneEvent",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class StreamEvent:
|
|
26
|
+
"""高阶流式事件基类。"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class StreamStartEvent(StreamEvent):
|
|
31
|
+
"""首个 Token 生成时发射,携带初始 partial Message 快照。"""
|
|
32
|
+
|
|
33
|
+
partial: Message
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class TextDeltaEvent(StreamEvent):
|
|
38
|
+
"""正文文本增量事件。"""
|
|
39
|
+
|
|
40
|
+
delta: str
|
|
41
|
+
partial: Message
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ThinkingDeltaEvent(StreamEvent):
|
|
46
|
+
"""思维链推理增量事件(对标 DeepSeek-R1 / Claude 3.7 Thinking)。"""
|
|
47
|
+
|
|
48
|
+
delta: str
|
|
49
|
+
partial: Message
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class ToolCallDeltaEvent(StreamEvent):
|
|
54
|
+
"""工具参数增量事件。"""
|
|
55
|
+
|
|
56
|
+
index: int
|
|
57
|
+
delta: str
|
|
58
|
+
partial: Message
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class ToolCallDoneEvent(StreamEvent):
|
|
63
|
+
"""单个工具调用参数组装完毕事件。"""
|
|
64
|
+
|
|
65
|
+
index: int
|
|
66
|
+
tool_call: ToolCall
|
|
67
|
+
partial: Message
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class StreamDoneEvent(StreamEvent):
|
|
72
|
+
"""流式正常结束事件,携带完整完型的 Message 实体与 Usage。"""
|
|
73
|
+
|
|
74
|
+
message: Message
|
|
75
|
+
usage: dict[str, int] | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class StreamErrorEvent(StreamEvent):
|
|
80
|
+
"""流式异常或中途取消事件(Never-Throw 保证)。"""
|
|
81
|
+
|
|
82
|
+
error: Message
|
|
83
|
+
stop_reason: str = "error" # "error" | "cancelled"
|
|
84
|
+
exc: Exception | None = None
|