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,297 @@
1
+ from __future__ import annotations
2
+ from collections.abc import AsyncGenerator, AsyncIterator
3
+ from contextlib import aclosing
4
+ from dataclasses import fields
5
+ from tau.inference.model.registry import ModelRegistry
6
+ from tau.inference.api.registry import LazyAPI
7
+ from tau.inference.api.text.registry import LLMAPIRegistry
8
+ from tau.inference.provider.registry import TextProviderRegistry, ProviderRegistry
9
+ from tau.inference.provider.types import APIProvider, OAuthProvider
10
+ from tau.auth.manager import AuthManager
11
+ from tau.auth.types import OAuthCredential
12
+ from tau.inference.types import LLMContext, LLMEvent, LLMOptions
13
+ from tau.message.types import LLMMessage, SystemMessage
14
+ from typing import TYPE_CHECKING, Optional
15
+
16
+ if TYPE_CHECKING:
17
+ from tau.tool.types import Tool
18
+ from tau.inference.types import ThinkingLevel
19
+
20
+
21
+ class TextLLM:
22
+ """Wrapper around inference APIs with model/provider resolution and option merging."""
23
+ _apis: LLMAPIRegistry | None = None
24
+ _models: ModelRegistry | None = None
25
+ _providers: TextProviderRegistry | None = None
26
+ _auth_manager: AuthManager | None = None
27
+
28
+ @classmethod
29
+ def _builtin_apis(cls) -> LLMAPIRegistry:
30
+ if cls._apis is None:
31
+ cls._apis = LLMAPIRegistry.from_builtins()
32
+ return cls._apis
33
+
34
+ @classmethod
35
+ def _builtin_models(cls) -> ModelRegistry:
36
+ if cls._models is None:
37
+ cls._models = ModelRegistry.from_text_builtins()
38
+ return cls._models
39
+
40
+ @classmethod
41
+ def _builtin_providers(cls) -> TextProviderRegistry:
42
+ if cls._providers is None:
43
+ cls._providers = TextProviderRegistry.from_builtins()
44
+ return cls._providers
45
+
46
+ @classmethod
47
+ def _builtin_auth_manager(cls) -> AuthManager:
48
+ if cls._auth_manager is None:
49
+ cls._auth_manager = AuthManager.create(ProviderRegistry(text=cls._builtin_providers()))
50
+ return cls._auth_manager
51
+
52
+ def __init__(
53
+ self,
54
+ model_id: str,
55
+ provider: str | None = None,
56
+ options: LLMOptions | None = None,
57
+ *,
58
+ models: Optional[ModelRegistry] = None,
59
+ providers: Optional[TextProviderRegistry] = None,
60
+ apis: Optional[LLMAPIRegistry] = None,
61
+ auth_manager: Optional[AuthManager] = None,
62
+ ) -> None:
63
+ """Initialize an LLM by resolving model, provider, and API implementation.
64
+
65
+ Args:
66
+ model_id: The model identifier (e.g., 'claude-3-5-sonnet-latest').
67
+ provider: Optional provider name; if omitted, defaults from model definition.
68
+ options: Optional LLMOptions for API key, base_url, temperature, etc.
69
+ models: Optional custom ModelRegistry; defaults to global builtin registry.
70
+ providers: Optional custom TextProviderRegistry; defaults to global builtin registry.
71
+ apis: Optional custom LLMAPIRegistry; defaults to global builtin registry.
72
+ auth_manager: Optional custom AuthManager; defaults to global builtin store.
73
+
74
+ Raises:
75
+ ValueError: If model_id or provider not found in registries.
76
+ RuntimeError: If OAuth provider requires credentials that are unavailable.
77
+ """
78
+ _models = models if models is not None else type(self)._builtin_models()
79
+ _providers = providers if providers is not None else type(self)._builtin_providers()
80
+ _apis = apis if apis is not None else type(self)._builtin_apis()
81
+ self._auth_manager = auth_manager if auth_manager is not None else type(self)._builtin_auth_manager()
82
+
83
+ # When provider is not pinned, try all registered variants of the model
84
+ # in order, skipping OAuth providers whose credentials are missing.
85
+ # This lets `model_id="claude-sonnet-4-6"` work with an API key even if
86
+ # an OAuth variant of the same model is registered first.
87
+ candidates = (
88
+ [_models.get(model_id, provider=provider)]
89
+ if provider is not None
90
+ else _models._models.get(model_id, [])
91
+ )
92
+ if not candidates or candidates[0] is None:
93
+ raise ValueError(f"Model '{model_id}' not found.")
94
+
95
+ model = None
96
+ resolved_provider = None
97
+ for candidate in candidates:
98
+ cand_provider = _providers.get(candidate.provider)
99
+ if cand_provider is None:
100
+ continue
101
+ if isinstance(cand_provider, OAuthProvider):
102
+ if not isinstance(self._auth_manager.get(cand_provider.id), OAuthCredential):
103
+ continue # no credentials — try next variant
104
+ model = candidate
105
+ resolved_provider = cand_provider
106
+ break
107
+
108
+ if model is None or resolved_provider is None:
109
+ tried = [c.provider for c in candidates]
110
+ raise RuntimeError(
111
+ f"No usable provider found for '{model_id}'. "
112
+ f"Tried: {tried}. Log in or set an API key."
113
+ )
114
+
115
+ self.model = model
116
+
117
+ # Keep the API reference unresolved (a "module:Class" string stays a
118
+ # string) so the provider SDK is not imported until the first request.
119
+ api_ref = model.api or resolved_provider.api
120
+
121
+ base_url_override = model.base_url
122
+
123
+ if isinstance(resolved_provider, OAuthProvider):
124
+ credential = self._auth_manager.get(resolved_provider.id)
125
+ if not isinstance(credential, OAuthCredential):
126
+ raise RuntimeError(
127
+ f"No credentials found for '{resolved_provider.id}'. Please log in first."
128
+ )
129
+ base_opts = LLMOptions(api_key=resolved_provider.get_api_key(credential))
130
+ if base_url_override:
131
+ base_opts.base_url = base_url_override
132
+ merged = self._merge_options(base_opts, options)
133
+ self.provider_id = resolved_provider.id
134
+ else:
135
+ base_opts = resolved_provider.options
136
+ if base_url_override:
137
+ override_opts = LLMOptions(base_url=base_url_override)
138
+ base_opts = self._merge_options(base_opts, override_opts)
139
+ merged = self._merge_options(base_opts, options)
140
+ self.provider_id = resolved_provider.id
141
+
142
+ if merged.max_tokens is None:
143
+ merged.max_tokens = model.max_tokens
144
+
145
+ # Resolve any "$ENV_VAR" / "!command" references in custom headers to
146
+ # their values. Done once here (per provider/model selection); the
147
+ # resolver memoizes, so a !command runs only the first time it's seen.
148
+ if merged.headers:
149
+ from tau.utils.secrets import resolve_secrets
150
+ merged.headers = resolve_secrets(merged.headers)
151
+
152
+ # Lazy adapter: exposes `.options` immediately but only imports the
153
+ # provider SDK and builds its client on first `.stream()`/`.invoke()`.
154
+ self.api = LazyAPI(_apis, api_ref, merged)
155
+
156
+ @classmethod
157
+ def list_available(cls) -> list:
158
+ """Return all models whose provider has usable auth (stored credential or env var)."""
159
+ import os
160
+ from tau.inference.provider.types import OAuthProvider
161
+ from tau.auth.types import OAuthCredential, APICredential
162
+
163
+ cls._auth_manager.reload() # pick up any credentials added since startup
164
+ result = []
165
+ seen: set[str] = set()
166
+ for candidates in cls._models._models.values():
167
+ for candidate in candidates:
168
+ key = f"{candidate.provider}/{candidate.id}"
169
+ if key in seen:
170
+ continue
171
+ provider = cls._providers.get(candidate.provider)
172
+ if provider is None:
173
+ continue
174
+ if isinstance(provider, OAuthProvider):
175
+ if not isinstance(cls._auth_manager.get(provider.id), OAuthCredential):
176
+ continue
177
+ else:
178
+ cred = cls._auth_manager.get(provider.id)
179
+ if not isinstance(cred, APICredential):
180
+ # fall back to env var (e.g. ANTHROPIC_API_KEY)
181
+ if not os.environ.get(f"{provider.id.upper()}_API_KEY"):
182
+ continue
183
+ seen.add(key)
184
+ result.append(candidate)
185
+ return result
186
+
187
+ def _merge_options(self, base: LLMOptions, override: LLMOptions | None) -> LLMOptions:
188
+ """Merge base options with override options, preferring non-None override values.
189
+
190
+ Args:
191
+ base: The base LLMOptions configuration.
192
+ override: Optional override LLMOptions; fields override base when non-None.
193
+
194
+ Returns:
195
+ A new LLMOptions with merged values.
196
+ """
197
+ if override is None:
198
+ return base
199
+ merged = LLMOptions(**{f.name: getattr(base, f.name) for f in fields(base)})
200
+ for f in fields(override):
201
+ value = getattr(override, f.name)
202
+ if value is not None:
203
+ setattr(merged, f.name, value)
204
+ return merged
205
+
206
+ def _resolve_messages(self, context: LLMContext) -> list[LLMMessage]:
207
+ """Resolve messages for the LLM call, prepending system prompt if needed.
208
+
209
+ Args:
210
+ context: The LLMContext with messages and optional system prompt.
211
+
212
+ Returns:
213
+ A list of LLMMessages with system message injected if needed.
214
+ """
215
+ messages = context.messages
216
+ if context.system_prompt:
217
+ if not messages or not isinstance(messages[0], SystemMessage):
218
+ messages = [SystemMessage.text(context.system_prompt)] + messages
219
+ return messages
220
+
221
+ async def stream(self, context: LLMContext) -> AsyncGenerator[LLMEvent, None]:
222
+ """Stream LLM events from the configured provider API.
223
+
224
+ Retries transient errors transparently as long as no events have been
225
+ yielded yet (pre-stream failures). Once streaming has started, errors
226
+ are forwarded as ErrorEvent because already-yielded events can't be
227
+ recalled.
228
+
229
+ Args:
230
+ context: The LLMContext with messages, tools, and response format options.
231
+
232
+ Yields:
233
+ LLMEvent objects (TextDeltaEvent, ToolCallEndEvent, EndEvent, ErrorEvent, etc.).
234
+ """
235
+ from tau.inference.types import ErrorEvent, RetryEvent, StartEvent, StopReason
236
+ from tau.inference.utils import classify_error
237
+ import asyncio
238
+
239
+ api_key = await self._auth_manager.get_api_key(self.provider_id)
240
+ if api_key:
241
+ self.api.options.api_key = api_key
242
+
243
+ messages = self._resolve_messages(context)
244
+ api_context = LLMContext(
245
+ messages=messages,
246
+ tools=context.tools,
247
+ response_format=context.response_format,
248
+ )
249
+
250
+ max_retries = self.api.options.max_retries
251
+ base_delay_s = self.api.options.retry_base_delay_ms / 1000
252
+
253
+ for attempt in range(max_retries + 1):
254
+ received_any = False
255
+ try:
256
+ async with aclosing(self.api.stream(api_context, model=self.model)) as stream:
257
+ async for event in stream:
258
+ # StartEvent/RetryEvent are emitted locally before any HTTP round-trip;
259
+ # don't count them as "received data" so retries still fire on empty bodies.
260
+ if not isinstance(event, (StartEvent, RetryEvent)):
261
+ received_any = True
262
+ yield event
263
+ return
264
+ except Exception as e:
265
+ classified = classify_error(e)
266
+ if received_any or not classified.retryable or attempt >= max_retries:
267
+ yield ErrorEvent(reason=StopReason.Error, error=str(e))
268
+ return
269
+ yield RetryEvent(attempt=attempt + 1, max_retries=max_retries, error=str(e))
270
+ await asyncio.sleep(base_delay_s * (2 ** attempt))
271
+
272
+ async def invoke(
273
+ self,
274
+ context: LLMContext,
275
+ thinking_level: Optional["ThinkingLevel"] = None,
276
+ ) -> list[LLMEvent]:
277
+ from tau.inference.types import ThinkingLevel
278
+ api_key = await self._auth_manager.get_api_key(self.provider_id)
279
+ if api_key:
280
+ self.api.options.api_key = api_key
281
+
282
+ original = self.api.options.thinking_level
283
+ if thinking_level is not None and thinking_level != ThinkingLevel.Off:
284
+ self.api.options.thinking_level = thinking_level
285
+ try:
286
+ messages = self._resolve_messages(context)
287
+ api_context = LLMContext(
288
+ messages=messages,
289
+ tools=context.tools,
290
+ response_format=context.response_format,
291
+ )
292
+ return await self.api.invoke(api_context, model=self.model)
293
+ except Exception as e:
294
+ from tau.inference.types import ErrorEvent, StopReason
295
+ return [ErrorEvent(reason=StopReason.Error, error=str(e))]
296
+ finally:
297
+ self.api.options.thinking_level = original
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class APIResponse:
6
+ status_code: int
7
+ headers: dict[str, str]
@@ -0,0 +1,228 @@
1
+ """Shared utilities for LLM API provider implementations."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+
8
+ __all__ = [
9
+ "parse_tool_args",
10
+ "openai_user_content", "openai_assistant_content", "openai_messages_to_chat", "openai_response_format",
11
+ "anthropic_messages_to_list", "anthropic_output_config", "anthropic_apply_message_cache",
12
+ ]
13
+
14
+
15
+ _CACHE_MARKER = {"type": "ephemeral"}
16
+
17
+
18
+ def anthropic_apply_message_cache(
19
+ messages: list[dict[str, Any]],
20
+ n: int = 2,
21
+ skip_tail: int = 0,
22
+ ) -> list[dict[str, Any]]:
23
+ """Inject cache_control breakpoints into the last n stable messages.
24
+
25
+ Implements the Anthropic 'system_and_3' caching strategy — the system
26
+ prompt is already marked by the caller; this adds up to 2 more breakpoints
27
+ on the tail of the stable session history so the bulk of the conversation
28
+ is served from cache on subsequent turns.
29
+
30
+ skip_tail: number of ephemeral messages at the end of the list to skip
31
+ (desktop/browser screenshots that change every turn and must not be cached).
32
+
33
+ Returns a new list; the original is not mutated.
34
+ """
35
+ import copy
36
+ messages = copy.deepcopy(messages)
37
+ total = len(messages)
38
+ stable_end = total - skip_tail # index just past the last stable message
39
+ stable_start = max(0, stable_end - n)
40
+ for msg in messages[stable_start:stable_end]:
41
+ content = msg.get("content")
42
+ if content is None or content == "":
43
+ msg["cache_control"] = _CACHE_MARKER
44
+ elif isinstance(content, str):
45
+ msg["content"] = [{"type": "text", "text": content, "cache_control": _CACHE_MARKER}]
46
+ elif isinstance(content, list) and content:
47
+ last = content[-1]
48
+ if isinstance(last, dict):
49
+ last["cache_control"] = _CACHE_MARKER
50
+ return messages
51
+
52
+
53
+ def parse_tool_args(value: Any) -> dict:
54
+ """Parse a tool-call arguments value into a dict.
55
+
56
+ Handles the three shapes that provider APIs return:
57
+ - already a dict → return as-is
58
+ - a JSON string → parse and return (empty string → {})
59
+ - anything else → return {}
60
+ Falls back to {} on JSONDecodeError.
61
+ """
62
+ if isinstance(value, dict):
63
+ return value
64
+ if not value:
65
+ return {}
66
+ try:
67
+ result = json.loads(value)
68
+ return result if isinstance(result, dict) else {}
69
+ except (json.JSONDecodeError, TypeError):
70
+ return {}
71
+
72
+
73
+ def openai_user_content(content_items: list) -> str | list[dict[str, Any]]:
74
+ """Convert user message contents to OpenAI chat format (completions/copilot/mistral)."""
75
+ from tau.message.types import TextContent, ImageContent
76
+ parts: list[dict[str, Any]] = []
77
+ for item in content_items:
78
+ match item:
79
+ case TextContent():
80
+ parts.append({"type": "text", "text": item.content})
81
+ case ImageContent():
82
+ for b64, mime in item.to_base64():
83
+ url = b64 if b64.startswith("http") else f"data:{mime or 'image/png'};base64,{b64}"
84
+ parts.append({"type": "image_url", "image_url": {"url": url}})
85
+ if item.dimension_note:
86
+ parts.append({"type": "text", "text": item.dimension_note})
87
+ if len(parts) == 1 and parts[0]["type"] == "text":
88
+ return parts[0]["text"]
89
+ return parts
90
+
91
+
92
+ def openai_assistant_content(content_items: list) -> tuple[str | None, list[dict[str, Any]]]:
93
+ """Convert assistant message contents to OpenAI chat format (completions/copilot)."""
94
+ from tau.message.types import TextContent, ToolCallContent
95
+ text_parts: list[str] = []
96
+ tool_calls: list[dict[str, Any]] = []
97
+ for item in content_items:
98
+ match item:
99
+ case TextContent():
100
+ text_parts.append(item.content)
101
+ case ToolCallContent():
102
+ tool_calls.append({
103
+ "id": item.id,
104
+ "type": "function",
105
+ "function": {"name": item.name, "arguments": json.dumps(item.args)},
106
+ })
107
+ return "".join(text_parts) or None, tool_calls
108
+
109
+
110
+ def openai_response_format(response_format: Any | None) -> dict[str, Any] | None:
111
+ """Convert response_format to OpenAI json_schema format (completions/copilot/mistral)."""
112
+ from tau.inference.types import normalize_structured_response_format
113
+ structured = normalize_structured_response_format(response_format)
114
+ if structured is None:
115
+ return None
116
+ return {
117
+ "type": "json_schema",
118
+ "json_schema": {
119
+ "name": structured.name,
120
+ "schema": structured.schema,
121
+ "strict": structured.strict,
122
+ },
123
+ }
124
+
125
+
126
+ def openai_messages_to_chat(messages: list) -> list[dict[str, Any]]:
127
+ """Convert a message list to OpenAI chat completions format."""
128
+ from tau.message.types import (
129
+ SystemMessage, UserMessage, AssistantMessage, ToolMessage,
130
+ TextContent, ToolResultContent,
131
+ )
132
+ result: list[dict[str, Any]] = []
133
+ for msg in messages:
134
+ match msg:
135
+ case SystemMessage():
136
+ text = "\n".join(c.content for c in msg.contents if isinstance(c, TextContent))
137
+ result.append({"role": "system", "content": text})
138
+ case UserMessage():
139
+ if not msg.contents:
140
+ continue
141
+ result.append({"role": "user", "content": openai_user_content(msg.contents)})
142
+ case AssistantMessage():
143
+ text, tool_calls = openai_assistant_content(msg.contents)
144
+ entry: dict[str, Any] = {"role": "assistant"}
145
+ if text is not None:
146
+ entry["content"] = text
147
+ if tool_calls:
148
+ entry["tool_calls"] = tool_calls
149
+ result.append(entry)
150
+ case ToolMessage():
151
+ for content in msg.contents:
152
+ if isinstance(content, ToolResultContent):
153
+ result.append({
154
+ "role": "tool",
155
+ "tool_call_id": content.id,
156
+ "content": content.content,
157
+ })
158
+ return result
159
+
160
+
161
+ def anthropic_messages_to_list(messages: list) -> tuple[str | None, list[dict[str, Any]]]:
162
+ """Convert a message list to Anthropic Messages API format."""
163
+ from tau.message.types import (
164
+ SystemMessage, UserMessage, AssistantMessage, ToolMessage,
165
+ TextContent, ImageContent, ThinkingContent, ToolCallContent, ToolResultContent,
166
+ )
167
+ system: str | None = None
168
+ result: list[dict[str, Any]] = []
169
+ for msg in messages:
170
+ match msg:
171
+ case SystemMessage():
172
+ system = "\n".join(c.content for c in msg.contents if isinstance(c, TextContent))
173
+ case UserMessage():
174
+ if not msg.contents:
175
+ continue
176
+ parts: list[dict[str, Any]] = []
177
+ has_text = False
178
+ has_image = False
179
+ for item in msg.contents:
180
+ match item:
181
+ case TextContent():
182
+ has_text = True
183
+ parts.append({"type": "text", "text": item.content})
184
+ case ImageContent():
185
+ has_image = True
186
+ for b64, mime in item.to_base64():
187
+ parts.append({
188
+ "type": "image",
189
+ "source": {"type": "base64", "media_type": mime or "image/png", "data": b64},
190
+ })
191
+ if item.dimension_note:
192
+ parts.append({"type": "text", "text": item.dimension_note})
193
+ if has_image and not has_text:
194
+ parts.append({"type": "text", "text": "(see attached image)"})
195
+ result.append({"role": "user", "content": parts})
196
+ case AssistantMessage():
197
+ parts = []
198
+ for item in msg.contents:
199
+ match item:
200
+ case TextContent():
201
+ parts.append({"type": "text", "text": item.content})
202
+ case ThinkingContent():
203
+ parts.append({"type": "thinking", "thinking": item.content, "signature": item.signature})
204
+ case ToolCallContent():
205
+ parts.append({"type": "tool_use", "id": item.id, "name": item.name, "input": item.args})
206
+ result.append({"role": "assistant", "content": parts})
207
+ case ToolMessage():
208
+ tool_results = []
209
+ for content in msg.contents:
210
+ if isinstance(content, ToolResultContent):
211
+ tool_results.append({
212
+ "type": "tool_result",
213
+ "tool_use_id": content.id,
214
+ "content": content.content,
215
+ "is_error": content.is_error,
216
+ })
217
+ if tool_results:
218
+ result.append({"role": "user", "content": tool_results})
219
+ return system, result
220
+
221
+
222
+ def anthropic_output_config(response_format: Any | None) -> dict[str, Any] | None:
223
+ """Convert response_format to Anthropic output config format."""
224
+ from tau.inference.types import normalize_structured_response_format
225
+ structured = normalize_structured_response_format(response_format)
226
+ if structured is None:
227
+ return None
228
+ return {"format": {"type": "json_schema", "schema": structured.schema}}
File without changes
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from tau.inference.model.types import Model
6
+ from tau.inference.types import GeneratedVideo, VideoContext, VideoOptions
7
+
8
+
9
+ class BaseVideoAPI(ABC):
10
+ """
11
+ Base class for all video generation providers.
12
+
13
+ Video generation is inherently async at the provider level — jobs are
14
+ submitted and polled until completion. Implementations handle the full
15
+ submit → poll → return cycle inside generate(), keeping the caller
16
+ interface simple and uniform.
17
+ """
18
+
19
+ def __init__(self, options: VideoOptions) -> None:
20
+ self.options = options
21
+
22
+ @abstractmethod
23
+ async def generate(self, model: Model, context: VideoContext) -> GeneratedVideo:
24
+ """Generate a video from text or image prompts."""
25
+ raise NotImplementedError
26
+
@@ -0,0 +1,7 @@
1
+ # Lazy registry entries: each provider API is referenced by a "module:ClassName"
2
+ # path so its SDK is only imported when that provider is actually used, not at
3
+ # registry construction.
4
+ VIDEO_APIS: list[tuple[str, str]] = [
5
+ ("fal-video", "tau.inference.api.video.fal_video:FalVideoAPI"),
6
+ ("openrouter-video", "tau.inference.api.video.openrouter_video:OpenRouterVideoAPI"),
7
+ ]