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,480 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import uuid
|
|
8
|
+
from collections.abc import AsyncIterator, Iterator
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from ..auth.antigravity import (
|
|
14
|
+
ANTIGRAVITY_USER_AGENT,
|
|
15
|
+
DEFAULT_ANTIGRAVITY_ENDPOINT,
|
|
16
|
+
AntigravityAuthResolver,
|
|
17
|
+
)
|
|
18
|
+
from ..config import Config
|
|
19
|
+
from ..models import Message, Response, StreamChunk, ToolCall
|
|
20
|
+
from .openai import OpenAIProvider
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
ANTIGRAVITY_ROUTING: dict[str, dict[str, str]] = {
|
|
25
|
+
"gemini-3.8-flash": {
|
|
26
|
+
"off": "gemini-3.8-flash-low",
|
|
27
|
+
"minimal": "gemini-3.8-flash-low",
|
|
28
|
+
"low": "gemini-3.8-flash-low",
|
|
29
|
+
"medium": "gemini-3.8-flash-medium",
|
|
30
|
+
"high": "gemini-3.8-flash-high",
|
|
31
|
+
},
|
|
32
|
+
"gemini-3.7-flash": {
|
|
33
|
+
"off": "gemini-3.7-flash-low",
|
|
34
|
+
"minimal": "gemini-3.7-flash-low",
|
|
35
|
+
"low": "gemini-3.7-flash-low",
|
|
36
|
+
"medium": "gemini-3.7-flash-medium",
|
|
37
|
+
"high": "gemini-3.7-flash-high",
|
|
38
|
+
},
|
|
39
|
+
"gemini-3.6-flash": {
|
|
40
|
+
"off": "gemini-3.6-flash-low",
|
|
41
|
+
"minimal": "gemini-3.6-flash-low",
|
|
42
|
+
"low": "gemini-3.6-flash-low",
|
|
43
|
+
"medium": "gemini-3.6-flash-medium",
|
|
44
|
+
"high": "gemini-3.6-flash-high",
|
|
45
|
+
},
|
|
46
|
+
"gemini-3.5-flash": {
|
|
47
|
+
"off": "gemini-3.5-flash-extra-low",
|
|
48
|
+
"minimal": "gemini-3.5-flash-extra-low",
|
|
49
|
+
"low": "gemini-3.5-flash-extra-low",
|
|
50
|
+
"medium": "gemini-3.5-flash-low",
|
|
51
|
+
"high": "gemini-3-flash-agent",
|
|
52
|
+
},
|
|
53
|
+
"gemini-3.1-pro": {
|
|
54
|
+
"off": "gemini-3.1-pro-low",
|
|
55
|
+
"minimal": "gemini-3.1-pro-low",
|
|
56
|
+
"low": "gemini-3.1-pro-low",
|
|
57
|
+
"medium": "gemini-3.1-pro-low",
|
|
58
|
+
"high": "gemini-pro-agent",
|
|
59
|
+
},
|
|
60
|
+
"claude-sonnet-4-6": {
|
|
61
|
+
"off": "claude-sonnet-4-6",
|
|
62
|
+
"minimal": "claude-sonnet-4-6",
|
|
63
|
+
"low": "claude-sonnet-4-6",
|
|
64
|
+
"medium": "claude-sonnet-4-6",
|
|
65
|
+
"high": "claude-sonnet-4-6",
|
|
66
|
+
},
|
|
67
|
+
"claude-opus-4-6": {
|
|
68
|
+
"off": "claude-opus-4-6-thinking",
|
|
69
|
+
"minimal": "claude-opus-4-6-thinking",
|
|
70
|
+
"low": "claude-opus-4-6-thinking",
|
|
71
|
+
"medium": "claude-opus-4-6-thinking",
|
|
72
|
+
"high": "claude-opus-4-6-thinking",
|
|
73
|
+
},
|
|
74
|
+
"gpt-oss-120b": {
|
|
75
|
+
"off": "gpt-oss-120b-medium",
|
|
76
|
+
"minimal": "gpt-oss-120b-medium",
|
|
77
|
+
"low": "gpt-oss-120b-medium",
|
|
78
|
+
"medium": "gpt-oss-120b-medium",
|
|
79
|
+
"high": "gpt-oss-120b-medium",
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ENDPOINT_CANDIDATES = [
|
|
84
|
+
"https://daily-cloudcode-pa.googleapis.com",
|
|
85
|
+
"https://cloudcode-pa.googleapis.com",
|
|
86
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AntigravityProvider(OpenAIProvider):
|
|
91
|
+
"""Google Cloud Code Assist (Antigravity) 原生 SSE 流式模型提供商适配器。"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
config: Config,
|
|
96
|
+
client: Any | None = None,
|
|
97
|
+
async_client: Any | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
self.auth_resolver = AntigravityAuthResolver()
|
|
100
|
+
base_url = config.base_url or os.environ.get("ANTIGRAVITY_BASE_URL") or DEFAULT_ANTIGRAVITY_ENDPOINT
|
|
101
|
+
|
|
102
|
+
# 自动解析有效 Token
|
|
103
|
+
api_key = config.api_key
|
|
104
|
+
self.project_id = os.environ.get("ANTIGRAVITY_PROJECT_ID", "aicode-consumers")
|
|
105
|
+
if not api_key:
|
|
106
|
+
try:
|
|
107
|
+
creds = self.auth_resolver.get_valid_credentials()
|
|
108
|
+
api_key = creds.access_token
|
|
109
|
+
self.project_id = creds.project_id
|
|
110
|
+
except Exception:
|
|
111
|
+
api_key = "placeholder_token" # noqa: S105
|
|
112
|
+
|
|
113
|
+
config = config.model_copy(
|
|
114
|
+
update={
|
|
115
|
+
"base_url": base_url,
|
|
116
|
+
"api_key": api_key,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
self.base_url = base_url
|
|
120
|
+
self.config = config
|
|
121
|
+
|
|
122
|
+
self._is_mock_client = client is not None or async_client is not None
|
|
123
|
+
super().__init__(config, client=client, async_client=async_client)
|
|
124
|
+
|
|
125
|
+
def _build_headers(self) -> dict[str, str]:
|
|
126
|
+
token = self.config.api_key or ""
|
|
127
|
+
return {
|
|
128
|
+
"Authorization": f"Bearer {token}",
|
|
129
|
+
"x-goog-user-project": self.project_id,
|
|
130
|
+
"User-Agent": ANTIGRAVITY_USER_AGENT,
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def _resolve_runtime_model(self, model: str, thinking_level: str | None = None) -> str:
|
|
135
|
+
level = (thinking_level or "low").lower()
|
|
136
|
+
if level in ("xhigh", "max"):
|
|
137
|
+
level = "high"
|
|
138
|
+
elif level == "minimal":
|
|
139
|
+
level = "low"
|
|
140
|
+
if model in ANTIGRAVITY_ROUTING:
|
|
141
|
+
return ANTIGRAVITY_ROUTING[model].get(level, ANTIGRAVITY_ROUTING[model].get("low", model))
|
|
142
|
+
return model
|
|
143
|
+
|
|
144
|
+
def _convert_antigravity_messages(
|
|
145
|
+
self, messages: list[Message]
|
|
146
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
|
147
|
+
contents: list[dict[str, Any]] = []
|
|
148
|
+
system_parts: list[dict[str, str]] = []
|
|
149
|
+
|
|
150
|
+
for msg in messages:
|
|
151
|
+
if msg.role == "system":
|
|
152
|
+
if msg.content:
|
|
153
|
+
system_parts.append({"text": msg.content})
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
if msg.role == "user":
|
|
157
|
+
contents.append(
|
|
158
|
+
{
|
|
159
|
+
"role": "user",
|
|
160
|
+
"parts": [{"text": msg.content or ""}],
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
elif msg.role == "assistant":
|
|
164
|
+
parts: list[dict[str, Any]] = []
|
|
165
|
+
if msg.content:
|
|
166
|
+
parts.append({"text": msg.content})
|
|
167
|
+
tool_calls = (msg.metadata or {}).get("tool_calls") or []
|
|
168
|
+
for tc in tool_calls:
|
|
169
|
+
tc_name = tc.get("name") if isinstance(tc, dict) else getattr(tc, "name", "")
|
|
170
|
+
tc_args = tc.get("args") if isinstance(tc, dict) else getattr(tc, "args", {})
|
|
171
|
+
if tc_name:
|
|
172
|
+
parts.append(
|
|
173
|
+
{
|
|
174
|
+
"functionCall": {
|
|
175
|
+
"name": tc_name,
|
|
176
|
+
"args": (tc_args if isinstance(tc_args, dict) else {}),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
if not parts:
|
|
181
|
+
parts.append({"text": ""})
|
|
182
|
+
contents.append({"role": "model", "parts": parts})
|
|
183
|
+
elif msg.role == "tool":
|
|
184
|
+
tool_name = (msg.metadata or {}).get("tool_name", "tool")
|
|
185
|
+
contents.append(
|
|
186
|
+
{
|
|
187
|
+
"role": "user",
|
|
188
|
+
"parts": [
|
|
189
|
+
{
|
|
190
|
+
"functionResponse": {
|
|
191
|
+
"name": tool_name,
|
|
192
|
+
"response": {"result": msg.content or ""},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
],
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
system_instruction = {"role": "user", "parts": system_parts} if system_parts else None
|
|
200
|
+
return contents, system_instruction
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _inline_schema_defs(schema: dict[str, Any]) -> dict[str, Any]:
|
|
204
|
+
"""将 Pydantic 生成的 $defs/$ref 递归内联展开,消除 Google Protobuf 解析异常。"""
|
|
205
|
+
try:
|
|
206
|
+
s = json.loads(json.dumps(schema))
|
|
207
|
+
except Exception:
|
|
208
|
+
return schema
|
|
209
|
+
defs = s.pop("$defs", {}) or s.pop("definitions", {})
|
|
210
|
+
|
|
211
|
+
def _replace(obj: Any) -> Any:
|
|
212
|
+
if isinstance(obj, dict):
|
|
213
|
+
if "$ref" in obj:
|
|
214
|
+
ref = str(obj["$ref"])
|
|
215
|
+
ref_key = ref.split("/")[-1]
|
|
216
|
+
if ref_key in defs:
|
|
217
|
+
resolved = _replace(defs[ref_key])
|
|
218
|
+
merged = {k: v for k, v in obj.items() if k != "$ref"}
|
|
219
|
+
if isinstance(resolved, dict):
|
|
220
|
+
return {**resolved, **merged}
|
|
221
|
+
return resolved
|
|
222
|
+
return {k: _replace(v) for k, v in obj.items()}
|
|
223
|
+
elif isinstance(obj, list):
|
|
224
|
+
return [_replace(x) for x in obj]
|
|
225
|
+
return obj
|
|
226
|
+
|
|
227
|
+
return _replace(s)
|
|
228
|
+
|
|
229
|
+
def _convert_tools(self, tools: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None:
|
|
230
|
+
if not tools:
|
|
231
|
+
return None
|
|
232
|
+
declarations: list[dict[str, Any]] = []
|
|
233
|
+
for t in tools:
|
|
234
|
+
fn = t.get("function", t)
|
|
235
|
+
name = fn.get("name", "")
|
|
236
|
+
if not name:
|
|
237
|
+
continue
|
|
238
|
+
raw_params = fn.get("parameters") or {"type": "object", "properties": {}}
|
|
239
|
+
clean_params = self._inline_schema_defs(raw_params)
|
|
240
|
+
decl: dict[str, Any] = {
|
|
241
|
+
"name": name,
|
|
242
|
+
"description": fn.get("description", ""),
|
|
243
|
+
"parametersJsonSchema": clean_params,
|
|
244
|
+
}
|
|
245
|
+
declarations.append(decl)
|
|
246
|
+
|
|
247
|
+
if declarations:
|
|
248
|
+
return [{"functionDeclarations": declarations}]
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def _is_mock(self) -> bool:
|
|
253
|
+
if self._is_mock_client:
|
|
254
|
+
return True
|
|
255
|
+
c = getattr(self, "client", None)
|
|
256
|
+
if c is not None and type(c).__module__ != "openai":
|
|
257
|
+
return True
|
|
258
|
+
ac = getattr(self, "async_client", None)
|
|
259
|
+
if ac is not None and type(ac).__module__ != "openai":
|
|
260
|
+
return True
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
def chat(
|
|
264
|
+
self,
|
|
265
|
+
messages: list[Message],
|
|
266
|
+
*,
|
|
267
|
+
model: str,
|
|
268
|
+
tools: list[dict] | None = None,
|
|
269
|
+
**kwargs,
|
|
270
|
+
) -> Response:
|
|
271
|
+
if self._is_mock:
|
|
272
|
+
return super().chat(messages, model=model, tools=tools, **kwargs)
|
|
273
|
+
return asyncio.run(self.achat(messages, model=model, tools=tools, **kwargs))
|
|
274
|
+
|
|
275
|
+
def stream(
|
|
276
|
+
self,
|
|
277
|
+
messages: list[Message],
|
|
278
|
+
*,
|
|
279
|
+
model: str,
|
|
280
|
+
tools: list[dict] | None = None,
|
|
281
|
+
**kwargs,
|
|
282
|
+
) -> Iterator[StreamChunk]:
|
|
283
|
+
if self._is_mock:
|
|
284
|
+
for c in super().stream(messages, model=model, tools=tools, **kwargs):
|
|
285
|
+
yield c
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
loop = asyncio.new_event_loop()
|
|
289
|
+
try:
|
|
290
|
+
gen = self.achat_stream(messages, model=model, tools=tools, **kwargs)
|
|
291
|
+
while True:
|
|
292
|
+
try:
|
|
293
|
+
chunk = loop.run_until_complete(gen.__anext__())
|
|
294
|
+
yield chunk
|
|
295
|
+
except StopAsyncIteration:
|
|
296
|
+
break
|
|
297
|
+
finally:
|
|
298
|
+
loop.close()
|
|
299
|
+
|
|
300
|
+
async def achat(
|
|
301
|
+
self,
|
|
302
|
+
messages: list[Message],
|
|
303
|
+
*,
|
|
304
|
+
model: str,
|
|
305
|
+
tools: list[dict] | None = None,
|
|
306
|
+
**kwargs,
|
|
307
|
+
) -> Response:
|
|
308
|
+
if self._is_mock:
|
|
309
|
+
return await super().achat(messages, model=model, tools=tools, **kwargs)
|
|
310
|
+
|
|
311
|
+
full_content: list[str] = []
|
|
312
|
+
reasoning_list: list[str] = []
|
|
313
|
+
tool_calls: list[ToolCall] = []
|
|
314
|
+
usage: dict[str, Any] | None = None
|
|
315
|
+
finish_reason: str = "stop"
|
|
316
|
+
|
|
317
|
+
async for chunk in self.achat_stream(messages, model=model, tools=tools, **kwargs):
|
|
318
|
+
if chunk.content:
|
|
319
|
+
full_content.append(chunk.content)
|
|
320
|
+
if chunk.metadata and chunk.metadata.get("reasoning_content"):
|
|
321
|
+
reasoning_list.append(chunk.metadata["reasoning_content"])
|
|
322
|
+
if chunk.tool_calls:
|
|
323
|
+
tool_calls.extend(chunk.tool_calls)
|
|
324
|
+
if chunk.usage:
|
|
325
|
+
usage = chunk.usage
|
|
326
|
+
if chunk.finish_reason:
|
|
327
|
+
finish_reason = chunk.finish_reason
|
|
328
|
+
|
|
329
|
+
return Response(
|
|
330
|
+
content="".join(full_content),
|
|
331
|
+
model=model,
|
|
332
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
333
|
+
reasoning_content="".join(reasoning_list) or None,
|
|
334
|
+
usage=usage,
|
|
335
|
+
finish_reason=finish_reason,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
async def achat_stream(
|
|
339
|
+
self,
|
|
340
|
+
messages: list[Message],
|
|
341
|
+
*,
|
|
342
|
+
model: str,
|
|
343
|
+
tools: list[dict] | None = None,
|
|
344
|
+
**kwargs,
|
|
345
|
+
) -> AsyncIterator[StreamChunk]:
|
|
346
|
+
if self._is_mock:
|
|
347
|
+
async for chunk in super().achat_stream(messages, model=model, tools=tools, **kwargs):
|
|
348
|
+
yield chunk
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
# 1. 解析最新凭据
|
|
352
|
+
access_token = self.config.api_key
|
|
353
|
+
project_id = self.project_id
|
|
354
|
+
try:
|
|
355
|
+
creds = self.auth_resolver.get_valid_credentials()
|
|
356
|
+
access_token = creds.access_token
|
|
357
|
+
project_id = creds.project_id or project_id
|
|
358
|
+
except Exception as exc:
|
|
359
|
+
logger.debug("Antigravity 凭据解析失败,尝试环境变量或已存配置: %s", exc)
|
|
360
|
+
|
|
361
|
+
# 2. 映射运行时模型 ID
|
|
362
|
+
runtime_model = self._resolve_runtime_model(model, kwargs.get("thinking_level"))
|
|
363
|
+
|
|
364
|
+
# 3. 构造请求体
|
|
365
|
+
contents, system_instruction = self._convert_antigravity_messages(messages)
|
|
366
|
+
request_obj: dict[str, Any] = {"contents": contents}
|
|
367
|
+
if system_instruction:
|
|
368
|
+
request_obj["systemInstruction"] = system_instruction
|
|
369
|
+
tool_decl = self._convert_tools(tools)
|
|
370
|
+
if tool_decl:
|
|
371
|
+
request_obj["tools"] = tool_decl
|
|
372
|
+
|
|
373
|
+
payload = {
|
|
374
|
+
"project": project_id,
|
|
375
|
+
"model": runtime_model,
|
|
376
|
+
"request": request_obj,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
headers = {
|
|
380
|
+
"Authorization": f"Bearer {access_token}",
|
|
381
|
+
"Content-Type": "application/json",
|
|
382
|
+
"User-Agent": ANTIGRAVITY_USER_AGENT,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# 4. 遍历多候选端点容灾发起 SSE 流式调用 (对标 pi-antigravity)
|
|
386
|
+
endpoints = (
|
|
387
|
+
[self.base_url]
|
|
388
|
+
if (self.base_url and self.base_url != DEFAULT_ANTIGRAVITY_ENDPOINT)
|
|
389
|
+
else ENDPOINT_CANDIDATES
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
timeout = kwargs.get("timeout", self.config.timeout or 60.0)
|
|
393
|
+
async with httpx.AsyncClient(timeout=timeout) as http_client:
|
|
394
|
+
resp = None
|
|
395
|
+
for ep in endpoints:
|
|
396
|
+
url = f"{ep}/v1internal:streamGenerateContent?alt=sse"
|
|
397
|
+
try:
|
|
398
|
+
r = await http_client.post(url, headers=headers, json=payload)
|
|
399
|
+
if r.status_code == 200:
|
|
400
|
+
resp = r
|
|
401
|
+
break
|
|
402
|
+
elif r.status_code != 429:
|
|
403
|
+
resp = r
|
|
404
|
+
break
|
|
405
|
+
except Exception:
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
if resp is None or resp.status_code != 200:
|
|
409
|
+
err_text = resp.text[:300] if resp else "Connection error"
|
|
410
|
+
raise RuntimeError(f"Antigravity request failed ({resp.status_code if resp else 'error'}): {err_text}")
|
|
411
|
+
|
|
412
|
+
# 5. 解析 SSE 响应并实时产出 StreamChunk
|
|
413
|
+
prompt_tokens = 0
|
|
414
|
+
completion_tokens = 0
|
|
415
|
+
cached_tokens = 0
|
|
416
|
+
thought_tokens = 0
|
|
417
|
+
total_tokens = 0
|
|
418
|
+
finish_reason = None
|
|
419
|
+
tool_calls: list[ToolCall] = []
|
|
420
|
+
|
|
421
|
+
for line in resp.text.splitlines():
|
|
422
|
+
if not line.startswith("data:"):
|
|
423
|
+
continue
|
|
424
|
+
data_str = line[5:].strip()
|
|
425
|
+
if not data_str:
|
|
426
|
+
continue
|
|
427
|
+
try:
|
|
428
|
+
data = json.loads(data_str)
|
|
429
|
+
except Exception:
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
response_data = data.get("response", {})
|
|
433
|
+
usage_meta = response_data.get("usageMetadata")
|
|
434
|
+
if usage_meta:
|
|
435
|
+
prompt_tokens = usage_meta.get("promptTokenCount", 0)
|
|
436
|
+
cached_tokens = usage_meta.get("cachedContentTokenCount", 0)
|
|
437
|
+
thought_tokens = usage_meta.get("thoughtsTokenCount", 0)
|
|
438
|
+
completion_tokens = usage_meta.get("candidatesTokenCount", 0) + thought_tokens
|
|
439
|
+
total_tokens = usage_meta.get("totalTokenCount", 0)
|
|
440
|
+
|
|
441
|
+
candidates = response_data.get("candidates", [])
|
|
442
|
+
for cand in candidates:
|
|
443
|
+
if cand.get("finishReason"):
|
|
444
|
+
finish_reason = cand["finishReason"]
|
|
445
|
+
parts = cand.get("content", {}).get("parts", [])
|
|
446
|
+
for p in parts:
|
|
447
|
+
if p.get("thought"):
|
|
448
|
+
yield StreamChunk(
|
|
449
|
+
content="",
|
|
450
|
+
metadata={"reasoning_content": p.get("text", "")},
|
|
451
|
+
)
|
|
452
|
+
elif "text" in p:
|
|
453
|
+
yield StreamChunk(content=p["text"])
|
|
454
|
+
elif "functionCall" in p:
|
|
455
|
+
fn = p["functionCall"]
|
|
456
|
+
call_id = f"call_{uuid.uuid4().hex[:8]}"
|
|
457
|
+
tc = ToolCall(
|
|
458
|
+
id=call_id,
|
|
459
|
+
name=fn.get("name", ""),
|
|
460
|
+
args=fn.get("args", {}),
|
|
461
|
+
)
|
|
462
|
+
tool_calls.append(tc)
|
|
463
|
+
yield StreamChunk(content="", tool_calls=[tc])
|
|
464
|
+
|
|
465
|
+
# 最终块携带完整 Usage
|
|
466
|
+
usage_dict: dict[str, Any] = {
|
|
467
|
+
"prompt_tokens": max(0, prompt_tokens - cached_tokens),
|
|
468
|
+
"completion_tokens": completion_tokens,
|
|
469
|
+
"total_tokens": total_tokens,
|
|
470
|
+
}
|
|
471
|
+
if cached_tokens > 0:
|
|
472
|
+
usage_dict["cache_read_tokens"] = cached_tokens
|
|
473
|
+
if thought_tokens > 0:
|
|
474
|
+
usage_dict["reasoning_tokens"] = thought_tokens
|
|
475
|
+
yield StreamChunk(
|
|
476
|
+
content="",
|
|
477
|
+
finish_reason=finish_reason or "stop",
|
|
478
|
+
usage=usage_dict,
|
|
479
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
480
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# pyright: reportArgumentType=false, reportCallIssue=false
|
|
2
|
+
"""DeepSeek provider:OpenAI 兼容端点 + reasoning_content 提取。"""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import AsyncIterator, Iterator
|
|
6
|
+
|
|
7
|
+
from ..config import Config
|
|
8
|
+
from ..models import Message, Response, StreamChunk
|
|
9
|
+
from .openai import OpenAIProvider, _ToolCallAccumulator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DeepSeekProvider(OpenAIProvider):
|
|
13
|
+
"""DeepSeek provider:复用 OpenAI 兼容翻译,额外提取 reasoning_content。"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: Config, client=None, async_client=None):
|
|
16
|
+
"""初始化。默认 base_url 指向 deepseek。"""
|
|
17
|
+
updates = {}
|
|
18
|
+
if client is None and config.base_url is None:
|
|
19
|
+
updates["base_url"] = "https://api.deepseek.com"
|
|
20
|
+
if client is None and config.api_key is None:
|
|
21
|
+
key = os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
|
22
|
+
if key:
|
|
23
|
+
updates["api_key"] = key
|
|
24
|
+
if updates:
|
|
25
|
+
config = config.model_copy(update=updates)
|
|
26
|
+
super().__init__(config, client=client, async_client=async_client)
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def _extract_reasoning(message) -> str | None:
|
|
30
|
+
"""提取 reasoning_content(推理模型思考内容)。"""
|
|
31
|
+
return getattr(message, "reasoning_content", None) or None
|
|
32
|
+
|
|
33
|
+
def chat(
|
|
34
|
+
self,
|
|
35
|
+
messages: list[Message],
|
|
36
|
+
*,
|
|
37
|
+
model: str,
|
|
38
|
+
tools: list[dict] | None = None,
|
|
39
|
+
**kwargs,
|
|
40
|
+
) -> Response:
|
|
41
|
+
response = self.client.chat.completions.create( # pyright: ignore[reportCallIssue, reportArgumentType]
|
|
42
|
+
model=model,
|
|
43
|
+
messages=self._convert_messages(messages),
|
|
44
|
+
tools=tools,
|
|
45
|
+
**kwargs,
|
|
46
|
+
)
|
|
47
|
+
choice = response.choices[0]
|
|
48
|
+
return Response(
|
|
49
|
+
content=choice.message.content or "",
|
|
50
|
+
model=response.model,
|
|
51
|
+
reasoning_content=self._extract_reasoning(choice.message),
|
|
52
|
+
usage=self._extract_usage(response),
|
|
53
|
+
finish_reason=choice.finish_reason,
|
|
54
|
+
tool_calls=self._extract_tool_calls(choice.message),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def stream(
|
|
58
|
+
self,
|
|
59
|
+
messages: list[Message],
|
|
60
|
+
*,
|
|
61
|
+
model: str,
|
|
62
|
+
tools: list[dict] | None = None,
|
|
63
|
+
**kwargs,
|
|
64
|
+
) -> Iterator[StreamChunk]:
|
|
65
|
+
reasoning_parts: list[str] = []
|
|
66
|
+
accumulator = _ToolCallAccumulator()
|
|
67
|
+
text_acc = ""
|
|
68
|
+
usage = None
|
|
69
|
+
final_finish_reason: str | None = None
|
|
70
|
+
for chunk in self.client.chat.completions.create( # pyright: ignore[reportCallIssue, reportArgumentType]
|
|
71
|
+
model=model,
|
|
72
|
+
messages=self._convert_messages(messages),
|
|
73
|
+
tools=tools,
|
|
74
|
+
stream=True,
|
|
75
|
+
**kwargs,
|
|
76
|
+
):
|
|
77
|
+
chunk_usage = self._extract_usage(chunk)
|
|
78
|
+
if chunk_usage:
|
|
79
|
+
usage = chunk_usage
|
|
80
|
+
if not chunk.choices:
|
|
81
|
+
continue # usage-only 末块(choices 为空)——usage 已捕获,流式结束
|
|
82
|
+
choice = chunk.choices[0]
|
|
83
|
+
if choice.finish_reason:
|
|
84
|
+
final_finish_reason = choice.finish_reason
|
|
85
|
+
delta = choice.delta
|
|
86
|
+
accumulator.add(delta)
|
|
87
|
+
if getattr(delta, "reasoning_content", None):
|
|
88
|
+
reasoning_parts.append(delta.reasoning_content)
|
|
89
|
+
if getattr(delta, "content", None):
|
|
90
|
+
text_acc += delta.content
|
|
91
|
+
yield StreamChunk(
|
|
92
|
+
content=delta.content, finish_reason=choice.finish_reason
|
|
93
|
+
)
|
|
94
|
+
tool_calls = accumulator.finish()
|
|
95
|
+
reasoning = "".join(reasoning_parts) if reasoning_parts else None
|
|
96
|
+
final_response = Response(
|
|
97
|
+
content=text_acc,
|
|
98
|
+
model=model,
|
|
99
|
+
tool_calls=tool_calls,
|
|
100
|
+
reasoning_content=reasoning,
|
|
101
|
+
usage=usage,
|
|
102
|
+
finish_reason=final_finish_reason,
|
|
103
|
+
)
|
|
104
|
+
yield StreamChunk(
|
|
105
|
+
content="",
|
|
106
|
+
tool_calls=tool_calls,
|
|
107
|
+
usage=usage,
|
|
108
|
+
finish_reason=final_finish_reason,
|
|
109
|
+
metadata={"reasoning_content": reasoning} if reasoning else None,
|
|
110
|
+
response=final_response,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
async def achat(
|
|
114
|
+
self,
|
|
115
|
+
messages: list[Message],
|
|
116
|
+
*,
|
|
117
|
+
model: str,
|
|
118
|
+
tools: list[dict] | None = None,
|
|
119
|
+
**kwargs,
|
|
120
|
+
) -> Response:
|
|
121
|
+
if self.async_client is None:
|
|
122
|
+
raise RuntimeError("async_client not provided; cannot run async methods")
|
|
123
|
+
response = await self.async_client.chat.completions.create( # pyright: ignore[reportCallIssue, reportArgumentType]
|
|
124
|
+
model=model,
|
|
125
|
+
messages=self._convert_messages(messages),
|
|
126
|
+
tools=tools,
|
|
127
|
+
**kwargs,
|
|
128
|
+
)
|
|
129
|
+
choice = response.choices[0]
|
|
130
|
+
return Response(
|
|
131
|
+
content=choice.message.content or "",
|
|
132
|
+
model=response.model,
|
|
133
|
+
reasoning_content=self._extract_reasoning(choice.message),
|
|
134
|
+
usage=self._extract_usage(response),
|
|
135
|
+
finish_reason=choice.finish_reason,
|
|
136
|
+
tool_calls=self._extract_tool_calls(choice.message),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def achat_stream(
|
|
140
|
+
self,
|
|
141
|
+
messages: list[Message],
|
|
142
|
+
*,
|
|
143
|
+
model: str,
|
|
144
|
+
tools: list[dict] | None = None,
|
|
145
|
+
**kwargs,
|
|
146
|
+
) -> AsyncIterator[StreamChunk]:
|
|
147
|
+
if self.async_client is None:
|
|
148
|
+
raise RuntimeError("async_client not provided; cannot run async methods")
|
|
149
|
+
reasoning_parts: list[str] = []
|
|
150
|
+
accumulator = _ToolCallAccumulator()
|
|
151
|
+
text_acc = ""
|
|
152
|
+
usage = None
|
|
153
|
+
final_finish_reason: str | None = None
|
|
154
|
+
stream = await self.async_client.chat.completions.create( # pyright: ignore[reportCallIssue, reportArgumentType]
|
|
155
|
+
model=model,
|
|
156
|
+
messages=self._convert_messages(messages),
|
|
157
|
+
tools=tools,
|
|
158
|
+
stream=True,
|
|
159
|
+
**kwargs,
|
|
160
|
+
)
|
|
161
|
+
async for chunk in stream:
|
|
162
|
+
chunk_usage = self._extract_usage(chunk)
|
|
163
|
+
if chunk_usage:
|
|
164
|
+
usage = chunk_usage
|
|
165
|
+
if not chunk.choices:
|
|
166
|
+
continue # usage-only 末块(choices 为空)——usage 已捕获,流式结束
|
|
167
|
+
choice = chunk.choices[0]
|
|
168
|
+
if choice.finish_reason:
|
|
169
|
+
final_finish_reason = choice.finish_reason
|
|
170
|
+
delta = choice.delta
|
|
171
|
+
accumulator.add(delta)
|
|
172
|
+
if getattr(delta, "reasoning_content", None):
|
|
173
|
+
reasoning_parts.append(delta.reasoning_content)
|
|
174
|
+
if getattr(delta, "content", None):
|
|
175
|
+
text_acc += delta.content
|
|
176
|
+
yield StreamChunk(
|
|
177
|
+
content=delta.content, finish_reason=choice.finish_reason
|
|
178
|
+
)
|
|
179
|
+
tool_calls = accumulator.finish()
|
|
180
|
+
reasoning = "".join(reasoning_parts) if reasoning_parts else None
|
|
181
|
+
final_response = Response(
|
|
182
|
+
content=text_acc,
|
|
183
|
+
model=model,
|
|
184
|
+
tool_calls=tool_calls,
|
|
185
|
+
reasoning_content=reasoning,
|
|
186
|
+
usage=usage,
|
|
187
|
+
finish_reason=final_finish_reason,
|
|
188
|
+
)
|
|
189
|
+
yield StreamChunk(
|
|
190
|
+
content="",
|
|
191
|
+
tool_calls=tool_calls,
|
|
192
|
+
usage=usage,
|
|
193
|
+
finish_reason=final_finish_reason,
|
|
194
|
+
metadata={"reasoning_content": reasoning} if reasoning else None,
|
|
195
|
+
response=final_response,
|
|
196
|
+
)
|