tau-coding-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. tau/__init__.py +0 -0
  2. tau/agent/__init__.py +11 -0
  3. tau/agent/prompt/__init__.py +10 -0
  4. tau/agent/prompt/builder.py +302 -0
  5. tau/agent/prompt/types.py +33 -0
  6. tau/agent/service.py +369 -0
  7. tau/agent/types.py +61 -0
  8. tau/auth/manager.py +247 -0
  9. tau/auth/storage.py +82 -0
  10. tau/auth/types.py +41 -0
  11. tau/builtins/__init__.py +4 -0
  12. tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
  13. tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
  14. tau/builtins/commands/__init__.py +41 -0
  15. tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  16. tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  17. tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
  18. tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
  19. tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
  20. tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
  21. tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
  22. tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
  23. tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
  24. tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
  25. tau/builtins/commands/clear.py +16 -0
  26. tau/builtins/commands/compact.py +28 -0
  27. tau/builtins/commands/reload.py +27 -0
  28. tau/builtins/commands/session.py +19 -0
  29. tau/builtins/extensions/footer/__init__.py +76 -0
  30. tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
  31. tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
  32. tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
  33. tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
  34. tau/builtins/extensions/footer/git.py +26 -0
  35. tau/builtins/extensions/footer/model.py +69 -0
  36. tau/builtins/extensions/footer/utils.py +44 -0
  37. tau/builtins/extensions/header/__init__.py +18 -0
  38. tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
  39. tau/builtins/models/__init__.py +0 -0
  40. tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
  41. tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
  42. tau/builtins/models/audio.py +43 -0
  43. tau/builtins/models/image.py +43 -0
  44. tau/builtins/models/text.py +482 -0
  45. tau/builtins/models/video.py +40 -0
  46. tau/builtins/prompts/commit.md +7 -0
  47. tau/builtins/prompts/docs.md +7 -0
  48. tau/builtins/prompts/explain.md +7 -0
  49. tau/builtins/prompts/fix.md +7 -0
  50. tau/builtins/prompts/refactor.md +7 -0
  51. tau/builtins/prompts/review.md +7 -0
  52. tau/builtins/prompts/test.md +7 -0
  53. tau/builtins/providers/__init__.py +0 -0
  54. tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  55. tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
  56. tau/builtins/providers/audio.py +10 -0
  57. tau/builtins/providers/image.py +9 -0
  58. tau/builtins/providers/text.py +33 -0
  59. tau/builtins/providers/video.py +6 -0
  60. tau/builtins/skills/code-review/SKILL.md +4 -0
  61. tau/builtins/skills/debug/SKILL.md +4 -0
  62. tau/builtins/skills/git-commit/SKILL.md +4 -0
  63. tau/builtins/themes/dark.yaml +1 -0
  64. tau/builtins/themes/light.yaml +46 -0
  65. tau/builtins/tools/__init__.py +73 -0
  66. tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  67. tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  68. tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
  69. tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
  70. tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
  71. tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
  72. tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
  73. tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
  74. tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
  75. tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
  76. tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
  77. tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
  78. tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
  79. tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
  80. tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
  81. tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
  82. tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
  83. tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
  84. tau/builtins/tools/edit.py +215 -0
  85. tau/builtins/tools/glob.py +112 -0
  86. tau/builtins/tools/grep.py +146 -0
  87. tau/builtins/tools/ls.py +135 -0
  88. tau/builtins/tools/read.py +122 -0
  89. tau/builtins/tools/terminal.py +150 -0
  90. tau/builtins/tools/write.py +105 -0
  91. tau/commands/__init__.py +10 -0
  92. tau/commands/registry.py +71 -0
  93. tau/commands/types.py +33 -0
  94. tau/console/__init__.py +0 -0
  95. tau/console/cli.py +266 -0
  96. tau/console/commands/__init__.py +0 -0
  97. tau/console/commands/auth.py +193 -0
  98. tau/console/commands/packages.py +104 -0
  99. tau/console/commands/update.py +76 -0
  100. tau/core/__init__.py +0 -0
  101. tau/core/registry.py +102 -0
  102. tau/engine/__init__.py +47 -0
  103. tau/engine/service.py +768 -0
  104. tau/engine/types.py +163 -0
  105. tau/extensions/__init__.py +28 -0
  106. tau/extensions/api.py +928 -0
  107. tau/extensions/context.py +462 -0
  108. tau/extensions/events.py +70 -0
  109. tau/extensions/loader.py +386 -0
  110. tau/extensions/runtime.py +184 -0
  111. tau/extensions/settings.py +137 -0
  112. tau/hooks/__init__.py +112 -0
  113. tau/hooks/engine.py +237 -0
  114. tau/hooks/inference.py +21 -0
  115. tau/hooks/runtime.py +126 -0
  116. tau/hooks/service.py +121 -0
  117. tau/hooks/session.py +117 -0
  118. tau/hooks/tui.py +61 -0
  119. tau/hooks/types.py +72 -0
  120. tau/inference/__init__.py +180 -0
  121. tau/inference/api/__init__.py +0 -0
  122. tau/inference/api/audio/__init__.py +0 -0
  123. tau/inference/api/audio/base.py +29 -0
  124. tau/inference/api/audio/builtins.py +15 -0
  125. tau/inference/api/audio/elevenlabs_audio.py +183 -0
  126. tau/inference/api/audio/gemini_audio.py +95 -0
  127. tau/inference/api/audio/openai_audio.py +159 -0
  128. tau/inference/api/audio/registry.py +15 -0
  129. tau/inference/api/audio/sarvam_audio.py +163 -0
  130. tau/inference/api/audio/service.py +103 -0
  131. tau/inference/api/audio/utils.py +47 -0
  132. tau/inference/api/image/__init__.py +0 -0
  133. tau/inference/api/image/base.py +17 -0
  134. tau/inference/api/image/builtins.py +8 -0
  135. tau/inference/api/image/gemini_image.py +77 -0
  136. tau/inference/api/image/openai_image.py +103 -0
  137. tau/inference/api/image/openrouter.py +144 -0
  138. tau/inference/api/image/registry.py +15 -0
  139. tau/inference/api/image/service.py +71 -0
  140. tau/inference/api/registry.py +82 -0
  141. tau/inference/api/text/__init__.py +0 -0
  142. tau/inference/api/text/anthropic_claude_code.py +222 -0
  143. tau/inference/api/text/anthropic_messages.py +196 -0
  144. tau/inference/api/text/base.py +40 -0
  145. tau/inference/api/text/builtins.py +19 -0
  146. tau/inference/api/text/gemini_generate.py +234 -0
  147. tau/inference/api/text/github_copilot_chat.py +172 -0
  148. tau/inference/api/text/google_antigravity.py +522 -0
  149. tau/inference/api/text/mistral_chat.py +284 -0
  150. tau/inference/api/text/ollama_chat.py +200 -0
  151. tau/inference/api/text/openai_codex_responses.py +497 -0
  152. tau/inference/api/text/openai_completions.py +227 -0
  153. tau/inference/api/text/openai_responses.py +235 -0
  154. tau/inference/api/text/registry.py +50 -0
  155. tau/inference/api/text/service.py +297 -0
  156. tau/inference/api/text/types.py +7 -0
  157. tau/inference/api/text/utils.py +228 -0
  158. tau/inference/api/video/__init__.py +0 -0
  159. tau/inference/api/video/base.py +26 -0
  160. tau/inference/api/video/builtins.py +7 -0
  161. tau/inference/api/video/fal_video.py +119 -0
  162. tau/inference/api/video/openrouter_video.py +142 -0
  163. tau/inference/api/video/registry.py +15 -0
  164. tau/inference/api/video/service.py +72 -0
  165. tau/inference/model/__init__.py +0 -0
  166. tau/inference/model/registry.py +102 -0
  167. tau/inference/model/types.py +65 -0
  168. tau/inference/provider/__init__.py +0 -0
  169. tau/inference/provider/oauth/__init__.py +35 -0
  170. tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
  171. tau/inference/provider/oauth/github_copilot.py +333 -0
  172. tau/inference/provider/oauth/google_antigravity.py +258 -0
  173. tau/inference/provider/oauth/openai_codex.py +309 -0
  174. tau/inference/provider/oauth/pkce.py +14 -0
  175. tau/inference/provider/oauth/types.py +46 -0
  176. tau/inference/provider/oauth/utils.py +154 -0
  177. tau/inference/provider/registry.py +141 -0
  178. tau/inference/provider/types.py +114 -0
  179. tau/inference/types.py +549 -0
  180. tau/inference/utils.py +219 -0
  181. tau/message/__init__.py +0 -0
  182. tau/message/types.py +482 -0
  183. tau/message/utils.py +178 -0
  184. tau/packages/__init__.py +11 -0
  185. tau/packages/manager.py +190 -0
  186. tau/packages/types.py +20 -0
  187. tau/packages/utils.py +67 -0
  188. tau/prompts/expand.py +58 -0
  189. tau/prompts/loader.py +69 -0
  190. tau/prompts/registry.py +45 -0
  191. tau/prompts/types.py +24 -0
  192. tau/rpc/__init__.py +8 -0
  193. tau/rpc/mode.py +783 -0
  194. tau/rpc/types.py +252 -0
  195. tau/runtime/service.py +759 -0
  196. tau/runtime/types.py +303 -0
  197. tau/session/branch_summarization.py +312 -0
  198. tau/session/compaction.py +646 -0
  199. tau/session/manager.py +652 -0
  200. tau/session/types.py +188 -0
  201. tau/session/utils.py +233 -0
  202. tau/settings/manager.py +1077 -0
  203. tau/settings/paths.py +150 -0
  204. tau/settings/storage.py +63 -0
  205. tau/settings/types.py +173 -0
  206. tau/settings/utils.py +25 -0
  207. tau/skills/loader.py +91 -0
  208. tau/skills/registry.py +70 -0
  209. tau/skills/types.py +25 -0
  210. tau/themes/loader.py +238 -0
  211. tau/themes/registry.py +108 -0
  212. tau/themes/types.py +19 -0
  213. tau/tool/__init__.py +3 -0
  214. tau/tool/registry.py +117 -0
  215. tau/tool/render.py +21 -0
  216. tau/tool/types.py +244 -0
  217. tau/trust/__init__.py +13 -0
  218. tau/trust/manager.py +80 -0
  219. tau/trust/types.py +14 -0
  220. tau/trust/utils.py +72 -0
  221. tau/tui/__init__.py +54 -0
  222. tau/tui/agent_hooks.py +346 -0
  223. tau/tui/ansi.py +330 -0
  224. tau/tui/app.py +540 -0
  225. tau/tui/autocomplete.py +33 -0
  226. tau/tui/capabilities.py +119 -0
  227. tau/tui/commands/__init__.py +3 -0
  228. tau/tui/commands/appearance.py +498 -0
  229. tau/tui/commands/auth.py +232 -0
  230. tau/tui/commands/context.py +38 -0
  231. tau/tui/commands/misc.py +82 -0
  232. tau/tui/commands/model.py +118 -0
  233. tau/tui/commands/session.py +464 -0
  234. tau/tui/component.py +268 -0
  235. tau/tui/components/__init__.py +0 -0
  236. tau/tui/components/autocomplete_manager.py +267 -0
  237. tau/tui/components/autocomplete_picker.py +143 -0
  238. tau/tui/components/box.py +90 -0
  239. tau/tui/components/command_palette.py +144 -0
  240. tau/tui/components/dynamic_border.py +19 -0
  241. tau/tui/components/file_picker.py +233 -0
  242. tau/tui/components/image.py +181 -0
  243. tau/tui/components/inline_selector.py +71 -0
  244. tau/tui/components/layout.py +1194 -0
  245. tau/tui/components/message_list.py +692 -0
  246. tau/tui/components/modal.py +97 -0
  247. tau/tui/components/model_palette.py +204 -0
  248. tau/tui/components/picker_overlay.py +174 -0
  249. tau/tui/components/prompt_overlay.py +236 -0
  250. tau/tui/components/resume_modal.py +372 -0
  251. tau/tui/components/select_list.py +222 -0
  252. tau/tui/components/settings_modal.py +274 -0
  253. tau/tui/components/settings_schema.py +203 -0
  254. tau/tui/components/spinner.py +119 -0
  255. tau/tui/components/text_input.py +396 -0
  256. tau/tui/components/text_prompt.py +82 -0
  257. tau/tui/components/tree_select_list.py +580 -0
  258. tau/tui/components/trust_screen.py +97 -0
  259. tau/tui/diff.py +114 -0
  260. tau/tui/fuzzy.py +99 -0
  261. tau/tui/input.py +496 -0
  262. tau/tui/input_handler.py +716 -0
  263. tau/tui/keybindings.py +87 -0
  264. tau/tui/markdown.py +286 -0
  265. tau/tui/message_renderers.py +31 -0
  266. tau/tui/overlay.py +326 -0
  267. tau/tui/renderer.py +378 -0
  268. tau/tui/terminal.py +499 -0
  269. tau/tui/theme.py +148 -0
  270. tau/tui/tui.py +544 -0
  271. tau/tui/ui_context.py +768 -0
  272. tau/tui/utils.py +20 -0
  273. tau/utils/__init__.py +0 -0
  274. tau/utils/http_proxy.py +221 -0
  275. tau/utils/image_processing.py +172 -0
  276. tau/utils/secrets.py +59 -0
  277. tau/utils/version_check.py +60 -0
  278. tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
  279. tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
  280. tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
  281. tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  282. tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  283. tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,234 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from collections.abc import AsyncGenerator, AsyncIterator
