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,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
4
|
+
from typing import Any
|
|
5
|
+
from google import genai
|
|
6
|
+
from google.genai import types as genai_types
|
|
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, ThinkingBudgets, 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
|
+
_STOP_REASON: dict[str, StopReason] = {
|
|
26
|
+
"STOP": StopReason.Stop,
|
|
27
|
+
"MAX_TOKENS": StopReason.Length,
|
|
28
|
+
"SAFETY": StopReason.ContentFilter,
|
|
29
|
+
"RECITATION": StopReason.ContentFilter,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _messages_to_gemini(
|
|
34
|
+
messages: list[LLMMessage],
|
|
35
|
+
) -> tuple[str | None, list[genai_types.Content]]:
|
|
36
|
+
system: str | None = None
|
|
37
|
+
contents: list[genai_types.Content] = []
|
|
38
|
+
|
|
39
|
+
for msg in messages:
|
|
40
|
+
match msg:
|
|
41
|
+
case SystemMessage():
|
|
42
|
+
system = "\n".join(c.content for c in msg.contents if isinstance(c, TextContent))
|
|
43
|
+
case UserMessage():
|
|
44
|
+
parts: list[genai_types.Part] = []
|
|
45
|
+
for item in msg.contents:
|
|
46
|
+
match item:
|
|
47
|
+
case TextContent():
|
|
48
|
+
parts.append(genai_types.Part(text=item.content))
|
|
49
|
+
case ImageContent():
|
|
50
|
+
for b64, mime in item.to_base64():
|
|
51
|
+
parts.append(genai_types.Part(
|
|
52
|
+
inline_data=genai_types.Blob(mime_type=mime or "image/png", data=b64),
|
|
53
|
+
))
|
|
54
|
+
if parts:
|
|
55
|
+
contents.append(genai_types.Content(role="user", parts=parts))
|
|
56
|
+
case AssistantMessage():
|
|
57
|
+
parts = []
|
|
58
|
+
for item in msg.contents:
|
|
59
|
+
match item:
|
|
60
|
+
case TextContent():
|
|
61
|
+
parts.append(genai_types.Part(text=item.content))
|
|
62
|
+
case ToolCallContent():
|
|
63
|
+
parts.append(genai_types.Part(
|
|
64
|
+
function_call=genai_types.FunctionCall(
|
|
65
|
+
name=item.name,
|
|
66
|
+
args=item.args,
|
|
67
|
+
),
|
|
68
|
+
))
|
|
69
|
+
if parts:
|
|
70
|
+
contents.append(genai_types.Content(role="model", parts=parts))
|
|
71
|
+
case ToolMessage():
|
|
72
|
+
parts = []
|
|
73
|
+
for content in msg.contents:
|
|
74
|
+
if isinstance(content, ToolResultContent):
|
|
75
|
+
parts.append(genai_types.Part(
|
|
76
|
+
function_response=genai_types.FunctionResponse(
|
|
77
|
+
name=content.id,
|
|
78
|
+
response={"result": content.content},
|
|
79
|
+
),
|
|
80
|
+
))
|
|
81
|
+
if parts:
|
|
82
|
+
contents.append(genai_types.Content(role="user", parts=parts))
|
|
83
|
+
|
|
84
|
+
return system, contents
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _response_schema(response_format: Any | None) -> dict[str, Any] | None:
|
|
88
|
+
structured = normalize_structured_response_format(response_format)
|
|
89
|
+
return structured.schema if structured is not None else None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class GeminiGenerateAPI(BaseAPI):
|
|
93
|
+
def __init__(self, options: LLMOptions) -> None:
|
|
94
|
+
super().__init__(options)
|
|
95
|
+
self._client = genai.Client(api_key=options.api_key)
|
|
96
|
+
|
|
97
|
+
def _build_config(
|
|
98
|
+
self,
|
|
99
|
+
tools: Optional[list[Tool]] = None,
|
|
100
|
+
response_format: Any | None = None,
|
|
101
|
+
) -> genai_types.GenerateContentConfig:
|
|
102
|
+
params: dict[str, Any] = {
|
|
103
|
+
"temperature": self.options.temperature,
|
|
104
|
+
}
|
|
105
|
+
if self.options.max_tokens is not None:
|
|
106
|
+
params["max_output_tokens"] = self.options.max_tokens
|
|
107
|
+
schema = _response_schema(response_format)
|
|
108
|
+
if schema is not None:
|
|
109
|
+
params["response_mime_type"] = "application/json"
|
|
110
|
+
params["response_schema"] = schema
|
|
111
|
+
|
|
112
|
+
budget = None
|
|
113
|
+
if self.options.thinking_level is not None and self.options.thinking_level != ThinkingLevel.Off:
|
|
114
|
+
budgets = self.options.thinking_budgets or ThinkingBudgets()
|
|
115
|
+
budget = budgets.get(self.options.thinking_level)
|
|
116
|
+
if budget is not None:
|
|
117
|
+
params["thinking_config"] = genai_types.ThinkingConfig(
|
|
118
|
+
thinking_budget=budget,
|
|
119
|
+
include_thoughts=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if tools:
|
|
123
|
+
params["tools"] = [
|
|
124
|
+
genai_types.Tool(
|
|
125
|
+
function_declarations=[
|
|
126
|
+
genai_types.FunctionDeclaration(
|
|
127
|
+
name=t.name,
|
|
128
|
+
description=t.description,
|
|
129
|
+
parameters=t.schema.model_json_schema(),
|
|
130
|
+
)
|
|
131
|
+
for t in tools
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
return genai_types.GenerateContentConfig(**params)
|
|
137
|
+
|
|
138
|
+
async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
|
|
139
|
+
system, contents = _messages_to_gemini(context.messages)
|
|
140
|
+
config = self._build_config(
|
|
141
|
+
tools=context.tools or None,
|
|
142
|
+
response_format=context.response_format,
|
|
143
|
+
)
|
|
144
|
+
effective_system = context.system_prompt or system
|
|
145
|
+
if effective_system:
|
|
146
|
+
config.system_instruction = effective_system
|
|
147
|
+
|
|
148
|
+
if self.options.on_payload:
|
|
149
|
+
payload = {"config": config, "contents": contents}
|
|
150
|
+
modified = self.options.on_payload(payload)
|
|
151
|
+
if modified is not None:
|
|
152
|
+
config = modified.get("config", config)
|
|
153
|
+
contents = modified.get("contents", contents)
|
|
154
|
+
|
|
155
|
+
text_index = 0
|
|
156
|
+
thinking_index = 0
|
|
157
|
+
tool_index = 0
|
|
158
|
+
text_started = False
|
|
159
|
+
thinking_started = False
|
|
160
|
+
text_buf = ""
|
|
161
|
+
thinking_buf = ""
|
|
162
|
+
_input_tokens = 0
|
|
163
|
+
_output_tokens = 0
|
|
164
|
+
_cache_read_tokens = 0
|
|
165
|
+
|
|
166
|
+
yield StartEvent()
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
async for chunk in await self._client.aio.models.generate_content_stream(
|
|
170
|
+
model=model.id,
|
|
171
|
+
contents=contents,
|
|
172
|
+
config=config,
|
|
173
|
+
):
|
|
174
|
+
if self._cancelled():
|
|
175
|
+
yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
|
|
176
|
+
return
|
|
177
|
+
um = getattr(chunk, 'usage_metadata', None)
|
|
178
|
+
if um:
|
|
179
|
+
_input_tokens = getattr(um, 'prompt_token_count', 0) or 0
|
|
180
|
+
_output_tokens = getattr(um, 'candidates_token_count', 0) or 0
|
|
181
|
+
_cache_read_tokens = getattr(um, 'cached_content_token_count', 0) or 0
|
|
182
|
+
|
|
183
|
+
if not chunk.candidates:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
candidate = chunk.candidates[0]
|
|
187
|
+
if candidate.content and candidate.content.parts:
|
|
188
|
+
for part in candidate.content.parts:
|
|
189
|
+
if getattr(part, "thought", False) and part.text:
|
|
190
|
+
if not thinking_started:
|
|
191
|
+
yield ThinkingStartEvent(thinking=None)
|
|
192
|
+
thinking_started = True
|
|
193
|
+
thinking_buf += part.text
|
|
194
|
+
yield ThinkingDeltaEvent(thinking=ThinkingContent(content=part.text))
|
|
195
|
+
elif part.text:
|
|
196
|
+
if thinking_started:
|
|
197
|
+
yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
|
|
198
|
+
thinking_started = False
|
|
199
|
+
thinking_index += 1
|
|
200
|
+
thinking_buf = ""
|
|
201
|
+
if not text_started:
|
|
202
|
+
yield TextStartEvent(text=TextContent(content=""))
|
|
203
|
+
text_started = True
|
|
204
|
+
text_buf += part.text
|
|
205
|
+
yield TextDeltaEvent(text=TextContent(content=part.text))
|
|
206
|
+
elif part.function_call:
|
|
207
|
+
fc = part.function_call
|
|
208
|
+
tool_id = fc.name
|
|
209
|
+
args_str = json.dumps(dict(fc.args)) if fc.args else ""
|
|
210
|
+
yield ToolCallStartEvent(tool_call=ToolCallContent(id=tool_id, name=fc.name))
|
|
211
|
+
yield ToolCallDeltaEvent(tool_call=ToolCallContent(id=tool_id))
|
|
212
|
+
yield ToolCallEndEvent(tool_call=ToolCallContent(id=tool_id, name=fc.name, args=json.loads(args_str) if args_str else {}))
|
|
213
|
+
tool_index += 1
|
|
214
|
+
|
|
215
|
+
finish_reason = getattr(candidate, "finish_reason", None)
|
|
216
|
+
if finish_reason and str(finish_reason) not in ("", "FINISH_REASON_UNSPECIFIED"):
|
|
217
|
+
if thinking_started:
|
|
218
|
+
yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
|
|
219
|
+
if text_started:
|
|
220
|
+
yield TextEndEvent(text=TextContent(content=text_buf))
|
|
221
|
+
reason_str = finish_reason.name if hasattr(finish_reason, "name") else str(finish_reason)
|
|
222
|
+
stop = StopReason.ToolCalls if tool_index > 0 else _STOP_REASON.get(reason_str, StopReason.Stop)
|
|
223
|
+
yield EndEvent(reason=stop, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
yield ErrorEvent(reason=StopReason.Abort, error=str(exc))
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if thinking_started:
|
|
231
|
+
yield ThinkingEndEvent(thinking=ThinkingContent(content=thinking_buf))
|
|
232
|
+
if text_started:
|
|
233
|
+
yield TextEndEvent(text=TextContent(content=text_buf))
|
|
234
|
+
yield EndEvent(reason=StopReason.Stop, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)
|
|
@@ -0,0 +1,172 @@
|
|
|
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.provider.oauth.github_copilot import get_copilot_base_url
|
|
10
|
+
from tau.inference.types import (
|
|
11
|
+
LLMContext, LLMEvent, LLMOptions, StopReason, ThinkingLevel,
|
|
12
|
+
StartEvent, EndEvent, ErrorEvent,
|
|
13
|
+
TextStartEvent, TextDeltaEvent, TextEndEvent,
|
|
14
|
+
ToolCallStartEvent, ToolCallDeltaEvent, ToolCallEndEvent,
|
|
15
|
+
)
|
|
16
|
+
from tau.message.types import (
|
|
17
|
+
SystemMessage, UserMessage, AssistantMessage, ToolMessage,
|
|
18
|
+
TextContent, ImageContent, ToolCallContent, ToolResultContent,
|
|
19
|
+
)
|
|
20
|
+
from typing import Optional, TYPE_CHECKING
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from tau.tool.types import Tool
|
|
23
|
+
|
|
24
|
+
_COPILOT_HEADERS = {
|
|
25
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
26
|
+
"Editor-Version": "vscode/1.107.0",
|
|
27
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
28
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_STOP_REASON: dict[str, StopReason] = {
|
|
32
|
+
"stop": StopReason.Stop,
|
|
33
|
+
"length": StopReason.Length,
|
|
34
|
+
"tool_calls": StopReason.ToolCalls,
|
|
35
|
+
"content_filter": StopReason.ContentFilter,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GitHubCopilotChatAPI(BaseAPI):
|
|
41
|
+
"""Streaming LLM API adapter for the GitHub Copilot Chat endpoint (OpenAI-compatible)."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, options: LLMOptions) -> None:
|
|
44
|
+
"""Resolve the Copilot base URL and initialise the AsyncOpenAI client with Copilot headers."""
|
|
45
|
+
super().__init__(options)
|
|
46
|
+
base_url = options.base_url or get_copilot_base_url(options.api_key)
|
|
47
|
+
self._client = AsyncOpenAI(
|
|
48
|
+
api_key=options.api_key or "github-copilot",
|
|
49
|
+
base_url=base_url,
|
|
50
|
+
default_headers={**_COPILOT_HEADERS, **(options.headers or {})},
|
|
51
|
+
max_retries=options.max_retries,
|
|
52
|
+
timeout=options.timeout.total_seconds(),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _build_params(self, model: Model, messages: list[dict[str, Any]], tools: Optional[list[Tool]] = None) -> dict[str, Any]:
|
|
56
|
+
"""Assemble the Copilot Chat Completions request payload."""
|
|
57
|
+
params: dict[str, Any] = {
|
|
58
|
+
"model": model.id,
|
|
59
|
+
"messages": messages,
|
|
60
|
+
"temperature": self.options.temperature,
|
|
61
|
+
}
|
|
62
|
+
if self.options.max_tokens is not None:
|
|
63
|
+
params["max_completion_tokens"] = self.options.max_tokens
|
|
64
|
+
|
|
65
|
+
if tools:
|
|
66
|
+
params["tools"] = [
|
|
67
|
+
{
|
|
68
|
+
"type": "function",
|
|
69
|
+
"function": {
|
|
70
|
+
"name": tool.name,
|
|
71
|
+
"description": tool.description,
|
|
72
|
+
"parameters": tool.schema.model_json_schema(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for tool in tools
|
|
76
|
+
]
|
|
77
|
+
params["tool_choice"] = "auto"
|
|
78
|
+
|
|
79
|
+
return params
|
|
80
|
+
|
|
81
|
+
async def stream(self, context: LLMContext, model: Model) -> AsyncGenerator[LLMEvent, None]: # type: ignore[override]
|
|
82
|
+
"""Stream LLMEvents from the GitHub Copilot Chat API."""
|
|
83
|
+
chat_messages = openai_messages_to_chat(context.messages)
|
|
84
|
+
if context.system_prompt:
|
|
85
|
+
chat_messages = [{"role": "system", "content": context.system_prompt}] + chat_messages
|
|
86
|
+
params = self._build_params(model, chat_messages, tools=context.tools or None)
|
|
87
|
+
response_format = openai_response_format(context.response_format)
|
|
88
|
+
if response_format is not None:
|
|
89
|
+
params["response_format"] = response_format
|
|
90
|
+
|
|
91
|
+
if self.options.on_payload:
|
|
92
|
+
modified = self.options.on_payload(params)
|
|
93
|
+
if modified is not None:
|
|
94
|
+
params = modified
|
|
95
|
+
|
|
96
|
+
text_started = False
|
|
97
|
+
text_buf = ""
|
|
98
|
+
tool_started: dict[int, bool] = {}
|
|
99
|
+
tool_bufs: dict[int, str] = {}
|
|
100
|
+
tool_meta: dict[int, dict[str, str]] = {}
|
|
101
|
+
_input_tokens = 0
|
|
102
|
+
_output_tokens = 0
|
|
103
|
+
_cache_read_tokens = 0
|
|
104
|
+
|
|
105
|
+
yield StartEvent()
|
|
106
|
+
|
|
107
|
+
# async with closes the SDK stream (and its httpx response) on every
|
|
108
|
+
# exit path — cancellation return or an upstream GeneratorExit — instead
|
|
109
|
+
# of leaving it to the GC asyncgen finalizer.
|
|
110
|
+
async with await self._client.chat.completions.create(**params, stream=True, stream_options={"include_usage": True}) as sdk_stream:
|
|
111
|
+
async for chunk in sdk_stream:
|
|
112
|
+
if self._cancelled():
|
|
113
|
+
yield ErrorEvent(reason=StopReason.Abort, error="Cancelled")
|
|
114
|
+
return
|
|
115
|
+
usage_data = getattr(chunk, 'usage', None)
|
|
116
|
+
if usage_data:
|
|
117
|
+
_input_tokens = getattr(usage_data, 'prompt_tokens', 0) or 0
|
|
118
|
+
_output_tokens = getattr(usage_data, 'completion_tokens', 0) or 0
|
|
119
|
+
_details = getattr(usage_data, 'prompt_tokens_details', None)
|
|
120
|
+
_cache_read_tokens = getattr(_details, 'cached_tokens', 0) or 0
|
|
121
|
+
choice = chunk.choices[0] if chunk.choices else None
|
|
122
|
+
if choice is None:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
delta = choice.delta
|
|
126
|
+
|
|
127
|
+
if delta.content:
|
|
128
|
+
if not text_started:
|
|
129
|
+
yield TextStartEvent(text=TextContent(content=""))
|
|
130
|
+
text_started = True
|
|
131
|
+
text_buf += delta.content
|
|
132
|
+
yield TextDeltaEvent(text=TextContent(content=delta.content))
|
|
133
|
+
|
|
134
|
+
if delta.tool_calls:
|
|
135
|
+
for tc in delta.tool_calls:
|
|
136
|
+
idx = tc.index
|
|
137
|
+
if idx not in tool_started:
|
|
138
|
+
tool_started[idx] = True
|
|
139
|
+
tool_bufs[idx] = ""
|
|
140
|
+
tool_meta[idx] = {
|
|
141
|
+
"id": tc.id or "",
|
|
142
|
+
"name": tc.function.name or "" if tc.function else "",
|
|
143
|
+
}
|
|
144
|
+
yield ToolCallStartEvent(tool_call=ToolCallContent(
|
|
145
|
+
id=tool_meta[idx]["id"],
|
|
146
|
+
name=tool_meta[idx]["name"],
|
|
147
|
+
))
|
|
148
|
+
if tc.function and tc.function.arguments:
|
|
149
|
+
tool_bufs[idx] += tc.function.arguments
|
|
150
|
+
yield ToolCallDeltaEvent(tool_call=ToolCallContent(id=tool_meta[idx]["id"]))
|
|
151
|
+
|
|
152
|
+
if choice.finish_reason:
|
|
153
|
+
if text_started:
|
|
154
|
+
yield TextEndEvent(text=TextContent(content=text_buf))
|
|
155
|
+
text_started = False
|
|
156
|
+
text_buf = ""
|
|
157
|
+
|
|
158
|
+
for idx in sorted(tool_started):
|
|
159
|
+
args_str = tool_bufs[idx].strip()
|
|
160
|
+
args = parse_tool_args(args_str)
|
|
161
|
+
|
|
162
|
+
yield ToolCallEndEvent(tool_call=ToolCallContent(
|
|
163
|
+
id=tool_meta[idx]["id"],
|
|
164
|
+
name=tool_meta[idx]["name"],
|
|
165
|
+
args=args,
|
|
166
|
+
))
|
|
167
|
+
tool_started.clear()
|
|
168
|
+
tool_bufs.clear()
|
|
169
|
+
tool_meta.clear()
|
|
170
|
+
|
|
171
|
+
stop_reason = _STOP_REASON.get(choice.finish_reason, StopReason.Stop)
|
|
172
|
+
yield EndEvent(reason=stop_reason, input_tokens=_input_tokens, output_tokens=_output_tokens, cache_read_tokens=_cache_read_tokens)
|