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,227 @@
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.types import (
10
+ LLMContext, LLMEvent, LLMOptions, StopReason, ThinkingLevel,
11
+ StartEvent, EndEvent, ErrorEvent,
12
+ TextStartEvent, TextDeltaEvent, TextEndEvent,
13
+ ThinkingStartEvent, ThinkingDeltaEvent, ThinkingEndEvent,
14
+ ToolCallStartEvent, ToolCallDeltaEvent, ToolCallEndEvent,
15
+ )
16
+ from tau.message.types import (
17
+ SystemMessage, UserMessage, AssistantMessage, ToolMessage,
18
+ TextContent, ImageContent, ThinkingContent, ToolCallContent, ToolResultContent,
19
+ )
20
+ from typing import Optional, TYPE_CHECKING
21
+ if TYPE_CHECKING:
22
+ from tau.tool.types import Tool
23
+
24
+ _REASONING_EFFORT: dict[ThinkingLevel, str] = {
25
+ ThinkingLevel.Minimal: "low",
26
+ ThinkingLevel.Low: "low",
27
+ ThinkingLevel.Medium: "medium",
28
+ ThinkingLevel.High: "high",
29
+ ThinkingLevel.XHigh: "high",
30
+ ThinkingLevel.Max: "high",
31
+ }
32
+
33
+ _STOP_REASON: dict[str, StopReason] = {
34
+ "stop": StopReason.Stop,
35
+ "length": StopReason.Length,
36
+ "tool_calls": StopReason.ToolCalls,
37
+ "content_filter": StopReason.ContentFilter,
38
+ }
39
+
40
+
41
+ def _clean_schema(schema: dict[str, Any]) -> dict[str, Any]:
42
+ """Strip fields that trip up strict OpenAI-compatible APIs (title, $defs, etc.)."""
43
+ result: dict[str, Any] = {}
44
+ for k, v in schema.items():
45
+ if k in ("title", "$schema"):
46
+ continue
47
+ if k == "anyOf" and isinstance(v, list):
48
+ non_null = [_clean_schema(s) if isinstance(s, dict) else s for s in v if s != {"type": "null"}]
49
+ if len(non_null) == 1:
50
+ result.update(non_null[0])
51
+ else:
52
+ result[k] = non_null
53
+ elif isinstance(v, dict):
54
+ result[k] = _clean_schema(v)
55
+ elif isinstance(v, list):
56
+ result[k] = [_clean_schema(i) if isinstance(i, dict) else i for i in v]
57
+ else:
58
+ result[k] = v
59
+ return result
60
+
61
+
62
+
63
+ class OpenAICompletionsAPI(BaseAPI):
64
+ """Streaming LLM API adapter for the OpenAI Chat Completions endpoint."""
65
+
66
+ def __init__(self, options: LLMOptions) -> None:
67
+ """Initialise the AsyncOpenAI client with the supplied options."""
68
+ super().__init__(options)
69
+ self._client = AsyncOpenAI(
70
+ api_key=options.api_key or "placeholder",
71
+ base_url=options.base_url,
72
+ default_headers=options.headers,
73
+ max_retries=options.max_retries,
74
+ timeout=options.timeout.total_seconds(),
75
+ )
76
+
77
+ def _build_params(self, model: Model, messages: list[dict[str, Any]], tools: Optional[list[Tool]] = None) -> dict[str, Any]:
78
+ """Assemble the OpenAI Chat Completions request payload."""
79
+ params: dict[str, Any] = {
80
+ "model": model.id,
81
+ "messages": messages,
82
+ "temperature": self.options.temperature,
83
+ }
84
+ if self.options.max_tokens is not None:
85
+ params["max_completion_tokens"] = self.options.max_tokens
86
+ if self.options.thinking_level is not None and self.options.thinking_level != ThinkingLevel.Off:
87
+ params["reasoning_effort"] = _REASONING_EFFORT[self.options.thinking_level]
88
+
89
+ if tools:
90
+ params["tools"] = [
91
+ {
92
+ "type": "function",
93
+ "function": {
94
+ "name": tool.name,
95
+ "description": tool.description,
96
+ "parameters": _clean_schema(tool.schema.model_json_schema()),
97
+ }
98
+ }
99
+ for tool in tools
100
+ ]
101
+ params["tool_choice"] = "auto"
102
+
103
+ return params
104
+
105
+ async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
106
+ """Stream LLMEvents from the OpenAI Chat Completions API."""
107
+ if self.options.api_key:
108
+ self._client.api_key = self.options.api_key
109
+ chat_messages = openai_messages_to_chat(context.messages)
110
+ if context.system_prompt:
111
+ chat_messages = [{"role": "system", "content": context.system_prompt}] + chat_messages
112
+ params = self._build_params(model, chat_messages, tools=context.tools or None)
113
+ response_format = openai_response_format(context.response_format)
114
+ if response_format is not None:
115
+ params["response_format"] = response_format
116
+
117
+ if self.options.on_payload:
118
+ modified = self.options.on_payload(params)
119
+ if modified is not None:
120
+ params = modified
121
+
122
+ text_started = False
123
+ text_buf = ""
124
+ thinking_started = False
125
+ thinking_buf = ""
126
+ # Tool-call accumulation state keyed by delta index (OpenAI streams partial tool calls per-index).
127
+ tool_started: dict[int, bool] = {}
128
+ tool_bufs: dict[int, str] = {}
129
+ tool_meta: dict[int, dict[str, str]] = {}
130
+ _input_tokens = 0
131
+ _output_tokens = 0
132
+ _cache_read_tokens = 0
133
+
134
+ yield StartEvent()
135
+
136
+ # async with closes the SDK stream (and its httpx response) on every
137
+ # exit path — cancellation return or an upstream GeneratorExit — instead
138
+ # of leaving it to the GC asyncgen finalizer.
139
+ async with await self._client.chat.completions.create(**params, stream=True, stream_options={"include_usage": True}, extra_body=self.options.extra_params or {}) as sdk_stream:
140
+ async for chunk in sdk_stream:
141
+ if self._cancelled():
142
+ yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
143
+ return
144
+ usage_data = getattr(chunk, 'usage', None)
145
+ if usage_data:
146
+ _input_tokens = getattr(usage_data, 'prompt_tokens', 0) or 0
147
+ _output_tokens = getattr(usage_data, 'completion_tokens', 0) or 0
148
+ _details = getattr(usage_data, 'prompt_tokens_details', None)
149
+ _cache_read_tokens = getattr(_details, 'cached_tokens', 0) or 0
150
+ choice = chunk.choices[0] if chunk.choices else None
151
+ if choice is None:
152
+ continue
153
+
154
+ delta = choice.delta
155
+
156
+ # Handle reasoning/thinking content (often used by NVIDIA and some OpenAI models)
157
+ reasoning = getattr(delta, "reasoning_content", None) or getattr(delta, "thinking", None)
158
+ if reasoning:
159
+ if not thinking_started:
160
+ yield ThinkingStartEvent(thinking=ThinkingContent(content=""))
161
+ thinking_started = True
162
+ thinking_buf += reasoning
163
+ yield ThinkingDeltaEvent(thinking=ThinkingContent(content=reasoning))
164
+
165
+ if delta.content:
166
+ # If thinking was happening, end it before starting text
167
+ if thinking_started:
168
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
169
+ thinking_started = False
170
+ thinking_buf = ""
171
+
172
+ if not text_started:
173
+ yield TextStartEvent(text=TextContent(content=""))
174
+ text_started = True
175
+ text_buf += delta.content
176
+ yield TextDeltaEvent(text=TextContent(content=delta.content))
177
+
178
+ if delta.tool_calls:
179
+ # If thinking was happening, end it
180
+ if thinking_started:
181
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
182
+ thinking_started = False
183
+ thinking_buf = ""
184
+
185
+ for tc in delta.tool_calls:
186
+ idx = tc.index
187
+ if idx not in tool_started:
188
+ tool_started[idx] = True
189
+ tool_bufs[idx] = ""
190
+ tool_meta[idx] = {"id": tc.id or "", "name": tc.function.name or "" if tc.function else ""}
191
+ yield ToolCallStartEvent(tool_call=ToolCallContent(
192
+ id=tool_meta[idx]["id"],
193
+ name=tool_meta[idx]["name"],
194
+ )
195
+ )
196
+ if tc.function and tc.function.arguments:
197
+ tool_bufs[idx] += tc.function.arguments
198
+ yield ToolCallDeltaEvent(tool_call=ToolCallContent(id=tool_meta[idx]["id"])
199
+ )
200
+
201
+ if choice.finish_reason:
202
+ if thinking_started:
203
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
204
+ thinking_started = False
205
+ thinking_buf = ""
206
+
207
+ if text_started:
208
+ yield TextEndEvent(text=TextContent(content=text_buf))
209
+ text_started = False
210
+ text_buf = ""
211
+
212
+ for idx in sorted(tool_started):
213
+ args_str = tool_bufs[idx].strip()
214
+ args = parse_tool_args(args_str)
215
+
216
+ yield ToolCallEndEvent(tool_call=ToolCallContent(
217
+ id=tool_meta[idx]["id"],
218
+ name=tool_meta[idx]["name"],
219
+ args=args,
220
+ )
221
+ )
222
+ tool_started.clear()
223
+ tool_bufs.clear()
224
+ tool_meta.clear()
225
+
226
+ stop_reason = _STOP_REASON.get(choice.finish_reason, StopReason.Stop)
227
+ yield EndEvent(reason=stop_reason, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)
@@ -0,0 +1,235 @@
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 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.types import (
10
+ LLMContext, LLMEvent, LLMOptions, StopReason, 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
+ _THINKING_EFFORT: dict[ThinkingLevel, str] = {
26
+ ThinkingLevel.Minimal: "low",
27
+ ThinkingLevel.Low: "low",
28
+ ThinkingLevel.Medium: "medium",
29
+ ThinkingLevel.High: "high",
30
+ ThinkingLevel.XHigh: "high",
31
+ ThinkingLevel.Max: "high",
32
+ }
33
+
34
+ _STOP_REASON: dict[str, StopReason] = {
35
+ "stop": StopReason.Stop,
36
+ "max_output_tokens": StopReason.Length,
37
+ "tool_calls": StopReason.ToolCalls,
38
+ "content_filter": StopReason.ContentFilter,
39
+ }
40
+
41
+
42
+ def _content_to_openai(content_items: list) -> list[dict[str, Any]]:
43
+ """Convert typed message content items to OpenAI Responses API content parts."""
44
+ parts: list[dict[str, Any]] = []
45
+ for item in content_items:
46
+ match item:
47
+ case TextContent():
48
+ parts.append({"type": "input_text", "text": item.content})
49
+ case ImageContent():
50
+ for b64, mime in item.to_base64():
51
+ url = b64 if b64.startswith("http") else f"data:{mime or 'image/png'};base64,{b64}"
52
+ parts.append({"type": "input_image", "image_url": url})
53
+ case ThinkingContent():
54
+ parts.append({
55
+ "type": "thinking",
56
+ "thinking": item.content,
57
+ "signature": item.signature,
58
+ })
59
+ case ToolCallContent():
60
+ parts.append({
61
+ "type": "function_call",
62
+ "call_id": item.id,
63
+ "name": item.name,
64
+ "arguments": json.dumps(item.args),
65
+ })
66
+ return parts
67
+
68
+
69
+ def _messages_to_input(
70
+ messages: list[LLMMessage],
71
+ ) -> tuple[str | None, list[dict[str, Any]]]:
72
+ """Convert a message list to OpenAI Responses API input items, extracting system as instructions."""
73
+ instructions: str | None = None
74
+ input_items: list[dict[str, Any]] = []
75
+
76
+ for msg in messages:
77
+ match msg:
78
+ case SystemMessage():
79
+ text_parts = [c.content for c in msg.contents if isinstance(c, TextContent)]
80
+ instructions = "\n".join(text_parts)
81
+ case ToolMessage():
82
+ for content in msg.contents:
83
+ if isinstance(content, ToolResultContent):
84
+ input_items.append({
85
+ "type": "function_call_output",
86
+ "call_id": content.id,
87
+ "output": content.content,
88
+ })
89
+ case UserMessage() | AssistantMessage():
90
+ role = "user" if isinstance(msg, UserMessage) else "assistant"
91
+ parts = _content_to_openai(msg.contents)
92
+ if parts:
93
+ input_items.append({"role": role, "content": parts})
94
+
95
+ return instructions, input_items
96
+
97
+
98
+ def _text_format(response_format: Any | None) -> dict[str, Any] | None:
99
+ """Convert response_format to the OpenAI Responses API text.format structure."""
100
+ structured = normalize_structured_response_format(response_format)
101
+ if structured is None:
102
+ return None
103
+ return {
104
+ "format": {
105
+ "type": "json_schema",
106
+ "name": structured.name,
107
+ "schema": structured.schema,
108
+ "strict": structured.strict,
109
+ }
110
+ }
111
+
112
+
113
+ class OpenAIResponsesAPI(BaseAPI):
114
+ """Streaming LLM API adapter for the OpenAI Responses API (o-series / GPT-4o)."""
115
+
116
+ def __init__(self, options: LLMOptions) -> None:
117
+ """Initialise the AsyncOpenAI client with the supplied options."""
118
+ super().__init__(options)
119
+ self._client = AsyncOpenAI(
120
+ api_key=options.api_key or "placeholder",
121
+ base_url=options.base_url,
122
+ default_headers=options.headers,
123
+ max_retries=options.max_retries,
124
+ timeout=options.timeout.total_seconds(),
125
+ )
126
+
127
+ def _build_params(self, model: Model, instructions: str | None, input_items: list, tools: Optional[list[Tool]] = None) -> dict[str, Any]:
128
+ """Assemble the OpenAI Responses API request payload."""
129
+ params: dict[str, Any] = {
130
+ "model": model.id,
131
+ "input": input_items,
132
+ "temperature": self.options.temperature,
133
+ }
134
+ if instructions:
135
+ params["instructions"] = instructions
136
+ if self.options.max_tokens is not None:
137
+ params["max_output_tokens"] = self.options.max_tokens
138
+ if self.options.thinking_level is not None and self.options.thinking_level != ThinkingLevel.Off:
139
+ params["reasoning"] = {"effort": _THINKING_EFFORT[self.options.thinking_level]}
140
+
141
+ if tools:
142
+ params["tools"] = [
143
+ {
144
+ "type": "function",
145
+ "name": tool.name,
146
+ "description": tool.description,
147
+ "parameters": tool.schema.model_json_schema(),
148
+ }
149
+ for tool in tools
150
+ ]
151
+
152
+ return params
153
+
154
+ async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
155
+ """Stream LLMEvents from the OpenAI Responses API."""
156
+ if self.options.api_key:
157
+ self._client.api_key = self.options.api_key
158
+ instructions, input_items = _messages_to_input(context.messages)
159
+ params = self._build_params(model, instructions, input_items, tools=context.tools or None)
160
+ text_format = _text_format(context.response_format)
161
+ if text_format is not None:
162
+ params["text"] = text_format
163
+
164
+ if self.options.on_payload:
165
+ modified = self.options.on_payload(params)
166
+ if modified is not None:
167
+ params = modified
168
+
169
+ tool_names: dict[str, str] = {}
170
+ _input_tokens = 0
171
+ _output_tokens = 0
172
+ _cache_read_tokens = 0
173
+
174
+ yield StartEvent()
175
+
176
+ async with self._client.responses.stream(**params) as stream:
177
+ async for event in stream:
178
+ if self._cancelled():
179
+ yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
180
+ return
181
+ etype = event.type
182
+
183
+ if etype == "response.output_item.added":
184
+ item = event.item
185
+ if item.type == "message":
186
+ yield TextStartEvent(text=TextContent(content=""))
187
+ elif item.type == "reasoning":
188
+ yield ThinkingStartEvent(thinking=None)
189
+ elif item.type == "function_call":
190
+ tool_names[item.call_id] = item.name
191
+ yield ToolCallStartEvent(tool_call=ToolCallContent(id=item.call_id, name=item.name))
192
+
193
+ elif etype == "response.output_text.delta":
194
+ yield TextDeltaEvent(text=TextContent(content=event.delta))
195
+
196
+ elif etype == "response.output_text.done":
197
+ yield TextEndEvent(text=TextContent(content=event.text))
198
+
199
+ elif etype == "response.reasoning_summary_text.delta":
200
+ yield ThinkingDeltaEvent(thinking=ThinkingContent(content=event.delta))
201
+
202
+ elif etype == "response.reasoning_summary_text.done":
203
+ yield ThinkingEndEvent(thinking=ThinkingContent(content=event.text))
204
+
205
+ elif etype == "response.function_call_arguments.delta":
206
+ call_id = event.item_id
207
+ yield ToolCallDeltaEvent(tool_call=ToolCallContent(id=call_id))
208
+
209
+ elif etype == "response.function_call_arguments.done":
210
+ call_id = event.item_id
211
+ args_str = event.arguments.strip()
212
+ args = parse_tool_args(args_str)
213
+
214
+ yield ToolCallEndEvent(tool_call=ToolCallContent(
215
+ id=call_id,
216
+ name=tool_names.get(call_id, ""),
217
+ args=args
218
+ ))
219
+
220
+ elif etype == "response.done":
221
+ resp = event.response
222
+ u = getattr(resp, 'usage', None)
223
+ if u:
224
+ _input_tokens = getattr(u, 'input_tokens', 0) or 0
225
+ _output_tokens = getattr(u, 'output_tokens', 0) or 0
226
+ _details = getattr(u, 'prompt_tokens_details', None)
227
+ _cache_read_tokens = getattr(_details, 'cached_tokens', 0) or 0
228
+ stop_reason = _STOP_REASON.get(
229
+ getattr(resp, "stop_reason", None) or "",
230
+ StopReason.Stop,
231
+ )
232
+ yield EndEvent(reason=stop_reason, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)
233
+
234
+ elif etype == "error":
235
+ yield ErrorEvent(reason=StopReason.Abort, error=str(event))
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+ import importlib
3
+ from typing import Type, Union
4
+ from tau.inference.api.text.base import BaseLLMAPI
5
+
6
+ _Entry = Union[Type[BaseLLMAPI], str]
7
+
8
+
9
+ class LLMAPIRegistry:
10
+ def __init__(self) -> None:
11
+ self._apis: dict[str, _Entry] = {}
12
+
13
+ def register(self, name: str, api: _Entry) -> None:
14
+ """Register an API class or a lazy 'module:ClassName' import path."""
15
+ self._apis[name] = api
16
+
17
+ def unregister(self, name: str) -> None:
18
+ self._apis.pop(name, None)
19
+
20
+ def list(self) -> list[Type[BaseLLMAPI]]:
21
+ return [self._resolve(name) for name in list(self._apis)]
22
+
23
+ def get(self, name: str) -> Type[BaseLLMAPI] | None:
24
+ if name not in self._apis:
25
+ return None
26
+ return self._resolve(name)
27
+
28
+ def reset(self) -> None:
29
+ self._apis.clear()
30
+
31
+ def _resolve(self, name: str) -> Type[BaseLLMAPI]:
32
+ entry = self._apis[name]
33
+ if isinstance(entry, str):
34
+ mod_path, _, cls_name = entry.partition(":")
35
+ cls = getattr(importlib.import_module(mod_path), cls_name)
36
+ self._apis[name] = cls
37
+ return cls
38
+ return entry
39
+
40
+ @classmethod
41
+ def from_builtins(cls) -> LLMAPIRegistry:
42
+ from tau.inference.api.text.builtins import LLM_APIS
43
+ instance = cls()
44
+ for name, api in LLM_APIS:
45
+ instance.register(name, api)
46
+ return instance
47
+
48
+
49
+ # Backward-compat alias
50
+ APIRegistry = LLMAPIRegistry