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,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
|