klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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/cli/main.py +9 -4
- klaude_code/cli/runtime.py +42 -43
- klaude_code/command/__init__.py +7 -5
- klaude_code/command/clear_cmd.py +6 -29
- klaude_code/command/command_abc.py +44 -8
- klaude_code/command/diff_cmd.py +33 -27
- klaude_code/command/export_cmd.py +18 -26
- klaude_code/command/help_cmd.py +10 -8
- klaude_code/command/model_cmd.py +11 -40
- klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
- klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
- klaude_code/command/prompt-init.md +2 -5
- klaude_code/command/prompt_command.py +6 -6
- klaude_code/command/refresh_cmd.py +4 -5
- klaude_code/command/registry.py +16 -19
- klaude_code/command/terminal_setup_cmd.py +12 -11
- klaude_code/config/__init__.py +4 -0
- klaude_code/config/config.py +25 -26
- klaude_code/config/list_model.py +8 -3
- klaude_code/config/select_model.py +1 -1
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/__init__.py +0 -3
- klaude_code/core/agent.py +25 -50
- klaude_code/core/executor.py +268 -101
- klaude_code/core/prompt.py +12 -12
- klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
- klaude_code/core/reminders.py +76 -95
- klaude_code/core/task.py +21 -14
- klaude_code/core/tool/__init__.py +45 -11
- klaude_code/core/tool/file/apply_patch.py +5 -1
- klaude_code/core/tool/file/apply_patch_tool.py +11 -13
- klaude_code/core/tool/file/edit_tool.py +27 -23
- klaude_code/core/tool/file/multi_edit_tool.py +15 -17
- klaude_code/core/tool/file/read_tool.py +41 -36
- klaude_code/core/tool/file/write_tool.py +13 -15
- klaude_code/core/tool/memory/memory_tool.py +85 -68
- klaude_code/core/tool/memory/skill_tool.py +10 -12
- klaude_code/core/tool/shell/bash_tool.py +24 -22
- klaude_code/core/tool/shell/command_safety.py +12 -1
- klaude_code/core/tool/sub_agent_tool.py +11 -12
- klaude_code/core/tool/todo/todo_write_tool.py +21 -28
- klaude_code/core/tool/todo/update_plan_tool.py +14 -24
- klaude_code/core/tool/tool_abc.py +3 -4
- klaude_code/core/tool/tool_context.py +7 -7
- klaude_code/core/tool/tool_registry.py +30 -47
- klaude_code/core/tool/tool_runner.py +35 -43
- klaude_code/core/tool/truncation.py +14 -20
- klaude_code/core/tool/web/mermaid_tool.py +12 -14
- klaude_code/core/tool/web/web_fetch_tool.py +15 -17
- klaude_code/core/turn.py +19 -7
- klaude_code/llm/__init__.py +3 -4
- klaude_code/llm/anthropic/client.py +30 -46
- klaude_code/llm/anthropic/input.py +4 -11
- klaude_code/llm/client.py +29 -8
- klaude_code/llm/input_common.py +66 -36
- klaude_code/llm/openai_compatible/client.py +42 -84
- klaude_code/llm/openai_compatible/input.py +11 -16
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
- klaude_code/llm/openrouter/client.py +40 -289
- klaude_code/llm/openrouter/input.py +13 -35
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +5 -75
- klaude_code/llm/responses/client.py +34 -55
- klaude_code/llm/responses/input.py +24 -26
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/events.py +3 -2
- klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
- klaude_code/protocol/model.py +49 -4
- klaude_code/protocol/op.py +18 -16
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/{core → protocol}/sub_agent.py +7 -0
- klaude_code/session/export.py +150 -70
- klaude_code/session/session.py +28 -14
- klaude_code/session/templates/export_session.html +180 -42
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +11 -5
- klaude_code/ui/__init__.py +91 -8
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
- klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +0 -16
- klaude_code/ui/renderers/developer.py +18 -18
- klaude_code/ui/renderers/diffs.py +36 -14
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/metadata.py +50 -27
- klaude_code/ui/renderers/sub_agent.py +43 -9
- klaude_code/ui/renderers/thinking.py +33 -1
- klaude_code/ui/renderers/tools.py +212 -20
- klaude_code/ui/renderers/user_input.py +19 -23
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
- klaude_code/ui/{renderers → rich}/status.py +29 -18
- klaude_code/ui/{base → rich}/theme.py +8 -2
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
- klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
- klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
- klaude_code-1.2.3.dist-info/RECORD +161 -0
- klaude_code/core/clipboard_manifest.py +0 -124
- klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
- klaude_code/ui/base/__init__.py +0 -1
- klaude_code/ui/base/display_abc.py +0 -36
- klaude_code/ui/base/input_abc.py +0 -20
- klaude_code/ui/repl/display.py +0 -36
- klaude_code/ui/repl/event_handler.py +0 -247
- klaude_code/ui/repl/input.py +0 -773
- klaude_code/ui/rich_ext/__init__.py +0 -1
- klaude_code-1.2.1.dist-info/RECORD +0 -151
- /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
- /klaude_code/ui/{base → core}/stage_manager.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
- /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
- /klaude_code/ui/{base → utils}/debouncer.py +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
|
@@ -9,14 +9,12 @@ from pydantic import BaseModel, Field
|
|
|
9
9
|
|
|
10
10
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
11
11
|
from klaude_code.core.tool.tool_registry import register
|
|
12
|
-
from klaude_code.protocol
|
|
13
|
-
from klaude_code.protocol.model import MermaidLinkUIExtra, ToolResultItem, ToolResultUIExtra, ToolResultUIExtraType
|
|
14
|
-
from klaude_code.protocol.tools import MERMAID
|
|
12
|
+
from klaude_code.protocol import llm_param, model, tools
|
|
15
13
|
|
|
16
14
|
_MERMAID_LIVE_PREFIX = "https://mermaid.live/view#pako:"
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
@register(MERMAID)
|
|
17
|
+
@register(tools.MERMAID)
|
|
20
18
|
class MermaidTool(ToolABC):
|
|
21
19
|
"""Create shareable Mermaid.live links for diagram rendering."""
|
|
22
20
|
|
|
@@ -24,9 +22,9 @@ class MermaidTool(ToolABC):
|
|
|
24
22
|
code: str = Field(description="The Mermaid diagram code to render")
|
|
25
23
|
|
|
26
24
|
@classmethod
|
|
27
|
-
def schema(cls) -> ToolSchema:
|
|
28
|
-
return ToolSchema(
|
|
29
|
-
name=MERMAID,
|
|
25
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
26
|
+
return llm_param.ToolSchema(
|
|
27
|
+
name=tools.MERMAID,
|
|
30
28
|
type="function",
|
|
31
29
|
description=load_desc(Path(__file__).parent / "mermaid_tool.md"),
|
|
32
30
|
parameters={
|
|
@@ -43,26 +41,26 @@ class MermaidTool(ToolABC):
|
|
|
43
41
|
)
|
|
44
42
|
|
|
45
43
|
@classmethod
|
|
46
|
-
async def call(cls, arguments: str) -> ToolResultItem:
|
|
44
|
+
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
47
45
|
try:
|
|
48
46
|
args = cls.MermaidArguments.model_validate_json(arguments)
|
|
49
47
|
except Exception as exc: # pragma: no cover - defensive
|
|
50
|
-
return ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
|
|
48
|
+
return model.ToolResultItem(status="error", output=f"Invalid arguments: {exc}")
|
|
51
49
|
|
|
52
50
|
link = cls._build_link(args.code)
|
|
53
51
|
line_count = cls._count_lines(args.code)
|
|
54
|
-
ui_extra = ToolResultUIExtra(
|
|
55
|
-
type=ToolResultUIExtraType.MERMAID_LINK,
|
|
56
|
-
mermaid_link=MermaidLinkUIExtra(link=link, line_count=line_count),
|
|
52
|
+
ui_extra = model.ToolResultUIExtra(
|
|
53
|
+
type=model.ToolResultUIExtraType.MERMAID_LINK,
|
|
54
|
+
mermaid_link=model.MermaidLinkUIExtra(link=link, line_count=line_count),
|
|
57
55
|
)
|
|
58
56
|
output = f"Mermaid diagram rendered successfully ({line_count} lines)."
|
|
59
|
-
return ToolResultItem(status="success", output=output, ui_extra=ui_extra)
|
|
57
|
+
return model.ToolResultItem(status="success", output=output, ui_extra=ui_extra)
|
|
60
58
|
|
|
61
59
|
@staticmethod
|
|
62
60
|
def _build_link(code: str) -> str:
|
|
63
61
|
state = {
|
|
64
62
|
"code": code,
|
|
65
|
-
"mermaid": {"theme": "
|
|
63
|
+
"mermaid": {"theme": "neutral"},
|
|
66
64
|
"autoSync": True,
|
|
67
65
|
"updateDiagram": True,
|
|
68
66
|
}
|
|
@@ -9,9 +9,7 @@ from pydantic import BaseModel
|
|
|
9
9
|
|
|
10
10
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
11
11
|
from klaude_code.core.tool.tool_registry import register
|
|
12
|
-
from klaude_code.protocol
|
|
13
|
-
from klaude_code.protocol.model import ToolResultItem
|
|
14
|
-
from klaude_code.protocol.tools import WEB_FETCH
|
|
12
|
+
from klaude_code.protocol import llm_param, model, tools
|
|
15
13
|
|
|
16
14
|
DEFAULT_TIMEOUT_SEC = 30
|
|
17
15
|
DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; KlaudeCode/1.0)"
|
|
@@ -80,12 +78,12 @@ def _fetch_url(url: str, timeout: int = DEFAULT_TIMEOUT_SEC) -> tuple[str, str]:
|
|
|
80
78
|
return content_type, text
|
|
81
79
|
|
|
82
80
|
|
|
83
|
-
@register(WEB_FETCH)
|
|
81
|
+
@register(tools.WEB_FETCH)
|
|
84
82
|
class WebFetchTool(ToolABC):
|
|
85
83
|
@classmethod
|
|
86
|
-
def schema(cls) -> ToolSchema:
|
|
87
|
-
return ToolSchema(
|
|
88
|
-
name=WEB_FETCH,
|
|
84
|
+
def schema(cls) -> llm_param.ToolSchema:
|
|
85
|
+
return llm_param.ToolSchema(
|
|
86
|
+
name=tools.WEB_FETCH,
|
|
89
87
|
type="function",
|
|
90
88
|
description=load_desc(Path(__file__).parent / "web_fetch_tool.md"),
|
|
91
89
|
parameters={
|
|
@@ -104,23 +102,23 @@ class WebFetchTool(ToolABC):
|
|
|
104
102
|
url: str
|
|
105
103
|
|
|
106
104
|
@classmethod
|
|
107
|
-
async def call(cls, arguments: str) -> ToolResultItem:
|
|
105
|
+
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
108
106
|
try:
|
|
109
107
|
args = WebFetchTool.WebFetchArguments.model_validate_json(arguments)
|
|
110
108
|
except ValueError as e:
|
|
111
|
-
return ToolResultItem(
|
|
109
|
+
return model.ToolResultItem(
|
|
112
110
|
status="error",
|
|
113
111
|
output=f"Invalid arguments: {e}",
|
|
114
112
|
)
|
|
115
113
|
return await cls.call_with_args(args)
|
|
116
114
|
|
|
117
115
|
@classmethod
|
|
118
|
-
async def call_with_args(cls, args: WebFetchArguments) -> ToolResultItem:
|
|
116
|
+
async def call_with_args(cls, args: WebFetchArguments) -> model.ToolResultItem:
|
|
119
117
|
url = args.url
|
|
120
118
|
|
|
121
119
|
# Basic URL validation
|
|
122
120
|
if not url.startswith(("http://", "https://")):
|
|
123
|
-
return ToolResultItem(
|
|
121
|
+
return model.ToolResultItem(
|
|
124
122
|
status="error",
|
|
125
123
|
output="Invalid URL: must start with http:// or https://",
|
|
126
124
|
)
|
|
@@ -129,33 +127,33 @@ class WebFetchTool(ToolABC):
|
|
|
129
127
|
content_type, text = await asyncio.to_thread(_fetch_url, url)
|
|
130
128
|
processed = _process_content(content_type, text)
|
|
131
129
|
|
|
132
|
-
return ToolResultItem(
|
|
130
|
+
return model.ToolResultItem(
|
|
133
131
|
status="success",
|
|
134
132
|
output=processed,
|
|
135
133
|
)
|
|
136
134
|
|
|
137
135
|
except urllib.error.HTTPError as e:
|
|
138
|
-
return ToolResultItem(
|
|
136
|
+
return model.ToolResultItem(
|
|
139
137
|
status="error",
|
|
140
138
|
output=f"HTTP error {e.code}: {e.reason}",
|
|
141
139
|
)
|
|
142
140
|
except urllib.error.URLError as e:
|
|
143
|
-
return ToolResultItem(
|
|
141
|
+
return model.ToolResultItem(
|
|
144
142
|
status="error",
|
|
145
143
|
output=f"URL error: {e.reason}",
|
|
146
144
|
)
|
|
147
145
|
except UnicodeDecodeError as e:
|
|
148
|
-
return ToolResultItem(
|
|
146
|
+
return model.ToolResultItem(
|
|
149
147
|
status="error",
|
|
150
148
|
output=f"Content is not valid UTF-8: {e}",
|
|
151
149
|
)
|
|
152
150
|
except TimeoutError:
|
|
153
|
-
return ToolResultItem(
|
|
151
|
+
return model.ToolResultItem(
|
|
154
152
|
status="error",
|
|
155
153
|
output=f"Request timed out after {DEFAULT_TIMEOUT_SEC} seconds",
|
|
156
154
|
)
|
|
157
155
|
except Exception as e:
|
|
158
|
-
return ToolResultItem(
|
|
156
|
+
return model.ToolResultItem(
|
|
159
157
|
status="error",
|
|
160
158
|
output=f"Failed to fetch URL: {e}",
|
|
161
159
|
)
|
klaude_code/core/turn.py
CHANGED
|
@@ -3,8 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import AsyncGenerator, Callable, MutableMapping, Sequence
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
|
|
6
|
-
from klaude_code.core.tool
|
|
7
|
-
from klaude_code.core.tool.tool_context import TodoContext, tool_context
|
|
6
|
+
from klaude_code.core.tool import TodoContext, ToolABC, tool_context
|
|
8
7
|
from klaude_code.core.tool.tool_runner import (
|
|
9
8
|
ToolExecutionCallStarted,
|
|
10
9
|
ToolExecutionResult,
|
|
@@ -12,8 +11,8 @@ from klaude_code.core.tool.tool_runner import (
|
|
|
12
11
|
ToolExecutor,
|
|
13
12
|
ToolExecutorEvent,
|
|
14
13
|
)
|
|
15
|
-
from klaude_code.llm
|
|
16
|
-
from klaude_code.protocol import events,
|
|
14
|
+
from klaude_code.llm import LLMClientABC
|
|
15
|
+
from klaude_code.protocol import events, llm_param, model
|
|
17
16
|
from klaude_code.trace import DebugType, log_debug
|
|
18
17
|
|
|
19
18
|
|
|
@@ -32,7 +31,7 @@ class TurnExecutionContext:
|
|
|
32
31
|
append_history: Callable[[Sequence[model.ConversationItem]], None]
|
|
33
32
|
llm_client: LLMClientABC
|
|
34
33
|
system_prompt: str | None
|
|
35
|
-
tools: list[
|
|
34
|
+
tools: list[llm_param.ToolSchema]
|
|
36
35
|
tool_registry: dict[str, type[ToolABC]]
|
|
37
36
|
# For tool context
|
|
38
37
|
file_tracker: MutableMapping[str, float]
|
|
@@ -121,7 +120,7 @@ class TurnExecutor:
|
|
|
121
120
|
error_message: str | None = None
|
|
122
121
|
|
|
123
122
|
async for response_item in ctx.llm_client.call(
|
|
124
|
-
|
|
123
|
+
llm_param.LLMCallParameter(
|
|
125
124
|
input=ctx.get_conversation_history(),
|
|
126
125
|
system=ctx.system_prompt,
|
|
127
126
|
tools=ctx.tools,
|
|
@@ -172,7 +171,20 @@ class TurnExecutor:
|
|
|
172
171
|
case model.StreamErrorItem() as item:
|
|
173
172
|
response_failed = True
|
|
174
173
|
error_message = item.error
|
|
175
|
-
log_debug(
|
|
174
|
+
log_debug(
|
|
175
|
+
"[StreamError]",
|
|
176
|
+
item.error,
|
|
177
|
+
style="red",
|
|
178
|
+
debug_type=DebugType.RESPONSE,
|
|
179
|
+
)
|
|
180
|
+
case model.ToolCallStartItem() as item:
|
|
181
|
+
yield events.TurnToolCallStartEvent(
|
|
182
|
+
session_id=ctx.session_id,
|
|
183
|
+
response_id=item.response_id,
|
|
184
|
+
tool_call_id=item.call_id,
|
|
185
|
+
tool_name=item.name,
|
|
186
|
+
arguments="",
|
|
187
|
+
)
|
|
176
188
|
case model.ToolCallItem() as item:
|
|
177
189
|
turn_tool_calls.append(item)
|
|
178
190
|
case _:
|
klaude_code/llm/__init__.py
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
"""LLM package init.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Imports built-in LLM clients so their ``@register`` decorators run and they
|
|
4
|
+
become available via the registry.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .anthropic import AnthropicClient
|
|
8
8
|
from .client import LLMClientABC
|
|
9
9
|
from .openai_compatible import OpenAICompatibleClient
|
|
10
10
|
from .openrouter import OpenRouterClient
|
|
11
|
-
from .registry import
|
|
11
|
+
from .registry import create_llm_client
|
|
12
12
|
from .responses import ResponsesClient
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
15
|
"LLMClientABC",
|
|
16
|
-
"LLMClients",
|
|
17
16
|
"ResponsesClient",
|
|
18
17
|
"OpenAICompatibleClient",
|
|
19
18
|
"OpenRouterClient",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
3
|
from collections.abc import AsyncGenerator
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import override
|
|
5
5
|
|
|
6
6
|
import anthropic
|
|
7
7
|
import httpx
|
|
@@ -17,42 +17,19 @@ from anthropic.types.beta.beta_text_delta import BetaTextDelta
|
|
|
17
17
|
from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
|
|
18
18
|
from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
|
|
19
19
|
|
|
20
|
+
from klaude_code import const
|
|
20
21
|
from klaude_code.llm.anthropic.input import convert_history_to_input, convert_system_to_input, convert_tool_schema
|
|
21
|
-
from klaude_code.llm.client import LLMClientABC
|
|
22
|
+
from klaude_code.llm.client import LLMClientABC, call_with_logged_payload
|
|
23
|
+
from klaude_code.llm.input_common import apply_config_defaults
|
|
22
24
|
from klaude_code.llm.registry import register
|
|
23
|
-
from klaude_code.
|
|
24
|
-
from klaude_code.protocol
|
|
25
|
-
LLMCallParameter,
|
|
26
|
-
LLMClientProtocol,
|
|
27
|
-
LLMConfigParameter,
|
|
28
|
-
apply_config_defaults,
|
|
29
|
-
)
|
|
30
|
-
from klaude_code.protocol.model import StreamErrorItem
|
|
25
|
+
from klaude_code.llm.usage import calculate_cost
|
|
26
|
+
from klaude_code.protocol import llm_param, model
|
|
31
27
|
from klaude_code.trace import DebugType, log_debug
|
|
32
28
|
|
|
33
|
-
P = ParamSpec("P")
|
|
34
|
-
R = TypeVar("R")
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
def call_with_logged_payload(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
|
|
38
|
-
"""Call an SDK function while logging the JSON payload.
|
|
39
|
-
|
|
40
|
-
The function reuses the original callable's type signature via ParamSpec
|
|
41
|
-
so static type checkers can validate arguments at the call site.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
payload = {k: v for k, v in kwargs.items() if v is not None}
|
|
45
|
-
log_debug(
|
|
46
|
-
json.dumps(payload, ensure_ascii=False, default=str),
|
|
47
|
-
style="yellow",
|
|
48
|
-
debug_type=DebugType.LLM_PAYLOAD,
|
|
49
|
-
)
|
|
50
|
-
return func(*args, **kwargs)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@register(LLMClientProtocol.ANTHROPIC)
|
|
30
|
+
@register(llm_param.LLMClientProtocol.ANTHROPIC)
|
|
54
31
|
class AnthropicClient(LLMClientABC):
|
|
55
|
-
def __init__(self, config: LLMConfigParameter):
|
|
32
|
+
def __init__(self, config: llm_param.LLMConfigParameter):
|
|
56
33
|
super().__init__(config)
|
|
57
34
|
client = anthropic.AsyncAnthropic(
|
|
58
35
|
api_key=config.api_key,
|
|
@@ -63,11 +40,11 @@ class AnthropicClient(LLMClientABC):
|
|
|
63
40
|
|
|
64
41
|
@classmethod
|
|
65
42
|
@override
|
|
66
|
-
def create(cls, config: LLMConfigParameter) -> "LLMClientABC":
|
|
43
|
+
def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
|
|
67
44
|
return cls(config)
|
|
68
45
|
|
|
69
46
|
@override
|
|
70
|
-
async def call(self, param: LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
47
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
71
48
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
72
49
|
|
|
73
50
|
request_start_time = time.time()
|
|
@@ -86,15 +63,15 @@ class AnthropicClient(LLMClientABC):
|
|
|
86
63
|
"disable_parallel_tool_use": False,
|
|
87
64
|
},
|
|
88
65
|
stream=True,
|
|
89
|
-
max_tokens=param.max_tokens or
|
|
90
|
-
temperature=param.temperature or
|
|
66
|
+
max_tokens=param.max_tokens or const.DEFAULT_MAX_TOKENS,
|
|
67
|
+
temperature=param.temperature or const.DEFAULT_TEMPERATURE,
|
|
91
68
|
messages=messages,
|
|
92
69
|
system=system,
|
|
93
70
|
tools=tools,
|
|
94
71
|
betas=["interleaved-thinking-2025-05-14", "context-1m-2025-08-07"],
|
|
95
72
|
thinking=anthropic.types.ThinkingConfigEnabledParam(
|
|
96
73
|
type=param.thinking.type,
|
|
97
|
-
budget_tokens=param.thinking.budget_tokens or
|
|
74
|
+
budget_tokens=param.thinking.budget_tokens or const.DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
|
|
98
75
|
)
|
|
99
76
|
if param.thinking and param.thinking.type == "enabled"
|
|
100
77
|
else anthropic.types.ThinkingConfigDisabledParam(
|
|
@@ -168,6 +145,11 @@ class AnthropicClient(LLMClientABC):
|
|
|
168
145
|
case BetaRawContentBlockStartEvent() as event:
|
|
169
146
|
match event.content_block:
|
|
170
147
|
case BetaToolUseBlock() as block:
|
|
148
|
+
yield model.ToolCallStartItem(
|
|
149
|
+
response_id=response_id,
|
|
150
|
+
call_id=block.id,
|
|
151
|
+
name=block.name,
|
|
152
|
+
)
|
|
171
153
|
current_tool_name = block.name
|
|
172
154
|
current_tool_call_id = block.id
|
|
173
155
|
current_tool_inputs = []
|
|
@@ -218,20 +200,22 @@ class AnthropicClient(LLMClientABC):
|
|
|
218
200
|
if time_duration >= 0.15:
|
|
219
201
|
throughput_tps = output_tokens / time_duration
|
|
220
202
|
|
|
203
|
+
usage = model.Usage(
|
|
204
|
+
input_tokens=input_tokens,
|
|
205
|
+
output_tokens=output_tokens,
|
|
206
|
+
cached_tokens=cached_tokens,
|
|
207
|
+
total_tokens=total_tokens,
|
|
208
|
+
context_usage_percent=context_usage_percent,
|
|
209
|
+
throughput_tps=throughput_tps,
|
|
210
|
+
first_token_latency_ms=first_token_latency_ms,
|
|
211
|
+
)
|
|
212
|
+
calculate_cost(usage, self._config.cost)
|
|
221
213
|
yield model.ResponseMetadataItem(
|
|
222
|
-
usage=
|
|
223
|
-
input_tokens=input_tokens,
|
|
224
|
-
output_tokens=output_tokens,
|
|
225
|
-
cached_tokens=cached_tokens,
|
|
226
|
-
total_tokens=total_tokens,
|
|
227
|
-
context_usage_percent=context_usage_percent,
|
|
228
|
-
throughput_tps=throughput_tps,
|
|
229
|
-
first_token_latency_ms=first_token_latency_ms,
|
|
230
|
-
),
|
|
214
|
+
usage=usage,
|
|
231
215
|
response_id=response_id,
|
|
232
216
|
model_name=str(param.model),
|
|
233
217
|
)
|
|
234
218
|
case _:
|
|
235
219
|
pass
|
|
236
220
|
except RateLimitError as e:
|
|
237
|
-
yield StreamErrorItem(error=f"{e.__class__.__name__} {str(e)}")
|
|
221
|
+
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {str(e)}")
|
|
@@ -17,15 +17,8 @@ from anthropic.types.beta.beta_text_block_param import BetaTextBlockParam
|
|
|
17
17
|
from anthropic.types.beta.beta_tool_param import BetaToolParam
|
|
18
18
|
from anthropic.types.beta.beta_url_image_source_param import BetaURLImageSourceParam
|
|
19
19
|
|
|
20
|
-
from klaude_code.llm.input_common import
|
|
21
|
-
|
|
22
|
-
ToolGroup,
|
|
23
|
-
UserGroup,
|
|
24
|
-
merge_reminder_text,
|
|
25
|
-
parse_message_groups,
|
|
26
|
-
)
|
|
27
|
-
from klaude_code.protocol import model as protocol_model
|
|
28
|
-
from klaude_code.protocol import llm_parameter, model
|
|
20
|
+
from klaude_code.llm.input_common import AssistantGroup, ToolGroup, UserGroup, merge_reminder_text, parse_message_groups
|
|
21
|
+
from klaude_code.protocol import llm_param, model
|
|
29
22
|
|
|
30
23
|
AllowedMediaType = Literal["image/png", "image/jpeg", "image/gif", "image/webp"]
|
|
31
24
|
_INLINE_IMAGE_MEDIA_TYPES: tuple[AllowedMediaType, ...] = (
|
|
@@ -108,7 +101,7 @@ def _assistant_group_to_message(group: AssistantGroup, model_name: str | None) -
|
|
|
108
101
|
# Process reasoning items in original order so that text and
|
|
109
102
|
# encrypted parts are paired correctly for the given model.
|
|
110
103
|
for item in group.reasoning_items:
|
|
111
|
-
if isinstance(item,
|
|
104
|
+
if isinstance(item, model.ReasoningTextItem):
|
|
112
105
|
if model_name != item.model:
|
|
113
106
|
continue
|
|
114
107
|
current_reasoning_content = item.content
|
|
@@ -189,7 +182,7 @@ def convert_system_to_input(system: str | None) -> list[BetaTextBlockParam]:
|
|
|
189
182
|
|
|
190
183
|
|
|
191
184
|
def convert_tool_schema(
|
|
192
|
-
tools: list[
|
|
185
|
+
tools: list[llm_param.ToolSchema] | None,
|
|
193
186
|
) -> list[BetaToolParam]:
|
|
194
187
|
if tools is None:
|
|
195
188
|
return []
|
klaude_code/llm/client.py
CHANGED
|
@@ -1,28 +1,49 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
3
|
from collections.abc import AsyncGenerator
|
|
3
|
-
from typing import cast
|
|
4
|
+
from typing import Callable, ParamSpec, TypeVar, cast
|
|
4
5
|
|
|
5
|
-
from klaude_code.protocol
|
|
6
|
-
from klaude_code.
|
|
6
|
+
from klaude_code.protocol import llm_param, model
|
|
7
|
+
from klaude_code.trace import DebugType, log_debug
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class LLMClientABC(ABC):
|
|
10
|
-
def __init__(self, config: LLMConfigParameter) -> None:
|
|
11
|
+
def __init__(self, config: llm_param.LLMConfigParameter) -> None:
|
|
11
12
|
self._config = config
|
|
12
13
|
|
|
13
14
|
@classmethod
|
|
14
15
|
@abstractmethod
|
|
15
|
-
def create(cls, config: LLMConfigParameter) -> "LLMClientABC":
|
|
16
|
+
def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
|
|
16
17
|
pass
|
|
17
18
|
|
|
18
19
|
@abstractmethod
|
|
19
|
-
async def call(self, param: LLMCallParameter) -> AsyncGenerator[ConversationItem, None]:
|
|
20
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
20
21
|
raise NotImplementedError
|
|
21
|
-
yield cast(ConversationItem, None) # pyright: ignore[reportUnreachable]
|
|
22
|
+
yield cast(model.ConversationItem, None) # pyright: ignore[reportUnreachable]
|
|
22
23
|
|
|
23
|
-
def get_llm_config(self) -> LLMConfigParameter:
|
|
24
|
+
def get_llm_config(self) -> llm_param.LLMConfigParameter:
|
|
24
25
|
return self._config
|
|
25
26
|
|
|
26
27
|
@property
|
|
27
28
|
def model_name(self) -> str:
|
|
28
29
|
return self._config.model or ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
P = ParamSpec("P")
|
|
33
|
+
R = TypeVar("R")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def call_with_logged_payload(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
|
|
37
|
+
"""Call an SDK function while logging the JSON payload.
|
|
38
|
+
|
|
39
|
+
The function reuses the original callable's type signature via ParamSpec
|
|
40
|
+
so static type checkers can validate arguments at the call site.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
payload = {k: v for k, v in kwargs.items() if v is not None}
|
|
44
|
+
log_debug(
|
|
45
|
+
json.dumps(payload, ensure_ascii=False, default=str),
|
|
46
|
+
style="yellow",
|
|
47
|
+
debug_type=DebugType.LLM_PAYLOAD,
|
|
48
|
+
)
|
|
49
|
+
return func(*args, **kwargs)
|
klaude_code/llm/input_common.py
CHANGED
|
@@ -8,19 +8,14 @@ since it uses a flat item list matching our internal protocol.
|
|
|
8
8
|
from collections.abc import Iterator
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from enum import Enum
|
|
11
|
-
from typing import Iterable
|
|
11
|
+
from typing import TYPE_CHECKING, Iterable
|
|
12
12
|
|
|
13
|
-
from klaude_code
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ReasoningTextItem,
|
|
20
|
-
ToolCallItem,
|
|
21
|
-
ToolResultItem,
|
|
22
|
-
UserMessageItem,
|
|
23
|
-
)
|
|
13
|
+
from klaude_code import const
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from klaude_code.protocol.llm_param import LLMCallParameter, LLMConfigParameter
|
|
17
|
+
|
|
18
|
+
from klaude_code.protocol import model
|
|
24
19
|
|
|
25
20
|
|
|
26
21
|
class GroupKind(Enum):
|
|
@@ -36,16 +31,16 @@ class UserGroup:
|
|
|
36
31
|
"""Aggregated user message group (UserMessageItem + DeveloperMessageItem)."""
|
|
37
32
|
|
|
38
33
|
text_parts: list[str] = field(default_factory=lambda: [])
|
|
39
|
-
images: list[ImageURLPart] = field(default_factory=lambda: [])
|
|
34
|
+
images: list[model.ImageURLPart] = field(default_factory=lambda: [])
|
|
40
35
|
|
|
41
36
|
|
|
42
37
|
@dataclass
|
|
43
38
|
class ToolGroup:
|
|
44
39
|
"""Aggregated tool result group (ToolResultItem + trailing DeveloperMessageItems)."""
|
|
45
40
|
|
|
46
|
-
tool_result: ToolResultItem
|
|
41
|
+
tool_result: model.ToolResultItem
|
|
47
42
|
reminder_texts: list[str] = field(default_factory=lambda: [])
|
|
48
|
-
reminder_images: list[ImageURLPart] = field(default_factory=lambda: [])
|
|
43
|
+
reminder_images: list[model.ImageURLPart] = field(default_factory=lambda: [])
|
|
49
44
|
|
|
50
45
|
|
|
51
46
|
@dataclass
|
|
@@ -53,32 +48,35 @@ class AssistantGroup:
|
|
|
53
48
|
"""Aggregated assistant message group."""
|
|
54
49
|
|
|
55
50
|
text_content: str | None = None
|
|
56
|
-
tool_calls: list[ToolCallItem] = field(default_factory=lambda: [])
|
|
57
|
-
reasoning_text: list[ReasoningTextItem] = field(default_factory=lambda: [])
|
|
58
|
-
reasoning_encrypted: list[ReasoningEncryptedItem] = field(default_factory=lambda: [])
|
|
51
|
+
tool_calls: list[model.ToolCallItem] = field(default_factory=lambda: [])
|
|
52
|
+
reasoning_text: list[model.ReasoningTextItem] = field(default_factory=lambda: [])
|
|
53
|
+
reasoning_encrypted: list[model.ReasoningEncryptedItem] = field(default_factory=lambda: [])
|
|
59
54
|
# Preserve original ordering of reasoning items for providers that
|
|
60
55
|
# need to emit them as an ordered stream (e.g. OpenRouter).
|
|
61
|
-
reasoning_items: list[ReasoningTextItem | ReasoningEncryptedItem] = field(default_factory=lambda: [])
|
|
56
|
+
reasoning_items: list[model.ReasoningTextItem | model.ReasoningEncryptedItem] = field(default_factory=lambda: [])
|
|
62
57
|
|
|
63
58
|
|
|
64
59
|
MessageGroup = UserGroup | ToolGroup | AssistantGroup
|
|
65
60
|
|
|
66
61
|
|
|
67
|
-
def _kind_of(item: ConversationItem) -> GroupKind:
|
|
68
|
-
if isinstance(
|
|
62
|
+
def _kind_of(item: model.ConversationItem) -> GroupKind:
|
|
63
|
+
if isinstance(
|
|
64
|
+
item,
|
|
65
|
+
(model.ReasoningTextItem, model.ReasoningEncryptedItem, model.AssistantMessageItem, model.ToolCallItem),
|
|
66
|
+
):
|
|
69
67
|
return GroupKind.ASSISTANT
|
|
70
|
-
if isinstance(item, UserMessageItem):
|
|
68
|
+
if isinstance(item, model.UserMessageItem):
|
|
71
69
|
return GroupKind.USER
|
|
72
|
-
if isinstance(item, ToolResultItem):
|
|
70
|
+
if isinstance(item, model.ToolResultItem):
|
|
73
71
|
return GroupKind.TOOL
|
|
74
|
-
if isinstance(item, DeveloperMessageItem):
|
|
72
|
+
if isinstance(item, model.DeveloperMessageItem):
|
|
75
73
|
return GroupKind.DEVELOPER
|
|
76
74
|
return GroupKind.OTHER
|
|
77
75
|
|
|
78
76
|
|
|
79
77
|
def group_response_items_gen(
|
|
80
|
-
items: Iterable[ConversationItem],
|
|
81
|
-
) -> Iterator[tuple[GroupKind, list[ConversationItem]]]:
|
|
78
|
+
items: Iterable[model.ConversationItem],
|
|
79
|
+
) -> Iterator[tuple[GroupKind, list[model.ConversationItem]]]:
|
|
82
80
|
"""Group response items into sublists with predictable attachment rules.
|
|
83
81
|
|
|
84
82
|
- Consecutive assistant-side items (ReasoningTextItem | ReasoningEncryptedItem |
|
|
@@ -88,10 +86,10 @@ def group_response_items_gen(
|
|
|
88
86
|
DeveloperMessage to attach to it.
|
|
89
87
|
- DeveloperMessage only attaches to the previous UserMessage/ToolMessage group.
|
|
90
88
|
"""
|
|
91
|
-
buffer: list[ConversationItem] = []
|
|
89
|
+
buffer: list[model.ConversationItem] = []
|
|
92
90
|
buffer_kind: GroupKind | None = None
|
|
93
91
|
|
|
94
|
-
def flush() -> Iterator[tuple[GroupKind, list[ConversationItem]]]:
|
|
92
|
+
def flush() -> Iterator[tuple[GroupKind, list[model.ConversationItem]]]:
|
|
95
93
|
"""Yield current group and reset buffer state."""
|
|
96
94
|
|
|
97
95
|
nonlocal buffer, buffer_kind
|
|
@@ -138,7 +136,7 @@ def group_response_items_gen(
|
|
|
138
136
|
yield (buffer_kind, buffer)
|
|
139
137
|
|
|
140
138
|
|
|
141
|
-
def parse_message_groups(history: list[ConversationItem]) -> list[MessageGroup]:
|
|
139
|
+
def parse_message_groups(history: list[model.ConversationItem]) -> list[MessageGroup]:
|
|
142
140
|
"""Parse conversation history into aggregated message groups.
|
|
143
141
|
|
|
144
142
|
This is the shared grouping logic for Anthropic, OpenAI-compatible, and OpenRouter.
|
|
@@ -153,7 +151,7 @@ def parse_message_groups(history: list[ConversationItem]) -> list[MessageGroup]:
|
|
|
153
151
|
case GroupKind.USER:
|
|
154
152
|
group = UserGroup()
|
|
155
153
|
for item in items:
|
|
156
|
-
if isinstance(item, (UserMessageItem, DeveloperMessageItem)):
|
|
154
|
+
if isinstance(item, (model.UserMessageItem, model.DeveloperMessageItem)):
|
|
157
155
|
if item.content:
|
|
158
156
|
group.text_parts.append(item.content)
|
|
159
157
|
if item.images:
|
|
@@ -161,12 +159,12 @@ def parse_message_groups(history: list[ConversationItem]) -> list[MessageGroup]:
|
|
|
161
159
|
groups.append(group)
|
|
162
160
|
|
|
163
161
|
case GroupKind.TOOL:
|
|
164
|
-
if not items or not isinstance(items[0], ToolResultItem):
|
|
162
|
+
if not items or not isinstance(items[0], model.ToolResultItem):
|
|
165
163
|
continue
|
|
166
164
|
tool_result = items[0]
|
|
167
165
|
group = ToolGroup(tool_result=tool_result)
|
|
168
166
|
for item in items[1:]:
|
|
169
|
-
if isinstance(item, DeveloperMessageItem):
|
|
167
|
+
if isinstance(item, model.DeveloperMessageItem):
|
|
170
168
|
if item.content:
|
|
171
169
|
group.reminder_texts.append(item.content)
|
|
172
170
|
if item.images:
|
|
@@ -177,18 +175,18 @@ def parse_message_groups(history: list[ConversationItem]) -> list[MessageGroup]:
|
|
|
177
175
|
group = AssistantGroup()
|
|
178
176
|
for item in items:
|
|
179
177
|
match item:
|
|
180
|
-
case AssistantMessageItem():
|
|
178
|
+
case model.AssistantMessageItem():
|
|
181
179
|
if item.content:
|
|
182
180
|
if group.text_content is None:
|
|
183
181
|
group.text_content = item.content
|
|
184
182
|
else:
|
|
185
183
|
group.text_content += item.content
|
|
186
|
-
case ToolCallItem():
|
|
184
|
+
case model.ToolCallItem():
|
|
187
185
|
group.tool_calls.append(item)
|
|
188
|
-
case ReasoningTextItem():
|
|
186
|
+
case model.ReasoningTextItem():
|
|
189
187
|
group.reasoning_text.append(item)
|
|
190
188
|
group.reasoning_items.append(item)
|
|
191
|
-
case ReasoningEncryptedItem():
|
|
189
|
+
case model.ReasoningEncryptedItem():
|
|
192
190
|
group.reasoning_encrypted.append(item)
|
|
193
191
|
group.reasoning_items.append(item)
|
|
194
192
|
case _:
|
|
@@ -207,3 +205,35 @@ def merge_reminder_text(tool_output: str | None, reminder_texts: list[str]) -> s
|
|
|
207
205
|
if reminder_texts:
|
|
208
206
|
base += "\n" + "\n".join(reminder_texts)
|
|
209
207
|
return base
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def apply_config_defaults(param: "LLMCallParameter", config: "LLMConfigParameter") -> "LLMCallParameter":
|
|
211
|
+
"""Apply config defaults to LLM call parameters."""
|
|
212
|
+
if param.model is None:
|
|
213
|
+
param.model = config.model
|
|
214
|
+
if param.temperature is None:
|
|
215
|
+
param.temperature = config.temperature
|
|
216
|
+
if param.max_tokens is None:
|
|
217
|
+
param.max_tokens = config.max_tokens
|
|
218
|
+
if param.context_limit is None:
|
|
219
|
+
param.context_limit = config.context_limit
|
|
220
|
+
if param.verbosity is None:
|
|
221
|
+
param.verbosity = config.verbosity
|
|
222
|
+
if param.thinking is None:
|
|
223
|
+
param.thinking = config.thinking
|
|
224
|
+
if param.provider_routing is None:
|
|
225
|
+
param.provider_routing = config.provider_routing
|
|
226
|
+
|
|
227
|
+
if param.model is None:
|
|
228
|
+
raise ValueError("Model is required")
|
|
229
|
+
if param.max_tokens is None:
|
|
230
|
+
param.max_tokens = const.DEFAULT_MAX_TOKENS
|
|
231
|
+
if param.temperature is None:
|
|
232
|
+
param.temperature = const.DEFAULT_TEMPERATURE
|
|
233
|
+
if param.thinking is not None and param.thinking.type == "enabled" and param.thinking.budget_tokens is None:
|
|
234
|
+
param.thinking.budget_tokens = const.DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS
|
|
235
|
+
|
|
236
|
+
if param.model and "gpt-5" in param.model:
|
|
237
|
+
param.temperature = 1.0 # Required for GPT-5
|
|
238
|
+
|
|
239
|
+
return param
|