4
+ from typing import Any
5
+ from google import genai
6
+ from google.genai import types as genai_types
7
+ from tau.inference.api.text.base import BaseLLMAPI as BaseAPI
8
+ from tau.inference.model.types import Model
9
+ from tau.inference.types import (
10
+ LLMContext, LLMEvent, LLMOptions, StopReason, ThinkingBudgets, ThinkingLevel,
11
+ StartEvent, EndEvent, ErrorEvent,
12
+ TextStartEvent, TextDeltaEvent, TextEndEvent,
13
+ ThinkingStartEvent, ThinkingDeltaEvent, ThinkingEndEvent,
14
+ ToolCallStartEvent, ToolCallDeltaEvent, ToolCallEndEvent,
15
+ normalize_structured_response_format,
16
+ )
17
+ from tau.message.types import (
18
+ SystemMessage, UserMessage, AssistantMessage, ToolMessage,
19
+ TextContent, ImageContent, ThinkingContent, ToolCallContent, ToolResultContent,
20
+ )
21
+ from typing import Optional, TYPE_CHECKING
22
+ if TYPE_CHECKING:
23
+ from tau.tool.types import Tool
24
+
25
+ _STOP_REASON: dict[str, StopReason] = {
26
+ "STOP": StopReason.Stop,
27
+ "MAX_TOKENS": StopReason.Length,
28
+ "SAFETY": StopReason.ContentFilter,
29
+ "RECITATION": StopReason.ContentFilter,
30
+ }
31
+
32
+
33
+ def _messages_to_gemini(
34
+ messages: list[LLMMessage],
35
+ ) -> tuple[str | None, list[genai_types.Content]]:
36
+ system: str | None = None
37
+ contents: list[genai_types.Content] = []
38
+
39
+ for msg in messages:
40
+ match msg:
41
+ case SystemMessage():
42
+ system = "\n".join(c.content for c in msg.contents if isinstance(c, TextContent))
43
+ case UserMessage():
44
+ parts: list[genai_types.Part] = []
45
+ for item in msg.contents:
46
+ match item:
47
+ case TextContent():
48
+ parts.append(genai_types.Part(text=item.content))
49
+ case ImageContent():
50
+ for b64, mime in item.to_base64():
51
+ parts.append(genai_types.Part(
52
+ inline_data=genai_types.Blob(mime_type=mime or "image/png", data=b64),
53
+ ))
54
+ if parts:
55
+ contents.append(genai_types.Content(role="user", parts=parts))
56
+ case AssistantMessage():
57
+ parts = []
58
+ for item in msg.contents:
59
+ match item:
60
+ case TextContent():
61
+ parts.append(genai_types.Part(text=item.content))
62
+ case ToolCallContent():
63
+ parts.append(genai_types.Part(
64
+ function_call=genai_types.FunctionCall(
65
+ name=item.name,
66
+ args=item.args,
67
+ ),
68
+ ))
69
+ if parts:
70
+ contents.append(genai_types.Content(role="model", parts=parts))
71
+ case ToolMessage():
72
+ parts = []
73
+ for content in msg.contents:
74
+ if isinstance(content, ToolResultContent):
75
+ parts.append(genai_types.Part(
76
+ function_response=genai_types.FunctionResponse(
77
+ name=content.id,
78
+ response={"result": content.content},
79
+ ),
80
+ ))
81
+ if parts:
82
+ contents.append(genai_types.Content(role="user", parts=parts))
83
+
84
+ return system, contents
85
+
86
+
87
+ def _response_schema(response_format: Any | None) -> dict[str, Any] | None:
88
+ structured = normalize_structured_response_format(response_format)
89
+ return structured.schema if structured is not None else None
90
+
91
+
92
+ class GeminiGenerateAPI(BaseAPI):
93
+ def __init__(self, options: LLMOptions) -> None:
94
+ super().__init__(options)
95
+ self._client = genai.Client(api_key=options.api_key)
96
+
97
+ def _build_config(
98
+ self,
99
+ tools: Optional[list[Tool]] = None,
100
+ response_format: Any | None = None,
101
+ ) -> genai_types.GenerateContentConfig:
102
+ params: dict[str, Any] = {
103
+ "temperature": self.options.temperature,
104
+ }
105
+ if self.options.max_tokens is not None:
106
+ params["max_output_tokens"] = self.options.max_tokens
107
+ schema = _response_schema(response_format)
108
+ if schema is not None:
109
+ params["response_mime_type"] = "application/json"
110
+ params["response_schema"] = schema
111
+
112
+ budget = None
113
+ if self.options.thinking_level is not None and self.options.thinking_level != ThinkingLevel.Off:
114
+ budgets = self.options.thinking_budgets or ThinkingBudgets()
115
+ budget = budgets.get(self.options.thinking_level)
116
+ if budget is not None:
117
+ params["thinking_config"] = genai_types.ThinkingConfig(
118
+ thinking_budget=budget,
119
+ include_thoughts=True,
120
+ )
121
+
122
+ if tools:
123
+ params["tools"] = [
124
+ genai_types.Tool(
125
+ function_declarations=[
126
+ genai_types.FunctionDeclaration(
127
+ name=t.name,
128
+ description=t.description,
129
+ parameters=t.schema.model_json_schema(),
130
+ )
131
+ for t in tools
132
+ ]
133
+ )
134
+ ]
135
+
136
+ return genai_types.GenerateContentConfig(**params)
137
+
138
+ async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
139
+ system, contents = _messages_to_gemini(context.messages)
140
+ config = self._build_config(
141
+ tools=context.tools or None,
142
+ response_format=context.response_format,
143
+ )
144
+ effective_system = context.system_prompt or system
145
+ if effective_system:
146
+ config.system_instruction = effective_system
147
+
148
+ if self.options.on_payload:
149
+ payload = {"config": config, "contents": contents}
150
+ modified = self.options.on_payload(payload)
151
+ if modified is not None:
152
+ config = modified.get("config", config)
153
+ contents = modified.get("contents", contents)
154
+
155
+ text_index = 0
156
+ thinking_index = 0
157
+ tool_index = 0
158
+ text_started = False
159
+ thinking_started = False
160
+ text_buf = ""
161
+ thinking_buf = ""
162
+ _input_tokens = 0
163
+ _output_tokens = 0
164
+ _cache_read_tokens = 0
165
+
166
+ yield StartEvent()
167
+
168
+ try:
169
+ async for chunk in await self._client.aio.models.generate_content_stream(
170
+ model=model.id,
171
+ contents=contents,
172
+ config=config,
173
+ ):
174
+ if self._cancelled():
175
+ yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
176
+ return
177
+ um = getattr(chunk, 'usage_metadata', None)
178
+ if um:
179
+ _input_tokens = getattr(um, 'prompt_token_count', 0) or 0
180
+ _output_tokens = getattr(um, 'candidates_token_count', 0) or 0
181
+ _cache_read_tokens = getattr(um, 'cached_content_token_count', 0) or 0
182
+
183
+ if not chunk.candidates:
184
+ continue
185
+
186
+ candidate = chunk.candidates[0]
187
+ if candidate.content and candidate.content.parts:
188
+ for part in candidate.content.parts:
189
+ if getattr(part, "thought", False) and part.text:
190
+ if not thinking_started:
191
+ yield ThinkingStartEvent(thinking=None)
192
+ thinking_started = True
193
+ thinking_buf += part.text
194
+ yield ThinkingDeltaEvent(thinking=ThinkingContent(content=part.text))
195
+ elif part.text:
196
+ if thinking_started:
197
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
198
+ thinking_started = False
199
+ thinking_index += 1
200
+ thinking_buf = ""
201
+ if not text_started:
202
+ yield TextStartEvent(text=TextContent(content=""))
203
+ text_started = True
204
+ text_buf += part.text
205
+ yield TextDeltaEvent(text=TextContent(content=part.text))
206
+ elif part.function_call:
207
+ fc = part.function_call
208
+ tool_id = fc.name
209
+ args_str = json.dumps(dict(fc.args)) if fc.args else ""
210
+ yield ToolCallStartEvent(tool_call=ToolCallContent(id=tool_id, name=fc.name))
211
+ yield ToolCallDeltaEvent(tool_call=ToolCallContent(id=tool_id))
212
+ yield ToolCallEndEvent(tool_call=ToolCallContent(id=tool_id, name=fc.name, args=json.loads(args_str) if args_str else {}))
213
+ tool_index += 1
214
+
215
+ finish_reason = getattr(candidate, "finish_reason", None)
216
+ if finish_reason and str(finish_reason) not in ("", "FINISH_REASON_UNSPECIFIED"):
217
+ if thinking_started:
218
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
219
+ if text_started:
220
+ yield TextEndEvent(text=TextContent(content=text_buf))
221
+ reason_str = finish_reason.name if hasattr(finish_reason, "name") else str(finish_reason)
222
+ stop = StopReason.ToolCalls if tool_index > 0 else _STOP_REASON.get(reason_str, StopReason.Stop)
223
+ yield EndEvent(reason=stop, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)
224
+ return
225
+
226
+ except Exception as exc:
227
+ yield ErrorEvent(reason=StopReason.Abort, error=str(exc))
228
+ return
229
+
230
+ if thinking_started:
231
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
232
+ if text_started:
233
+ yield TextEndEvent(text=TextContent(content=text_buf))
234
+ yield EndEvent(reason=StopReason.Stop, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from tau.inference.api.text.utils import parse_tool_args, openai_user_content, openai_assistant_content, openai_messages_to_chat, openai_response_format
4
+ from collections.abc import AsyncGenerator, AsyncIterator
5
+ from typing import Any
6
+ from openai import AsyncOpenAI
7
+ from tau.inference.api.text.base import BaseLLMAPI as BaseAPI
8
+ from tau.inference.model.types import Model
9
+ from tau.inference.provider.oauth.github_copilot import get_copilot_base_url
10
+ from tau.inference.types import (
11
+ LLMContext, LLMEvent, LLMOptions, StopReason, ThinkingLevel,
12
+ StartEvent, EndEvent, ErrorEvent,
13
+ TextStartEvent, TextDeltaEvent, TextEndEvent,
14
+ ToolCallStartEvent, ToolCallDeltaEvent, ToolCallEndEvent,
15
+ )
16
+ from tau.message.types import (
17
+ SystemMessage, UserMessage, AssistantMessage, ToolMessage,
18
+ TextContent, ImageContent, ToolCallContent, ToolResultContent,
19
+ )
20
+ from typing import Optional, TYPE_CHECKING
21
+ if TYPE_CHECKING:
22
+ from tau.tool.types import Tool
23
+
24
+ _COPILOT_HEADERS = {
25
+ "User-Agent": "GitHubCopilotChat/0.35.0",
26
+ "Editor-Version": "vscode/1.107.0",
27
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
28
+ "Copilot-Integration-Id": "vscode-chat",
29
+ }
30
+
31
+ _STOP_REASON: dict[str, StopReason] = {
32
+ "stop": StopReason.Stop,
33
+ "length": StopReason.Length,
34
+ "tool_calls": StopReason.ToolCalls,
35
+ "content_filter": StopReason.ContentFilter,
36
+ }
37
+
38
+
39
+
40
+ class GitHubCopilotChatAPI(BaseAPI):
41
+ """Streaming LLM API adapter for the GitHub Copilot Chat endpoint (OpenAI-compatible)."""
42
+
43
+ def __init__(self, options: LLMOptions) -> None:
44
+ """Resolve the Copilot base URL and initialise the AsyncOpenAI client with Copilot headers."""
45
+ super().__init__(options)
46
+ base_url = options.base_url or get_copilot_base_url(options.api_key)
47
+ self._client = AsyncOpenAI(
48
+ api_key=options.api_key or "github-copilot",
49
+ base_url=base_url,
50
+ default_headers={**_COPILOT_HEADERS, **(options.headers or {})},
51
+ max_retries=options.max_retries,
52
+ timeout=options.timeout.total_seconds(),
53
+ )
54
+
55
+ def _build_params(self, model: Model, messages: list[dict[str, Any]], tools: Optional[list[Tool]] = None) -> dict[str, Any]:
56
+ """Assemble the Copilot Chat Completions request payload."""
57
+ params: dict[str, Any] = {
58
+ "model": model.id,
59
+ "messages": messages,
60
+ "temperature": self.options.temperature,
61
+ }
62
+ if self.options.max_tokens is not None:
63
+ params["max_completion_tokens"] = self.options.max_tokens
64
+
65
+ if tools:
66
+ params["tools"] = [
67
+ {
68
+ "type": "function",
69
+ "function": {
70
+ "name": tool.name,
71
+ "description": tool.description,
72
+ "parameters": tool.schema.model_json_schema(),
73
+ }
74
+ }
75
+ for tool in tools
76
+ ]
77
+ params["tool_choice"] = "auto"
78
+
79
+ return params
80
+
81
+ async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
82
+ """Stream LLMEvents from the GitHub Copilot Chat API."""
83
+ chat_messages = openai_messages_to_chat(context.messages)
84
+ if context.system_prompt:
85
+ chat_messages = [{"role": "system", "content": context.system_prompt}] + chat_messages
86
+ params = self._build_params(model, chat_messages, tools=context.tools or None)
87
+ response_format = openai_response_format(context.response_format)
88
+ if response_format is not None:
89
+ params["response_format"] = response_format
90
+
91
+ if self.options.on_payload:
92
+ modified = self.options.on_payload(params)
93
+ if modified is not None:
94
+ params = modified
95
+
96
+ text_started = False
97
+ text_buf = ""
98
+ tool_started: dict[int, bool] = {}
99
+ tool_bufs: dict[int, str] = {}
100
+ tool_meta: dict[int, dict[str, str]] = {}
101
+ _input_tokens = 0
102
+ _output_tokens = 0
103
+ _cache_read_tokens = 0
104
+
105
+ yield StartEvent()
106
+
107
+ # async with closes the SDK stream (and its httpx response) on every
108
+ # exit path — cancellation return or an upstream GeneratorExit — instead
109
+ # of leaving it to the GC asyncgen finalizer.
110
+ async with await self._client.chat.completions.create(**params, stream=True, stream_options={"include_usage": True}) as sdk_stream:
111
+ async for chunk in sdk_stream:
112
+ if self._cancelled():
113
+ yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
114
+ return
115
+ usage_data = getattr(chunk, 'usage', None)
116
+ if usage_data:
117
+ _input_tokens = getattr(usage_data, 'prompt_tokens', 0) or 0
118
+ _output_tokens = getattr(usage_data, 'completion_tokens', 0) or 0
119
+ _details = getattr(usage_data, 'prompt_tokens_details', None)
120
+ _cache_read_tokens = getattr(_details, 'cached_tokens', 0) or 0
121
+ choice = chunk.choices[0] if chunk.choices else None
122
+ if choice is None:
123
+ continue
124
+
125
+ delta = choice.delta
126
+
127
+ if delta.content:
128
+ if not text_started:
129
+ yield TextStartEvent(text=TextContent(content=""))
130
+ text_started = True
131
+ text_buf += delta.content
132
+ yield TextDeltaEvent(text=TextContent(content=delta.content))
133
+
134
+ if delta.tool_calls:
135
+ for tc in delta.tool_calls:
136
+ idx = tc.index
137
+ if idx not in tool_started:
138
+ tool_started[idx] = True
139
+ tool_bufs[idx] = ""
140
+ tool_meta[idx] = {
141
+ "id": tc.id or "",
142
+ "name": tc.function.name or "" if tc.function else "",
143
+ }
144
+ yield ToolCallStartEvent(tool_call=ToolCallContent(
145
+ id=tool_meta[idx]["id"],
146
+ name=tool_meta[idx]["name"],
147
+ ))
148
+ if tc.function and tc.function.arguments:
149
+ tool_bufs[idx] += tc.function.arguments
150
+ yield ToolCallDeltaEvent(tool_call=ToolCallContent(id=tool_meta[idx]["id"]))
151
+
152
+ if choice.finish_reason:
153
+ if text_started:
154
+ yield TextEndEvent(text=TextContent(content=text_buf))
155
+ text_started = False
156
+ text_buf = ""
157
+
158
+ for idx in sorted(tool_started):
159
+ args_str = tool_bufs[idx].strip()
160
+ args = parse_tool_args(args_str)
161
+
162
+ yield ToolCallEndEvent(tool_call=ToolCallContent(
163
+ id=tool_meta[idx]["id"],
164
+ name=tool_meta[idx]["name"],
165
+ args=args,
166
+ ))
167
+ tool_started.clear()
168
+ tool_bufs.clear()
169
+ tool_meta.clear()
170
+
171
+ stop_reason = _STOP_REASON.get(choice.finish_reason, StopReason.Stop)
172
+ yield EndEvent(reason=stop_reason, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)