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,284 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from tau.inference.api.text.utils import parse_tool_args, openai_user_content, openai_response_format
4
+ from collections.abc import AsyncGenerator, AsyncIterator
5
+ from typing import Any
6
+ from mistralai.client import Mistral
7
+ from mistralai.client.models import ThinkChunk, TextChunk
8
+ from mistralai.client.types import UNSET
9
+ from tau.inference.api.text.base import BaseLLMAPI as BaseAPI
10
+ from tau.inference.model.types import Model
11
+ from tau.inference.types import (
12
+ LLMContext, LLMEvent, LLMOptions, StopReason, ThinkingLevel,
13
+ StartEvent, EndEvent, ErrorEvent,
14
+ TextStartEvent, TextDeltaEvent, TextEndEvent,
15
+ ThinkingStartEvent, ThinkingDeltaEvent, ThinkingEndEvent,
16
+ ToolCallStartEvent, ToolCallDeltaEvent, ToolCallEndEvent,
17
+ )
18
+ from tau.message.types import (
19
+ SystemMessage, UserMessage, AssistantMessage, ToolMessage,
20
+ TextContent, ImageContent, ThinkingContent, ToolCallContent, ToolResultContent,
21
+ )
22
+ from typing import Optional, TYPE_CHECKING
23
+ if TYPE_CHECKING:
24
+ from tau.tool.types import Tool
25
+
26
+ _STOP_REASON: dict[str, StopReason] = {
27
+ "stop": StopReason.Stop,
28
+ "length": StopReason.Length,
29
+ "model_length": StopReason.Length,
30
+ "tool_calls": StopReason.ToolCalls,
31
+ "error": StopReason.Error,
32
+ }
33
+
34
+ _MINIMAL_LEVELS = {ThinkingLevel.Low, ThinkingLevel.Minimal}
35
+
36
+
37
+
38
+ def _messages_to_mistral(messages: list[LLMMessage]) -> list[dict[str, Any]]:
39
+ """Convert a message list to Mistral Chat API format, preserving thinking content blocks."""
40
+ result: list[dict[str, Any]] = []
41
+ for msg in messages:
42
+ match msg:
43
+ case SystemMessage():
44
+ text = "\n".join(c.content for c in msg.contents if isinstance(c, TextContent))
45
+ result.append({"role": "system", "content": text})
46
+ case UserMessage():
47
+ if not msg.contents:
48
+ continue
49
+ result.append({"role": "user", "content": openai_user_content(msg.contents)})
50
+ case AssistantMessage():
51
+ text_parts: list[str] = []
52
+ tool_calls: list[dict[str, Any]] = []
53
+ content_chunks: list[dict[str, Any]] = []
54
+ has_thinking = any(isinstance(c, ThinkingContent) for c in msg.contents)
55
+ for item in msg.contents:
56
+ match item:
57
+ case ThinkingContent():
58
+ content_chunks.append({
59
+ "type": "thinking",
60
+ "thinking": [{"type": "text", "text": item.content}],
61
+ "signature": item.signature,
62
+ })
63
+ case TextContent():
64
+ if has_thinking:
65
+ content_chunks.append({"type": "text", "text": item.content})
66
+ else:
67
+ text_parts.append(item.content)
68
+ case ToolCallContent():
69
+ tool_calls.append({
70
+ "id": item.id,
71
+ "type": "function",
72
+ "function": {"name": item.name, "arguments": json.dumps(item.args)},
73
+ })
74
+ entry: dict[str, Any] = {"role": "assistant"}
75
+ if has_thinking:
76
+ # When thinking blocks are present, Mistral requires chunked content format.
77
+ entry["content"] = content_chunks
78
+ else:
79
+ text = "".join(text_parts) or None
80
+ if text is not None:
81
+ entry["content"] = text
82
+ if tool_calls:
83
+ entry["tool_calls"] = tool_calls
84
+ result.append(entry)
85
+ case ToolMessage():
86
+ for content in msg.contents:
87
+ if isinstance(content, ToolResultContent):
88
+ result.append({
89
+ "role": "tool",
90
+ "tool_call_id": content.id,
91
+ "content": content.content,
92
+ })
93
+ return result
94
+
95
+
96
+ class MistralChatAPI(BaseAPI):
97
+ """Streaming LLM API adapter for the Mistral Chat API."""
98
+
99
+ def __init__(self, options: LLMOptions) -> None:
100
+ """Initialise the Mistral client and cache the initial api_key for change detection."""
101
+ super().__init__(options)
102
+ self._client_key = options.api_key
103
+ self._client = self._build_client()
104
+
105
+ def _build_client(self) -> Mistral:
106
+ """Construct a fresh Mistral SDK client from the current options."""
107
+ return Mistral(
108
+ api_key=self.options.api_key,
109
+ server_url=self.options.base_url,
110
+ timeout_ms=int(self.options.timeout.total_seconds() * 1000),
111
+ )
112
+
113
+ def _sync_client(self) -> None:
114
+ """Rebuild the Mistral client if the api_key has changed since construction."""
115
+ # The api_key is resolved and assigned to options *after* __init__,
116
+ # so rebuild the client whenever it changes.
117
+ if self.options.api_key != self._client_key:
118
+ self._client_key = self.options.api_key
119
+ self._client = self._build_client()
120
+
121
+ async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
122
+ """Stream LLMEvents from the Mistral Chat API."""
123
+ self._sync_client()
124
+ mistral_messages = _messages_to_mistral(context.messages)
125
+ if context.system_prompt:
126
+ mistral_messages = [{"role": "system", "content": context.system_prompt}] + mistral_messages
127
+
128
+ reasoning_effort = None
129
+ if self.options.thinking_level is not None:
130
+ reasoning_effort = "none" if self.options.thinking_level in _MINIMAL_LEVELS else "high"
131
+
132
+ text_started = False
133
+ text_buf = ""
134
+ text_index = 0
135
+ thinking_started = False
136
+ thinking_buf = ""
137
+ thinking_index = 0
138
+ tool_index = 0
139
+ tool_started: dict[int, bool] = {}
140
+ tool_bufs: dict[int, str] = {}
141
+ tool_meta: dict[int, dict[str, str]] = {}
142
+ _input_tokens = 0
143
+ _output_tokens = 0
144
+ _cache_read_tokens = 0
145
+
146
+ yield StartEvent()
147
+
148
+ try:
149
+ kwargs: dict[str, Any] = {
150
+ "model": model.id,
151
+ "messages": mistral_messages,
152
+ "temperature": self.options.temperature,
153
+ "max_tokens": self.options.max_tokens,
154
+ }
155
+ if reasoning_effort is not None:
156
+ kwargs["reasoning_effort"] = reasoning_effort
157
+ response_format = openai_response_format(context.response_format)
158
+ if response_format is not None:
159
+ kwargs["response_format"] = response_format
160
+
161
+ tools = context.tools or None
162
+ if tools:
163
+ kwargs["tools"] = [
164
+ {
165
+ "type": "function",
166
+ "function": {
167
+ "name": tool.name,
168
+ "description": tool.description,
169
+ "parameters": tool.schema.model_json_schema(),
170
+ }
171
+ }
172
+ for tool in tools
173
+ ]
174
+ kwargs["tool_choice"] = "auto"
175
+
176
+ if self.options.on_payload:
177
+ modified = self.options.on_payload(kwargs)
178
+ if modified is not None:
179
+ kwargs = modified
180
+
181
+ async with await self._client.chat.stream_async(**kwargs) as stream:
182
+ async for event in stream:
183
+ if self._cancelled():
184
+ yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
185
+ return
186
+ chunk = event.data
187
+ usage_data = getattr(chunk, 'usage', None)
188
+ if usage_data and usage_data != UNSET:
189
+ _input_tokens = getattr(usage_data, 'prompt_tokens', 0) or 0
190
+ _output_tokens = getattr(usage_data, 'completion_tokens', 0) or 0
191
+ _details = getattr(usage_data, 'prompt_tokens_details', None)
192
+ _cache_read_tokens = getattr(_details, 'cached_tokens', 0) or 0
193
+ if not chunk.choices:
194
+ continue
195
+ choice = chunk.choices[0]
196
+ delta = choice.delta
197
+
198
+ content = delta.content
199
+ if content and content !=UNSET:
200
+ if isinstance(content, str):
201
+ if not text_started:
202
+ yield TextStartEvent(text=TextContent(content=""))
203
+ text_started = True
204
+ text_buf += content
205
+ yield TextDeltaEvent(text=TextContent(content=content))
206
+ elif isinstance(content, list):
207
+ for chunk_item in content:
208
+ if isinstance(chunk_item, ThinkChunk):
209
+ thinking_text = "".join(
210
+ t.text for t in chunk_item.thinking
211
+ if isinstance(t, TextChunk)
212
+ )
213
+ if thinking_text:
214
+ if not thinking_started:
215
+ yield ThinkingStartEvent(thinking=None)
216
+ thinking_started = True
217
+ thinking_buf += thinking_text
218
+ yield ThinkingDeltaEvent(thinking=ThinkingContent(content=thinking_text))
219
+ if chunk_item.closed:
220
+ if thinking_started:
221
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
222
+ thinking_index += 1
223
+ thinking_started = False
224
+ thinking_buf = ""
225
+ elif isinstance(chunk_item, TextChunk):
226
+ if not text_started:
227
+ yield TextStartEvent(text=TextContent(content=""))
228
+ text_started = True
229
+ text_buf += chunk_item.text
230
+ yield TextDeltaEvent(text=TextContent(content=chunk_item.text))
231
+
232
+ tool_calls = delta.tool_calls
233
+ if tool_calls and tool_calls != UNSET:
234
+ for tc in tool_calls:
235
+ idx = tc.index if tc.index is not None else 0
236
+ fn = tc.function
237
+ args = fn.arguments if isinstance(fn.arguments, str) else json.dumps(fn.arguments)
238
+ tc_id = tc.id or ""
239
+ if idx not in tool_started:
240
+ tool_started[idx] = True
241
+ tool_bufs[idx] = ""
242
+ tool_meta[idx] = {"id": tc_id, "name": fn.name}
243
+ yield ToolCallStartEvent(tool_call=ToolCallContent(id=tc_id, name=fn.name)
244
+ )
245
+ if args:
246
+ tool_bufs[idx] += args
247
+ yield ToolCallDeltaEvent(tool_call=ToolCallContent(id=tc_id)
248
+ )
249
+
250
+ finish = choice.finish_reason
251
+ if finish and finish != UNSET:
252
+ if thinking_started:
253
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
254
+ thinking_index += 1
255
+ thinking_started = False
256
+ thinking_buf = ""
257
+
258
+ if text_started:
259
+ yield TextEndEvent(text=TextContent(content=text_buf))
260
+ text_index += 1
261
+ text_started = False
262
+ text_buf = ""
263
+
264
+ for idx in sorted(tool_started):
265
+ args_str = tool_bufs[idx].strip()
266
+ args = parse_tool_args(args_str)
267
+
268
+ yield ToolCallEndEvent(tool_call=ToolCallContent(
269
+ id=tool_meta[idx]["id"],
270
+ name=tool_meta[idx]["name"],
271
+ args=args
272
+ )
273
+ )
274
+ if tool_started:
275
+ tool_index += len(tool_started)
276
+ tool_started.clear()
277
+ tool_bufs.clear()
278
+ tool_meta.clear()
279
+
280
+ stop_reason = _STOP_REASON.get(str(finish), StopReason.Stop)
281
+ yield EndEvent(reason=stop_reason, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)
282
+
283
+ except Exception as e:
284
+ yield ErrorEvent(reason=StopReason.Error, error=str(e))
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from tau.inference.api.text.utils import parse_tool_args
4
+ from collections.abc import AsyncGenerator, AsyncIterator
5
+ from typing import Any
6
+ from ollama import AsyncClient
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, ThinkingLevel,
11
+ StartEvent, EndEvent, ErrorEvent,
12
+ TextStartEvent, TextDeltaEvent, TextEndEvent,
13
+ ThinkingStartEvent, ThinkingDeltaEvent, ThinkingEndEvent,
14
+ ToolCallStartEvent, 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
+ _MINIMAL_LEVELS = {ThinkingLevel.Low, ThinkingLevel.Minimal}
26
+
27
+ _STOP_REASON: dict[str, StopReason] = {
28
+ "stop": StopReason.Stop,
29
+ "length": StopReason.Length,
30
+ }
31
+
32
+
33
+ def _messages_to_ollama(messages: list[LLMMessage]) -> list[dict[str, Any]]:
34
+ """Convert a message list to Ollama Chat API format, placing images in a separate field."""
35
+ result: list[dict[str, Any]] = []
36
+ for msg in messages:
37
+ match msg:
38
+ case SystemMessage():
39
+ text = "\n".join(c.content for c in msg.contents if isinstance(c, TextContent))
40
+ result.append({"role": "system", "content": text})
41
+ case UserMessage():
42
+ text_parts: list[str] = []
43
+ images: list[str] = []
44
+ for item in msg.contents:
45
+ match item:
46
+ case TextContent():
47
+ text_parts.append(item.content)
48
+ case ImageContent():
49
+ images.extend(b64 for b64, _ in item.to_base64())
50
+ if not text_parts and not images:
51
+ continue
52
+ entry: dict[str, Any] = {"role": "user", "content": "\n".join(text_parts)}
53
+ if images:
54
+ entry["images"] = images
55
+ result.append(entry)
56
+ case AssistantMessage():
57
+ text_parts = []
58
+ thinking_parts: list[str] = []
59
+ tool_calls: list[dict[str, Any]] = []
60
+ for item in msg.contents:
61
+ match item:
62
+ case TextContent():
63
+ text_parts.append(item.content)
64
+ case ThinkingContent():
65
+ thinking_parts.append(item.content)
66
+ case ToolCallContent():
67
+ tool_calls.append({
68
+ "function": {"name": item.name, "arguments": item.args}
69
+ })
70
+ entry = {"role": "assistant", "content": "\n".join(text_parts)}
71
+ if thinking_parts:
72
+ entry["thinking"] = "\n".join(thinking_parts)
73
+ if tool_calls:
74
+ entry["tool_calls"] = tool_calls
75
+ result.append(entry)
76
+ case ToolMessage():
77
+ for content in msg.contents:
78
+ if isinstance(content, ToolResultContent):
79
+ result.append({"role": "tool", "content": content.content})
80
+
81
+ return result
82
+
83
+
84
+ def _format(response_format: Any | None) -> dict[str, Any] | None:
85
+ """Extract the raw JSON schema dict for Ollama's format field, or None if unstructured."""
86
+ structured = normalize_structured_response_format(response_format)
87
+ return structured.schema if structured is not None else None
88
+
89
+
90
+ class OllamaChatAPI(BaseAPI):
91
+ """Streaming LLM API adapter for the Ollama Chat endpoint."""
92
+
93
+ def __init__(self, options: LLMOptions) -> None:
94
+ """Initialise the Ollama AsyncClient targeting the configured host."""
95
+ super().__init__(options)
96
+ self._client = AsyncClient(
97
+ host=options.base_url,
98
+ headers=options.headers or {},
99
+ timeout=options.timeout.total_seconds(),
100
+ )
101
+
102
+ def _inference_options(self) -> dict[str, Any]:
103
+ """Build Ollama model-level options dict (temperature, token limit)."""
104
+ opts: dict[str, Any] = {"temperature": self.options.temperature}
105
+ if self.options.max_tokens is not None:
106
+ opts["num_predict"] = self.options.max_tokens
107
+ return opts
108
+
109
+ async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
110
+ """Stream LLMEvents from the local Ollama Chat endpoint."""
111
+ ollama_messages = _messages_to_ollama(context.messages)
112
+ if context.system_prompt:
113
+ ollama_messages = [{"role": "system", "content": context.system_prompt}] + ollama_messages
114
+
115
+ think: bool | None = None
116
+ if self.options.thinking_level is not None:
117
+ think = self.options.thinking_level not in _MINIMAL_LEVELS
118
+
119
+ text_started = False
120
+ text_buf = ""
121
+ thinking_started = False
122
+ thinking_buf = ""
123
+ _input_tokens = 0
124
+ _output_tokens = 0
125
+
126
+ yield StartEvent()
127
+
128
+ try:
129
+ payload: dict[str, Any] = {
130
+ "model": model.id,
131
+ "messages": ollama_messages,
132
+ "stream": True,
133
+ "think": think,
134
+ "options": self._inference_options(),
135
+ }
136
+ response_format = _format(context.response_format)
137
+ if response_format is not None:
138
+ payload["format"] = response_format
139
+
140
+ tools = context.tools or None
141
+ if tools:
142
+ payload["tools"] = [
143
+ {
144
+ "type": "function",
145
+ "function": {
146
+ "name": tool.name,
147
+ "description": tool.description,
148
+ "parameters": tool.schema.model_json_schema(),
149
+ }
150
+ }
151
+ for tool in tools
152
+ ]
153
+
154
+ if self.options.on_payload:
155
+ modified = self.options.on_payload(payload)
156
+ if modified is not None:
157
+ payload = modified
158
+
159
+ async for chunk in await self._client.chat(**payload):
160
+ if self._cancelled():
161
+ yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
162
+ return
163
+ msg = chunk.message
164
+
165
+ if msg.thinking:
166
+ if not thinking_started:
167
+ yield ThinkingStartEvent(thinking=None)
168
+ thinking_started = True
169
+ thinking_buf += msg.thinking
170
+ yield ThinkingDeltaEvent(thinking=ThinkingContent(content=msg.thinking))
171
+
172
+ if msg.content:
173
+ if not text_started:
174
+ yield TextStartEvent(text=TextContent(content=""))
175
+ text_started = True
176
+ text_buf += msg.content
177
+ yield TextDeltaEvent(text=TextContent(content=msg.content))
178
+
179
+ # Ollama sends all tool calls in the final chunk, not incrementally.
180
+ if msg.tool_calls:
181
+ for i, tc in enumerate(msg.tool_calls):
182
+ fn = tc.function
183
+ args_raw = fn.arguments
184
+ args = parse_tool_args(args_raw)
185
+
186
+ yield ToolCallStartEvent(tool_call=ToolCallContent(name=fn.name))
187
+ yield ToolCallEndEvent(tool_call=ToolCallContent(name=fn.name, args=args))
188
+
189
+ if chunk.done:
190
+ _input_tokens = getattr(chunk, 'prompt_eval_count', 0) or 0
191
+ _output_tokens = getattr(chunk, 'eval_count', 0) or 0
192
+ if thinking_started:
193
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
194
+ if text_started:
195
+ yield TextEndEvent(text=TextContent(content=text_buf))
196
+ stop_reason = _STOP_REASON.get(chunk.done_reason or "", StopReason.Stop)
197
+ yield EndEvent(reason=stop_reason, input_tokens=_input_tokens, output_tokens=_output_tokens)
198
+
199
+ except Exception as e:
200
+ yield ErrorEvent(reason=StopReason.Error, error=str(e))