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.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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,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
|
+
]
|