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.
Files changed (141) hide show
  1. package/README.md +318 -0
  2. package/package.json +45 -0
  3. package/pyproject.toml +50 -0
  4. package/src/my_agent_core/__init__.py +123 -0
  5. package/src/my_agent_core/agent.py +441 -0
  6. package/src/my_agent_core/background.py +121 -0
  7. package/src/my_agent_core/context.py +505 -0
  8. package/src/my_agent_core/events.py +153 -0
  9. package/src/my_agent_core/extensions/__init__.py +9 -0
  10. package/src/my_agent_core/extensions/core.py +197 -0
  11. package/src/my_agent_core/hooks.py +130 -0
  12. package/src/my_agent_core/loop.py +709 -0
  13. package/src/my_agent_core/main.py +134 -0
  14. package/src/my_agent_core/memory.py +241 -0
  15. package/src/my_agent_core/message_queue.py +110 -0
  16. package/src/my_agent_core/plugins.py +212 -0
  17. package/src/my_agent_core/registry.py +186 -0
  18. package/src/my_agent_core/session/__init__.py +79 -0
  19. package/src/my_agent_core/session/entries.py +197 -0
  20. package/src/my_agent_core/session/jsonl.py +60 -0
  21. package/src/my_agent_core/session/memory.py +137 -0
  22. package/src/my_agent_core/session/session.py +400 -0
  23. package/src/my_agent_core/session/storage.py +245 -0
  24. package/src/my_agent_core/session/store.py +131 -0
  25. package/src/my_agent_core/session/tree.py +86 -0
  26. package/src/my_agent_core/skills.py +149 -0
  27. package/src/my_agent_core/subagent_tasks.py +170 -0
  28. package/src/my_agent_core/subagents.py +148 -0
  29. package/src/my_agent_core/task_store.py +248 -0
  30. package/src/my_agent_core/tool_history.py +189 -0
  31. package/src/my_agent_core/tools/__init__.py +5 -0
  32. package/src/my_agent_core/tools/builtin/__init__.py +5 -0
  33. package/src/my_agent_core/tools/builtin/task.py +30 -0
  34. package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
  35. package/src/my_agent_core/tools/core.py +239 -0
  36. package/src/my_agent_llm/__init__.py +45 -0
  37. package/src/my_agent_llm/auth/__init__.py +46 -0
  38. package/src/my_agent_llm/auth/antigravity.py +209 -0
  39. package/src/my_agent_llm/auth/manager.py +259 -0
  40. package/src/my_agent_llm/auth/quota.py +56 -0
  41. package/src/my_agent_llm/auth/schema.py +94 -0
  42. package/src/my_agent_llm/client.py +116 -0
  43. package/src/my_agent_llm/config.py +17 -0
  44. package/src/my_agent_llm/events.py +84 -0
  45. package/src/my_agent_llm/models.py +195 -0
  46. package/src/my_agent_llm/providers/__init__.py +4 -0
  47. package/src/my_agent_llm/providers/_base.py +94 -0
  48. package/src/my_agent_llm/providers/anthropic.py +298 -0
  49. package/src/my_agent_llm/providers/antigravity.py +480 -0
  50. package/src/my_agent_llm/providers/deepseek.py +196 -0
  51. package/src/my_agent_llm/providers/openai.py +364 -0
  52. package/src/my_agent_llm/providers/registry.py +16 -0
  53. package/src/my_agent_llm/stream.py +218 -0
  54. package/src/my_coding_agent/__init__.py +66 -0
  55. package/src/my_coding_agent/agent.py +208 -0
  56. package/src/my_coding_agent/cli.py +78 -0
  57. package/src/my_coding_agent/file_reference.py +80 -0
  58. package/src/my_coding_agent/macro.py +408 -0
  59. package/src/my_coding_agent/mcp.py +243 -0
  60. package/src/my_coding_agent/mutation_queue.py +37 -0
  61. package/src/my_coding_agent/paths.py +119 -0
  62. package/src/my_coding_agent/permissions.py +84 -0
  63. package/src/my_coding_agent/prompt.py +54 -0
  64. package/src/my_coding_agent/rpc_server.py +2817 -0
  65. package/src/my_coding_agent/settings.py +126 -0
  66. package/src/my_coding_agent/tools/__init__.py +55 -0
  67. package/src/my_coding_agent/tools/base.py +58 -0
  68. package/src/my_coding_agent/tools/bash.py +206 -0
  69. package/src/my_coding_agent/tools/edit.py +226 -0
  70. package/src/my_coding_agent/tools/find.py +118 -0
  71. package/src/my_coding_agent/tools/grep.py +177 -0
  72. package/src/my_coding_agent/tools/ls.py +112 -0
  73. package/src/my_coding_agent/tools/read.py +113 -0
  74. package/src/my_coding_agent/tools/write.py +72 -0
  75. package/tui/README.md +27 -0
  76. package/tui/bin/my-agent.js +98 -0
  77. package/tui/dist/app.d.ts +41 -0
  78. package/tui/dist/app.js +110 -0
  79. package/tui/dist/bridge/event-translator.d.ts +92 -0
  80. package/tui/dist/bridge/event-translator.js +216 -0
  81. package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
  82. package/tui/dist/bridge/kernel-bridge.js +132 -0
  83. package/tui/dist/client.d.ts +63 -0
  84. package/tui/dist/client.js +239 -0
  85. package/tui/dist/components/assistant-message.d.ts +19 -0
  86. package/tui/dist/components/assistant-message.js +90 -0
  87. package/tui/dist/components/compaction-summary-message.d.ts +19 -0
  88. package/tui/dist/components/compaction-summary-message.js +46 -0
  89. package/tui/dist/components/custom-editor.d.ts +18 -0
  90. package/tui/dist/components/custom-editor.js +56 -0
  91. package/tui/dist/components/dynamic-border.d.ts +9 -0
  92. package/tui/dist/components/dynamic-border.js +14 -0
  93. package/tui/dist/components/footer.d.ts +39 -0
  94. package/tui/dist/components/footer.js +199 -0
  95. package/tui/dist/components/header.d.ts +4 -0
  96. package/tui/dist/components/header.js +21 -0
  97. package/tui/dist/components/keys.d.ts +5 -0
  98. package/tui/dist/components/keys.js +12 -0
  99. package/tui/dist/components/login-selector.d.ts +26 -0
  100. package/tui/dist/components/login-selector.js +181 -0
  101. package/tui/dist/components/logout-selector.d.ts +19 -0
  102. package/tui/dist/components/logout-selector.js +88 -0
  103. package/tui/dist/components/model-selector.d.ts +40 -0
  104. package/tui/dist/components/model-selector.js +268 -0
  105. package/tui/dist/components/session-selector.d.ts +54 -0
  106. package/tui/dist/components/session-selector.js +393 -0
  107. package/tui/dist/components/settings-selector.d.ts +24 -0
  108. package/tui/dist/components/settings-selector.js +146 -0
  109. package/tui/dist/components/status-indicator.d.ts +25 -0
  110. package/tui/dist/components/status-indicator.js +60 -0
  111. package/tui/dist/components/theme-selector.d.ts +14 -0
  112. package/tui/dist/components/theme-selector.js +77 -0
  113. package/tui/dist/components/thinking-selector.d.ts +21 -0
  114. package/tui/dist/components/thinking-selector.js +128 -0
  115. package/tui/dist/components/tool-execution.d.ts +31 -0
  116. package/tui/dist/components/tool-execution.js +206 -0
  117. package/tui/dist/components/tree-selector.d.ts +40 -0
  118. package/tui/dist/components/tree-selector.js +173 -0
  119. package/tui/dist/components/user-message-selector.d.ts +21 -0
  120. package/tui/dist/components/user-message-selector.js +103 -0
  121. package/tui/dist/components/user-message.d.ts +5 -0
  122. package/tui/dist/components/user-message.js +15 -0
  123. package/tui/dist/index.d.ts +11 -0
  124. package/tui/dist/index.js +11 -0
  125. package/tui/dist/interactive/chat-viewport.d.ts +19 -0
  126. package/tui/dist/interactive/chat-viewport.js +41 -0
  127. package/tui/dist/interactive/components.d.ts +1 -0
  128. package/tui/dist/interactive/components.js +1 -0
  129. package/tui/dist/interactive/interactive-mode.d.ts +89 -0
  130. package/tui/dist/interactive/interactive-mode.js +1625 -0
  131. package/tui/dist/interactive/theme.d.ts +1 -0
  132. package/tui/dist/interactive/theme.js +1 -0
  133. package/tui/dist/interactive/tui-renderer.d.ts +8 -0
  134. package/tui/dist/interactive/tui-renderer.js +10 -0
  135. package/tui/dist/protocol.d.ts +78 -0
  136. package/tui/dist/protocol.js +1 -0
  137. package/tui/dist/theme/dark.json +54 -0
  138. package/tui/dist/theme/light.json +71 -0
  139. package/tui/dist/theme/theme.d.ts +20 -0
  140. package/tui/dist/theme/theme.js +86 -0
  141. 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
+ )