klaude-code 1.2.8__py3-none-any.whl → 1.2.10__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.
- klaude_code/auth/codex/__init__.py +1 -1
- klaude_code/cli/main.py +12 -1
- klaude_code/cli/runtime.py +7 -11
- klaude_code/command/__init__.py +68 -21
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +5 -2
- klaude_code/command/diff_cmd.py +5 -2
- klaude_code/command/export_cmd.py +7 -4
- klaude_code/command/help_cmd.py +6 -2
- klaude_code/command/model_cmd.py +5 -2
- klaude_code/command/prompt-deslop.md +14 -0
- klaude_code/command/prompt_command.py +8 -3
- klaude_code/command/refresh_cmd.py +6 -2
- klaude_code/command/registry.py +17 -5
- klaude_code/command/release_notes_cmd.py +89 -0
- klaude_code/command/status_cmd.py +98 -56
- klaude_code/command/terminal_setup_cmd.py +7 -4
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/agent.py +66 -26
- klaude_code/core/executor.py +2 -2
- klaude_code/core/manager/agent_manager.py +6 -7
- klaude_code/core/manager/llm_clients.py +47 -22
- klaude_code/core/manager/llm_clients_builder.py +19 -7
- klaude_code/core/manager/sub_agent_manager.py +6 -2
- klaude_code/core/prompt.py +38 -28
- klaude_code/core/reminders.py +4 -7
- klaude_code/core/task.py +59 -40
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/_utils.py +30 -0
- klaude_code/core/tool/file/apply_patch_tool.py +1 -1
- klaude_code/core/tool/file/edit_tool.py +6 -31
- klaude_code/core/tool/file/multi_edit_tool.py +7 -32
- klaude_code/core/tool/file/read_tool.py +6 -18
- klaude_code/core/tool/file/write_tool.py +6 -31
- klaude_code/core/tool/memory/__init__.py +5 -0
- klaude_code/core/tool/memory/memory_tool.py +2 -2
- klaude_code/core/tool/memory/skill_loader.py +2 -1
- klaude_code/core/tool/memory/skill_tool.py +13 -0
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_context.py +21 -4
- klaude_code/core/tool/tool_runner.py +5 -8
- klaude_code/core/tool/web/mermaid_tool.py +1 -4
- klaude_code/core/turn.py +40 -37
- klaude_code/llm/__init__.py +2 -12
- klaude_code/llm/anthropic/client.py +14 -44
- klaude_code/llm/client.py +2 -2
- klaude_code/llm/codex/client.py +4 -3
- klaude_code/llm/input_common.py +0 -6
- klaude_code/llm/openai_compatible/client.py +31 -74
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream_processor.py +82 -0
- klaude_code/llm/openrouter/client.py +32 -62
- klaude_code/llm/openrouter/input.py +4 -27
- klaude_code/llm/registry.py +33 -7
- klaude_code/llm/responses/client.py +16 -48
- klaude_code/llm/responses/input.py +1 -1
- klaude_code/llm/usage.py +61 -11
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +11 -2
- klaude_code/protocol/model.py +147 -24
- klaude_code/protocol/op.py +1 -0
- klaude_code/protocol/sub_agent.py +5 -1
- klaude_code/session/export.py +56 -32
- klaude_code/session/session.py +43 -21
- klaude_code/session/templates/export_session.html +4 -1
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/modes/repl/__init__.py +1 -5
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/event_handler.py +153 -54
- klaude_code/ui/modes/repl/renderer.py +4 -4
- klaude_code/ui/renderers/developer.py +35 -25
- klaude_code/ui/renderers/metadata.py +68 -30
- klaude_code/ui/renderers/tools.py +53 -87
- klaude_code/ui/rich/markdown.py +5 -5
- klaude_code/ui/terminal/control.py +2 -2
- klaude_code/version.py +3 -3
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/METADATA +1 -1
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/RECORD +82 -78
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/entry_points.txt +0 -0
|
@@ -7,38 +7,13 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from klaude_code.core.tool.file._utils import file_exists, is_directory, read_text, write_text
|
|
10
11
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
11
12
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
12
13
|
from klaude_code.core.tool.tool_registry import register
|
|
13
14
|
from klaude_code.protocol import llm_param, model, tools
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def _is_directory(path: str) -> bool:
|
|
17
|
-
try:
|
|
18
|
-
return Path(path).is_dir()
|
|
19
|
-
except Exception:
|
|
20
|
-
return False
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _file_exists(path: str) -> bool:
|
|
24
|
-
try:
|
|
25
|
-
return Path(path).exists()
|
|
26
|
-
except Exception:
|
|
27
|
-
return False
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _write_text(path: str, content: str) -> None:
|
|
31
|
-
parent = Path(path).parent
|
|
32
|
-
parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
34
|
-
f.write(content)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _read_text(path: str) -> str:
|
|
38
|
-
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
39
|
-
return f.read()
|
|
40
|
-
|
|
41
|
-
|
|
42
17
|
class WriteArguments(BaseModel):
|
|
43
18
|
file_path: str
|
|
44
19
|
content: str
|
|
@@ -78,14 +53,14 @@ class WriteTool(ToolABC):
|
|
|
78
53
|
|
|
79
54
|
file_path = os.path.abspath(args.file_path)
|
|
80
55
|
|
|
81
|
-
if
|
|
56
|
+
if is_directory(file_path):
|
|
82
57
|
return model.ToolResultItem(
|
|
83
58
|
status="error",
|
|
84
59
|
output="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
|
|
85
60
|
)
|
|
86
61
|
|
|
87
62
|
file_tracker = get_current_file_tracker()
|
|
88
|
-
exists =
|
|
63
|
+
exists = file_exists(file_path)
|
|
89
64
|
|
|
90
65
|
if exists:
|
|
91
66
|
tracked_mtime: float | None = None
|
|
@@ -113,12 +88,12 @@ class WriteTool(ToolABC):
|
|
|
113
88
|
before = ""
|
|
114
89
|
if exists:
|
|
115
90
|
try:
|
|
116
|
-
before = await asyncio.to_thread(
|
|
91
|
+
before = await asyncio.to_thread(read_text, file_path)
|
|
117
92
|
except Exception:
|
|
118
93
|
before = ""
|
|
119
94
|
|
|
120
95
|
try:
|
|
121
|
-
await asyncio.to_thread(
|
|
96
|
+
await asyncio.to_thread(write_text, file_path, args.content)
|
|
122
97
|
except Exception as e: # pragma: no cover
|
|
123
98
|
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
124
99
|
|
|
@@ -140,7 +115,7 @@ class WriteTool(ToolABC):
|
|
|
140
115
|
)
|
|
141
116
|
)
|
|
142
117
|
diff_text = "\n".join(diff_lines)
|
|
143
|
-
ui_extra = model.
|
|
118
|
+
ui_extra = model.DiffTextUIExtra(diff_text=diff_text)
|
|
144
119
|
|
|
145
120
|
message = f"File {'overwritten' if exists else 'created'} successfully at: {file_path}"
|
|
146
121
|
return model.ToolResultItem(status="success", output=message, ui_extra=ui_extra)
|
|
@@ -100,7 +100,7 @@ def _format_numbered_line(line_no: int, content: str) -> str:
|
|
|
100
100
|
return f"{line_no:>6}|{content}"
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def _make_diff_ui_extra(before: str, after: str, path: str) -> model.
|
|
103
|
+
def _make_diff_ui_extra(before: str, after: str, path: str) -> model.DiffTextUIExtra:
|
|
104
104
|
diff_lines = list(
|
|
105
105
|
difflib.unified_diff(
|
|
106
106
|
before.splitlines(),
|
|
@@ -111,7 +111,7 @@ def _make_diff_ui_extra(before: str, after: str, path: str) -> model.ToolResultU
|
|
|
111
111
|
)
|
|
112
112
|
)
|
|
113
113
|
diff_text = "\n".join(diff_lines)
|
|
114
|
-
return model.
|
|
114
|
+
return model.DiffTextUIExtra(diff_text=diff_text)
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
@register(tools.MEMORY)
|
|
@@ -13,15 +13,26 @@ class SkillTool(ToolABC):
|
|
|
13
13
|
"""Tool to execute/load a skill within the main conversation"""
|
|
14
14
|
|
|
15
15
|
_skill_loader: SkillLoader | None = None
|
|
16
|
+
_discovery_done: bool = False
|
|
16
17
|
|
|
17
18
|
@classmethod
|
|
18
19
|
def set_skill_loader(cls, loader: SkillLoader) -> None:
|
|
19
20
|
"""Set the skill loader instance"""
|
|
20
21
|
cls._skill_loader = loader
|
|
22
|
+
cls._discovery_done = False
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _ensure_skills_discovered(cls) -> None:
|
|
26
|
+
if cls._discovery_done:
|
|
27
|
+
return
|
|
28
|
+
if cls._skill_loader is not None:
|
|
29
|
+
cls._skill_loader.discover_skills()
|
|
30
|
+
cls._discovery_done = True
|
|
21
31
|
|
|
22
32
|
@classmethod
|
|
23
33
|
def schema(cls) -> llm_param.ToolSchema:
|
|
24
34
|
"""Generate schema with embedded available skills metadata"""
|
|
35
|
+
cls._ensure_skills_discovered()
|
|
25
36
|
skills_xml = cls._generate_skills_xml()
|
|
26
37
|
|
|
27
38
|
return llm_param.ToolSchema(
|
|
@@ -69,6 +80,8 @@ class SkillTool(ToolABC):
|
|
|
69
80
|
output=f"Invalid arguments: {e}",
|
|
70
81
|
)
|
|
71
82
|
|
|
83
|
+
cls._ensure_skills_discovered()
|
|
84
|
+
|
|
72
85
|
if not cls._skill_loader:
|
|
73
86
|
return model.ToolResultItem(
|
|
74
87
|
status="error",
|
|
@@ -79,5 +79,6 @@ class SubAgentTool(ToolABC):
|
|
|
79
79
|
return model.ToolResultItem(
|
|
80
80
|
status="success" if not result.error else "error",
|
|
81
81
|
output=result.task_result or "",
|
|
82
|
-
ui_extra=model.
|
|
82
|
+
ui_extra=model.SessionIdUIExtra(session_id=result.session_id),
|
|
83
|
+
task_metadata=result.task_metadata,
|
|
83
84
|
)
|
|
@@ -116,6 +116,6 @@ Your todo list has changed. DO NOT mention this explicitly to the user. Here are
|
|
|
116
116
|
return model.ToolResultItem(
|
|
117
117
|
status="success",
|
|
118
118
|
output=response,
|
|
119
|
-
ui_extra=model.
|
|
119
|
+
ui_extra=model.TodoListUIExtra(todo_list=ui_extra),
|
|
120
120
|
side_effects=[model.ToolSideEffect.TODO_CHANGE],
|
|
121
121
|
)
|
|
@@ -99,6 +99,6 @@ class UpdatePlanTool(ToolABC):
|
|
|
99
99
|
return model.ToolResultItem(
|
|
100
100
|
status="success",
|
|
101
101
|
output="Plan updated",
|
|
102
|
-
ui_extra=model.
|
|
102
|
+
ui_extra=model.TodoListUIExtra(todo_list=ui_extra),
|
|
103
103
|
side_effects=[model.ToolSideEffect.TODO_CHANGE],
|
|
104
104
|
)
|
|
@@ -22,6 +22,19 @@ class TodoContext:
|
|
|
22
22
|
set_todos: Callable[[list[model.TodoItem]], None]
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
@dataclass
|
|
26
|
+
class SessionTodoStore:
|
|
27
|
+
"""Adapter exposing session todos through an explicit interface."""
|
|
28
|
+
|
|
29
|
+
session: Session
|
|
30
|
+
|
|
31
|
+
def get(self) -> list[model.TodoItem]:
|
|
32
|
+
return self.session.todos
|
|
33
|
+
|
|
34
|
+
def set(self, todos: list[model.TodoItem]) -> None:
|
|
35
|
+
self.session.todos = todos
|
|
36
|
+
|
|
37
|
+
|
|
25
38
|
@dataclass
|
|
26
39
|
class ToolContextToken:
|
|
27
40
|
"""Tokens used to restore tool execution context.
|
|
@@ -55,10 +68,7 @@ def set_tool_context_from_session(session: Session) -> ToolContextToken:
|
|
|
55
68
|
"""
|
|
56
69
|
|
|
57
70
|
file_tracker_token = current_file_tracker_var.set(session.file_tracker)
|
|
58
|
-
todo_ctx =
|
|
59
|
-
get_todos=lambda: session.todos,
|
|
60
|
-
set_todos=lambda todos: setattr(session, "todos", todos),
|
|
61
|
-
)
|
|
71
|
+
todo_ctx = build_todo_context(session)
|
|
62
72
|
todo_token = current_todo_context_var.set(todo_ctx)
|
|
63
73
|
return ToolContextToken(file_tracker_token=file_tracker_token, todo_token=todo_token)
|
|
64
74
|
|
|
@@ -87,6 +97,13 @@ def tool_context(
|
|
|
87
97
|
reset_tool_context(token)
|
|
88
98
|
|
|
89
99
|
|
|
100
|
+
def build_todo_context(session: Session) -> TodoContext:
|
|
101
|
+
"""Create a TodoContext backed by the given session."""
|
|
102
|
+
|
|
103
|
+
store = SessionTodoStore(session)
|
|
104
|
+
return TodoContext(get_todos=store.get, set_todos=store.set)
|
|
105
|
+
|
|
106
|
+
|
|
90
107
|
def get_current_file_tracker() -> MutableMapping[str, float] | None:
|
|
91
108
|
"""Return the current file tracker mapping for this tool context."""
|
|
92
109
|
|
|
@@ -34,13 +34,10 @@ async def run_tool(tool_call: model.ToolCallItem, registry: dict[str, type[ToolA
|
|
|
34
34
|
truncation_result = truncate_tool_output(tool_result.output, tool_call)
|
|
35
35
|
tool_result.output = truncation_result.output
|
|
36
36
|
if truncation_result.was_truncated and truncation_result.saved_file_path:
|
|
37
|
-
tool_result.ui_extra = model.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
original_length=truncation_result.original_length,
|
|
42
|
-
truncated_length=truncation_result.truncated_length,
|
|
43
|
-
),
|
|
37
|
+
tool_result.ui_extra = model.TruncationUIExtra(
|
|
38
|
+
saved_file_path=truncation_result.saved_file_path,
|
|
39
|
+
original_length=truncation_result.original_length,
|
|
40
|
+
truncated_length=truncation_result.truncated_length,
|
|
44
41
|
)
|
|
45
42
|
return tool_result
|
|
46
43
|
except asyncio.CancelledError:
|
|
@@ -244,7 +241,7 @@ class ToolExecutor:
|
|
|
244
241
|
for side_effect in side_effects:
|
|
245
242
|
if side_effect == model.ToolSideEffect.TODO_CHANGE:
|
|
246
243
|
todos: list[model.TodoItem] | None = None
|
|
247
|
-
if tool_result.ui_extra
|
|
244
|
+
if isinstance(tool_result.ui_extra, model.TodoListUIExtra):
|
|
248
245
|
todos = tool_result.ui_extra.todo_list.todos
|
|
249
246
|
if todos is not None:
|
|
250
247
|
side_effect_events.append(ToolExecutionTodoChange(todos=todos))
|
|
@@ -49,10 +49,7 @@ class MermaidTool(ToolABC):
|
|
|
49
49
|
|
|
50
50
|
link = cls._build_link(args.code)
|
|
51
51
|
line_count = cls._count_lines(args.code)
|
|
52
|
-
ui_extra = model.
|
|
53
|
-
type=model.ToolResultUIExtraType.MERMAID_LINK,
|
|
54
|
-
mermaid_link=model.MermaidLinkUIExtra(link=link, line_count=line_count),
|
|
55
|
-
)
|
|
52
|
+
ui_extra = model.MermaidLinkUIExtra(link=link, line_count=line_count)
|
|
56
53
|
output = f"Mermaid diagram rendered successfully ({line_count} lines)."
|
|
57
54
|
return model.ToolResultItem(status="success", output=output, ui_extra=ui_extra)
|
|
58
55
|
|
klaude_code/core/turn.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import AsyncGenerator
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from klaude_code.core.tool import ToolABC, tool_context
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from klaude_code.core.task import SessionContext
|
|
5
11
|
|
|
6
|
-
from klaude_code.core.tool import TodoContext, ToolABC, tool_context
|
|
7
12
|
from klaude_code.core.tool.tool_runner import (
|
|
8
13
|
ToolExecutionCallStarted,
|
|
9
14
|
ToolExecutionResult,
|
|
@@ -26,16 +31,11 @@ class TurnError(Exception):
|
|
|
26
31
|
class TurnExecutionContext:
|
|
27
32
|
"""Execution context required to run a single turn."""
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
get_conversation_history: Callable[[], list[model.ConversationItem]]
|
|
31
|
-
append_history: Callable[[Sequence[model.ConversationItem]], None]
|
|
34
|
+
session_ctx: SessionContext
|
|
32
35
|
llm_client: LLMClientABC
|
|
33
36
|
system_prompt: str | None
|
|
34
37
|
tools: list[llm_param.ToolSchema]
|
|
35
38
|
tool_registry: dict[str, type[ToolABC]]
|
|
36
|
-
# For tool context
|
|
37
|
-
file_tracker: MutableMapping[str, float]
|
|
38
|
-
todo_context: TodoContext
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@dataclass
|
|
@@ -74,6 +74,7 @@ def build_events_from_tool_executor_event(session_id: str, event: ToolExecutorEv
|
|
|
74
74
|
result=tool_result.output or "",
|
|
75
75
|
ui_extra=tool_result.ui_extra,
|
|
76
76
|
status=tool_result.status,
|
|
77
|
+
task_metadata=tool_result.task_metadata,
|
|
77
78
|
)
|
|
78
79
|
)
|
|
79
80
|
case ToolExecutionTodoChange(todos=todos):
|
|
@@ -97,18 +98,18 @@ class TurnExecutor:
|
|
|
97
98
|
def __init__(self, context: TurnExecutionContext) -> None:
|
|
98
99
|
self._context = context
|
|
99
100
|
self._tool_executor: ToolExecutor | None = None
|
|
100
|
-
self.
|
|
101
|
+
self._turn_result: TurnResult | None = None
|
|
101
102
|
|
|
102
103
|
@property
|
|
103
104
|
def has_tool_call(self) -> bool:
|
|
104
|
-
return self.
|
|
105
|
+
return bool(self._turn_result and self._turn_result.tool_calls)
|
|
105
106
|
|
|
106
107
|
def cancel(self) -> list[events.Event]:
|
|
107
108
|
"""Cancel running tools and return any resulting events."""
|
|
108
109
|
ui_events: list[events.Event] = []
|
|
109
110
|
if self._tool_executor is not None:
|
|
110
111
|
for exec_event in self._tool_executor.cancel():
|
|
111
|
-
for ui_event in build_events_from_tool_executor_event(self._context.session_id, exec_event):
|
|
112
|
+
for ui_event in build_events_from_tool_executor_event(self._context.session_ctx.session_id, exec_event):
|
|
112
113
|
ui_events.append(ui_event)
|
|
113
114
|
self._tool_executor = None
|
|
114
115
|
return ui_events
|
|
@@ -120,44 +121,45 @@ class TurnExecutor:
|
|
|
120
121
|
TurnError: If the turn fails (stream error or non-completed status).
|
|
121
122
|
"""
|
|
122
123
|
ctx = self._context
|
|
124
|
+
session_ctx = ctx.session_ctx
|
|
123
125
|
|
|
124
|
-
yield events.TurnStartEvent(session_id=
|
|
126
|
+
yield events.TurnStartEvent(session_id=session_ctx.session_id)
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
self._turn_result = TurnResult(
|
|
127
129
|
reasoning_items=[],
|
|
128
130
|
assistant_message=None,
|
|
129
131
|
tool_calls=[],
|
|
130
132
|
stream_error=None,
|
|
131
133
|
)
|
|
132
134
|
|
|
133
|
-
async for event in self._consume_llm_stream(
|
|
135
|
+
async for event in self._consume_llm_stream(self._turn_result):
|
|
134
136
|
yield event
|
|
135
137
|
|
|
136
|
-
if
|
|
137
|
-
|
|
138
|
-
yield events.TurnEndEvent(session_id=
|
|
139
|
-
raise TurnError(
|
|
138
|
+
if self._turn_result.stream_error is not None:
|
|
139
|
+
session_ctx.append_history([self._turn_result.stream_error])
|
|
140
|
+
yield events.TurnEndEvent(session_id=session_ctx.session_id)
|
|
141
|
+
raise TurnError(self._turn_result.stream_error.error)
|
|
140
142
|
|
|
141
|
-
self._append_success_history(
|
|
142
|
-
self._has_tool_call = bool(turn_result.tool_calls)
|
|
143
|
+
self._append_success_history(self._turn_result)
|
|
143
144
|
|
|
144
|
-
if
|
|
145
|
-
async for ui_event in self._run_tool_executor(
|
|
145
|
+
if self._turn_result.tool_calls:
|
|
146
|
+
async for ui_event in self._run_tool_executor(self._turn_result.tool_calls):
|
|
146
147
|
yield ui_event
|
|
147
148
|
|
|
148
|
-
yield events.TurnEndEvent(session_id=
|
|
149
|
+
yield events.TurnEndEvent(session_id=session_ctx.session_id)
|
|
149
150
|
|
|
150
151
|
async def _consume_llm_stream(self, turn_result: TurnResult) -> AsyncGenerator[events.Event, None]:
|
|
151
152
|
"""Stream events from LLM and update turn_result in place."""
|
|
152
153
|
|
|
153
154
|
ctx = self._context
|
|
155
|
+
session_ctx = ctx.session_ctx
|
|
154
156
|
async for response_item in ctx.llm_client.call(
|
|
155
157
|
llm_param.LLMCallParameter(
|
|
156
|
-
input=
|
|
158
|
+
input=session_ctx.get_conversation_history(),
|
|
157
159
|
system=ctx.system_prompt,
|
|
158
160
|
tools=ctx.tools,
|
|
159
161
|
store=False,
|
|
160
|
-
session_id=
|
|
162
|
+
session_id=session_ctx.session_id,
|
|
161
163
|
)
|
|
162
164
|
):
|
|
163
165
|
log_debug(
|
|
@@ -174,7 +176,7 @@ class TurnExecutor:
|
|
|
174
176
|
yield events.ThinkingEvent(
|
|
175
177
|
content=item.content,
|
|
176
178
|
response_id=item.response_id,
|
|
177
|
-
session_id=
|
|
179
|
+
session_id=session_ctx.session_id,
|
|
178
180
|
)
|
|
179
181
|
case model.ReasoningEncryptedItem() as item:
|
|
180
182
|
turn_result.reasoning_items.append(item)
|
|
@@ -182,18 +184,18 @@ class TurnExecutor:
|
|
|
182
184
|
yield events.AssistantMessageDeltaEvent(
|
|
183
185
|
content=item.content,
|
|
184
186
|
response_id=item.response_id,
|
|
185
|
-
session_id=
|
|
187
|
+
session_id=session_ctx.session_id,
|
|
186
188
|
)
|
|
187
189
|
case model.AssistantMessageItem() as item:
|
|
188
190
|
turn_result.assistant_message = item
|
|
189
191
|
yield events.AssistantMessageEvent(
|
|
190
192
|
content=item.content or "",
|
|
191
193
|
response_id=item.response_id,
|
|
192
|
-
session_id=
|
|
194
|
+
session_id=session_ctx.session_id,
|
|
193
195
|
)
|
|
194
196
|
case model.ResponseMetadataItem() as item:
|
|
195
197
|
yield events.ResponseMetadataEvent(
|
|
196
|
-
session_id=
|
|
198
|
+
session_id=session_ctx.session_id,
|
|
197
199
|
metadata=item,
|
|
198
200
|
)
|
|
199
201
|
case model.StreamErrorItem() as item:
|
|
@@ -206,7 +208,7 @@ class TurnExecutor:
|
|
|
206
208
|
)
|
|
207
209
|
case model.ToolCallStartItem() as item:
|
|
208
210
|
yield events.TurnToolCallStartEvent(
|
|
209
|
-
session_id=
|
|
211
|
+
session_id=session_ctx.session_id,
|
|
210
212
|
response_id=item.response_id,
|
|
211
213
|
tool_call_id=item.call_id,
|
|
212
214
|
tool_name=item.name,
|
|
@@ -219,27 +221,28 @@ class TurnExecutor:
|
|
|
219
221
|
|
|
220
222
|
def _append_success_history(self, turn_result: TurnResult) -> None:
|
|
221
223
|
"""Persist successful turn artifacts to conversation history."""
|
|
222
|
-
|
|
224
|
+
session_ctx = self._context.session_ctx
|
|
223
225
|
if turn_result.reasoning_items:
|
|
224
|
-
|
|
226
|
+
session_ctx.append_history(turn_result.reasoning_items)
|
|
225
227
|
if turn_result.assistant_message:
|
|
226
|
-
|
|
228
|
+
session_ctx.append_history([turn_result.assistant_message])
|
|
227
229
|
if turn_result.tool_calls:
|
|
228
|
-
|
|
230
|
+
session_ctx.append_history(turn_result.tool_calls)
|
|
229
231
|
|
|
230
232
|
async def _run_tool_executor(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[events.Event, None]:
|
|
231
233
|
"""Run tools for the turn and translate executor events to UI events."""
|
|
232
234
|
|
|
233
235
|
ctx = self._context
|
|
234
|
-
|
|
236
|
+
session_ctx = ctx.session_ctx
|
|
237
|
+
with tool_context(session_ctx.file_tracker, session_ctx.todo_context):
|
|
235
238
|
executor = ToolExecutor(
|
|
236
239
|
registry=ctx.tool_registry,
|
|
237
|
-
append_history=
|
|
240
|
+
append_history=session_ctx.append_history,
|
|
238
241
|
)
|
|
239
242
|
self._tool_executor = executor
|
|
240
243
|
try:
|
|
241
244
|
async for exec_event in executor.run_tools(tool_calls):
|
|
242
|
-
for ui_event in build_events_from_tool_executor_event(
|
|
245
|
+
for ui_event in build_events_from_tool_executor_event(session_ctx.session_id, exec_event):
|
|
243
246
|
yield ui_event
|
|
244
247
|
finally:
|
|
245
248
|
self._tool_executor = None
|
klaude_code/llm/__init__.py
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
"""LLM package init.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
LLM clients are lazily loaded to avoid heavy imports at module load time.
|
|
4
|
+
Only LLMClientABC and create_llm_client are exposed.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from .anthropic import AnthropicClient
|
|
8
7
|
from .client import LLMClientABC
|
|
9
|
-
from .codex import CodexClient
|
|
10
|
-
from .openai_compatible import OpenAICompatibleClient
|
|
11
|
-
from .openrouter import OpenRouterClient
|
|
12
8
|
from .registry import create_llm_client
|
|
13
|
-
from .responses import ResponsesClient
|
|
14
9
|
|
|
15
10
|
__all__ = [
|
|
16
11
|
"LLMClientABC",
|
|
17
|
-
"ResponsesClient",
|
|
18
|
-
"OpenAICompatibleClient",
|
|
19
|
-
"OpenRouterClient",
|
|
20
|
-
"AnthropicClient",
|
|
21
|
-
"CodexClient",
|
|
22
12
|
"create_llm_client",
|
|
23
13
|
]
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import time
|
|
3
2
|
from collections.abc import AsyncGenerator
|
|
4
3
|
from typing import override
|
|
5
4
|
|
|
@@ -22,7 +21,7 @@ from klaude_code.llm.anthropic.input import convert_history_to_input, convert_sy
|
|
|
22
21
|
from klaude_code.llm.client import LLMClientABC, call_with_logged_payload
|
|
23
22
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
24
23
|
from klaude_code.llm.registry import register
|
|
25
|
-
from klaude_code.llm.usage import
|
|
24
|
+
from klaude_code.llm.usage import MetadataTracker, convert_anthropic_usage
|
|
26
25
|
from klaude_code.protocol import llm_param, model
|
|
27
26
|
from klaude_code.trace import DebugType, log_debug
|
|
28
27
|
|
|
@@ -47,9 +46,7 @@ class AnthropicClient(LLMClientABC):
|
|
|
47
46
|
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
48
47
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
first_token_time: float | None = None
|
|
52
|
-
last_token_time: float | None = None
|
|
49
|
+
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
53
50
|
|
|
54
51
|
messages = convert_history_to_input(param.input, param.model)
|
|
55
52
|
tools = convert_tool_schema(param.tools)
|
|
@@ -77,7 +74,7 @@ class AnthropicClient(LLMClientABC):
|
|
|
77
74
|
else anthropic.types.ThinkingConfigDisabledParam(
|
|
78
75
|
type="disabled",
|
|
79
76
|
),
|
|
80
|
-
extra_headers={"extra": json.dumps({"session_id": param.session_id})},
|
|
77
|
+
extra_headers={"extra": json.dumps({"session_id": param.session_id}, sort_keys=True)},
|
|
81
78
|
)
|
|
82
79
|
|
|
83
80
|
accumulated_thinking: list[str] = []
|
|
@@ -112,32 +109,24 @@ class AnthropicClient(LLMClientABC):
|
|
|
112
109
|
case BetaRawContentBlockDeltaEvent() as event:
|
|
113
110
|
match event.delta:
|
|
114
111
|
case BetaThinkingDelta() as delta:
|
|
115
|
-
|
|
116
|
-
first_token_time = time.time()
|
|
117
|
-
last_token_time = time.time()
|
|
112
|
+
metadata_tracker.record_token()
|
|
118
113
|
accumulated_thinking.append(delta.thinking)
|
|
119
114
|
case BetaSignatureDelta() as delta:
|
|
120
|
-
|
|
121
|
-
first_token_time = time.time()
|
|
122
|
-
last_token_time = time.time()
|
|
115
|
+
metadata_tracker.record_token()
|
|
123
116
|
yield model.ReasoningEncryptedItem(
|
|
124
117
|
encrypted_content=delta.signature,
|
|
125
118
|
response_id=response_id,
|
|
126
119
|
model=str(param.model),
|
|
127
120
|
)
|
|
128
121
|
case BetaTextDelta() as delta:
|
|
129
|
-
|
|
130
|
-
first_token_time = time.time()
|
|
131
|
-
last_token_time = time.time()
|
|
122
|
+
metadata_tracker.record_token()
|
|
132
123
|
accumulated_content.append(delta.text)
|
|
133
124
|
yield model.AssistantMessageDelta(
|
|
134
125
|
content=delta.text,
|
|
135
126
|
response_id=response_id,
|
|
136
127
|
)
|
|
137
128
|
case BetaInputJSONDelta() as delta:
|
|
138
|
-
|
|
139
|
-
first_token_time = time.time()
|
|
140
|
-
last_token_time = time.time()
|
|
129
|
+
metadata_tracker.record_token()
|
|
141
130
|
if current_tool_inputs is not None:
|
|
142
131
|
current_tool_inputs.append(delta.partial_json)
|
|
143
132
|
case _:
|
|
@@ -184,37 +173,18 @@ class AnthropicClient(LLMClientABC):
|
|
|
184
173
|
input_tokens += (event.usage.input_tokens or 0) + (event.usage.cache_creation_input_tokens or 0)
|
|
185
174
|
output_tokens += event.usage.output_tokens or 0
|
|
186
175
|
cached_tokens += event.usage.cache_read_input_tokens or 0
|
|
187
|
-
total_tokens = input_tokens + cached_tokens + output_tokens
|
|
188
|
-
context_usage_percent = (
|
|
189
|
-
(total_tokens / param.context_limit) * 100 if param.context_limit else None
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
throughput_tps: float | None = None
|
|
193
|
-
first_token_latency_ms: float | None = None
|
|
194
|
-
|
|
195
|
-
if first_token_time is not None:
|
|
196
|
-
first_token_latency_ms = (first_token_time - request_start_time) * 1000
|
|
197
176
|
|
|
198
|
-
|
|
199
|
-
time_duration = last_token_time - first_token_time
|
|
200
|
-
if time_duration >= 0.15:
|
|
201
|
-
throughput_tps = output_tokens / time_duration
|
|
202
|
-
|
|
203
|
-
usage = model.Usage(
|
|
177
|
+
usage = convert_anthropic_usage(
|
|
204
178
|
input_tokens=input_tokens,
|
|
205
179
|
output_tokens=output_tokens,
|
|
206
180
|
cached_tokens=cached_tokens,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
throughput_tps=throughput_tps,
|
|
210
|
-
first_token_latency_ms=first_token_latency_ms,
|
|
211
|
-
)
|
|
212
|
-
calculate_cost(usage, self._config.cost)
|
|
213
|
-
yield model.ResponseMetadataItem(
|
|
214
|
-
usage=usage,
|
|
215
|
-
response_id=response_id,
|
|
216
|
-
model_name=str(param.model),
|
|
181
|
+
context_limit=param.context_limit,
|
|
182
|
+
max_tokens=param.max_tokens,
|
|
217
183
|
)
|
|
184
|
+
metadata_tracker.set_usage(usage)
|
|
185
|
+
metadata_tracker.set_model_name(str(param.model))
|
|
186
|
+
metadata_tracker.set_response_id(response_id)
|
|
187
|
+
yield metadata_tracker.finalize()
|
|
218
188
|
case _:
|
|
219
189
|
pass
|
|
220
190
|
except (APIError, httpx.HTTPError) as e:
|
klaude_code/llm/client.py
CHANGED
|
@@ -19,7 +19,7 @@ class LLMClientABC(ABC):
|
|
|
19
19
|
@abstractmethod
|
|
20
20
|
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
21
21
|
raise NotImplementedError
|
|
22
|
-
yield cast(model.ConversationItem, None)
|
|
22
|
+
yield cast(model.ConversationItem, None)
|
|
23
23
|
|
|
24
24
|
def get_llm_config(self) -> llm_param.LLMConfigParameter:
|
|
25
25
|
return self._config
|
|
@@ -42,7 +42,7 @@ def call_with_logged_payload(func: Callable[P, R], *args: P.args, **kwargs: P.kw
|
|
|
42
42
|
|
|
43
43
|
payload = {k: v for k, v in kwargs.items() if v is not None}
|
|
44
44
|
log_debug(
|
|
45
|
-
json.dumps(payload, ensure_ascii=False, default=str),
|
|
45
|
+
json.dumps(payload, ensure_ascii=False, default=str, sort_keys=True),
|
|
46
46
|
style="yellow",
|
|
47
47
|
debug_type=DebugType.LLM_PAYLOAD,
|
|
48
48
|
)
|
klaude_code/llm/codex/client.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Codex LLM client using ChatGPT subscription via OAuth."""
|
|
2
2
|
|
|
3
|
-
import time
|
|
4
3
|
from collections.abc import AsyncGenerator
|
|
5
4
|
from typing import override
|
|
6
5
|
|
|
@@ -16,6 +15,7 @@ from klaude_code.llm.input_common import apply_config_defaults
|
|
|
16
15
|
from klaude_code.llm.registry import register
|
|
17
16
|
from klaude_code.llm.responses.client import parse_responses_stream
|
|
18
17
|
from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
|
|
18
|
+
from klaude_code.llm.usage import MetadataTracker
|
|
19
19
|
from klaude_code.protocol import llm_param, model
|
|
20
20
|
|
|
21
21
|
# Codex API configuration
|
|
@@ -24,6 +24,7 @@ CODEX_HEADERS = {
|
|
|
24
24
|
"originator": "codex_cli_rs",
|
|
25
25
|
# Mocked Codex-style user agent string
|
|
26
26
|
"User-Agent": "codex_cli_rs/0.0.0-klaude",
|
|
27
|
+
"OpenAI-Beta": "responses=experimental",
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
|
|
@@ -83,7 +84,7 @@ class CodexClient(LLMClientABC):
|
|
|
83
84
|
# Codex API requires store=False
|
|
84
85
|
param.store = False
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
87
88
|
|
|
88
89
|
inputs = convert_history_to_input(param.input, param.model)
|
|
89
90
|
tools = convert_tool_schema(param.tools)
|
|
@@ -125,5 +126,5 @@ class CodexClient(LLMClientABC):
|
|
|
125
126
|
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {str(e)}")
|
|
126
127
|
return
|
|
127
128
|
|
|
128
|
-
async for item in parse_responses_stream(stream, param,
|
|
129
|
+
async for item in parse_responses_stream(stream, param, metadata_tracker):
|
|
129
130
|
yield item
|