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.
Files changed (140) hide show
  1. klaude_code/cli/main.py +9 -4
  2. klaude_code/cli/runtime.py +42 -43
  3. klaude_code/command/__init__.py +7 -5
  4. klaude_code/command/clear_cmd.py +6 -29
  5. klaude_code/command/command_abc.py +44 -8
  6. klaude_code/command/diff_cmd.py +33 -27
  7. klaude_code/command/export_cmd.py +18 -26
  8. klaude_code/command/help_cmd.py +10 -8
  9. klaude_code/command/model_cmd.py +11 -40
  10. klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
  11. klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
  12. klaude_code/command/prompt-init.md +2 -5
  13. klaude_code/command/prompt_command.py +6 -6
  14. klaude_code/command/refresh_cmd.py +4 -5
  15. klaude_code/command/registry.py +16 -19
  16. klaude_code/command/terminal_setup_cmd.py +12 -11
  17. klaude_code/config/__init__.py +4 -0
  18. klaude_code/config/config.py +25 -26
  19. klaude_code/config/list_model.py +8 -3
  20. klaude_code/config/select_model.py +1 -1
  21. klaude_code/const/__init__.py +1 -1
  22. klaude_code/core/__init__.py +0 -3
  23. klaude_code/core/agent.py +25 -50
  24. klaude_code/core/executor.py +268 -101
  25. klaude_code/core/prompt.py +12 -12
  26. klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
  27. klaude_code/core/reminders.py +76 -95
  28. klaude_code/core/task.py +21 -14
  29. klaude_code/core/tool/__init__.py +45 -11
  30. klaude_code/core/tool/file/apply_patch.py +5 -1
  31. klaude_code/core/tool/file/apply_patch_tool.py +11 -13
  32. klaude_code/core/tool/file/edit_tool.py +27 -23
  33. klaude_code/core/tool/file/multi_edit_tool.py +15 -17
  34. klaude_code/core/tool/file/read_tool.py +41 -36
  35. klaude_code/core/tool/file/write_tool.py +13 -15
  36. klaude_code/core/tool/memory/memory_tool.py +85 -68
  37. klaude_code/core/tool/memory/skill_tool.py +10 -12
  38. klaude_code/core/tool/shell/bash_tool.py +24 -22
  39. klaude_code/core/tool/shell/command_safety.py +12 -1
  40. klaude_code/core/tool/sub_agent_tool.py +11 -12
  41. klaude_code/core/tool/todo/todo_write_tool.py +21 -28
  42. klaude_code/core/tool/todo/update_plan_tool.py +14 -24
  43. klaude_code/core/tool/tool_abc.py +3 -4
  44. klaude_code/core/tool/tool_context.py +7 -7
  45. klaude_code/core/tool/tool_registry.py +30 -47
  46. klaude_code/core/tool/tool_runner.py +35 -43
  47. klaude_code/core/tool/truncation.py +14 -20
  48. klaude_code/core/tool/web/mermaid_tool.py +12 -14
  49. klaude_code/core/tool/web/web_fetch_tool.py +15 -17
  50. klaude_code/core/turn.py +19 -7
  51. klaude_code/llm/__init__.py +3 -4
  52. klaude_code/llm/anthropic/client.py +30 -46
  53. klaude_code/llm/anthropic/input.py +4 -11
  54. klaude_code/llm/client.py +29 -8
  55. klaude_code/llm/input_common.py +66 -36
  56. klaude_code/llm/openai_compatible/client.py +42 -84
  57. klaude_code/llm/openai_compatible/input.py +11 -16
  58. klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
  59. klaude_code/llm/openrouter/client.py +40 -289
  60. klaude_code/llm/openrouter/input.py +13 -35
  61. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  62. klaude_code/llm/registry.py +5 -75
  63. klaude_code/llm/responses/client.py +34 -55
  64. klaude_code/llm/responses/input.py +24 -26
  65. klaude_code/llm/usage.py +109 -0
  66. klaude_code/protocol/__init__.py +4 -0
  67. klaude_code/protocol/events.py +3 -2
  68. klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
  69. klaude_code/protocol/model.py +49 -4
  70. klaude_code/protocol/op.py +18 -16
  71. klaude_code/protocol/op_handler.py +28 -0
  72. klaude_code/{core → protocol}/sub_agent.py +7 -0
  73. klaude_code/session/export.py +150 -70
  74. klaude_code/session/session.py +28 -14
  75. klaude_code/session/templates/export_session.html +180 -42
  76. klaude_code/trace/__init__.py +2 -2
  77. klaude_code/trace/log.py +11 -5
  78. klaude_code/ui/__init__.py +91 -8
  79. klaude_code/ui/core/__init__.py +1 -0
  80. klaude_code/ui/core/display.py +103 -0
  81. klaude_code/ui/core/input.py +71 -0
  82. klaude_code/ui/modes/__init__.py +1 -0
  83. klaude_code/ui/modes/debug/__init__.py +1 -0
  84. klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
  85. klaude_code/ui/modes/exec/__init__.py +1 -0
  86. klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
  87. klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
  88. klaude_code/ui/modes/repl/clipboard.py +152 -0
  89. klaude_code/ui/modes/repl/completers.py +429 -0
  90. klaude_code/ui/modes/repl/display.py +60 -0
  91. klaude_code/ui/modes/repl/event_handler.py +375 -0
  92. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  93. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  94. klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
  95. klaude_code/ui/renderers/assistant.py +21 -0
  96. klaude_code/ui/renderers/common.py +0 -16
  97. klaude_code/ui/renderers/developer.py +18 -18
  98. klaude_code/ui/renderers/diffs.py +36 -14
  99. klaude_code/ui/renderers/errors.py +1 -1
  100. klaude_code/ui/renderers/metadata.py +50 -27
  101. klaude_code/ui/renderers/sub_agent.py +43 -9
  102. klaude_code/ui/renderers/thinking.py +33 -1
  103. klaude_code/ui/renderers/tools.py +212 -20
  104. klaude_code/ui/renderers/user_input.py +19 -23
  105. klaude_code/ui/rich/__init__.py +1 -0
  106. klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
  107. klaude_code/ui/{renderers → rich}/status.py +29 -18
  108. klaude_code/ui/{base → rich}/theme.py +8 -2
  109. klaude_code/ui/terminal/__init__.py +1 -0
  110. klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
  111. klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
  112. klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
  113. klaude_code/ui/utils/__init__.py +1 -0
  114. klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
  115. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
  116. klaude_code-1.2.3.dist-info/RECORD +161 -0
  117. klaude_code/core/clipboard_manifest.py +0 -124
  118. klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
  119. klaude_code/ui/base/__init__.py +0 -1
  120. klaude_code/ui/base/display_abc.py +0 -36
  121. klaude_code/ui/base/input_abc.py +0 -20
  122. klaude_code/ui/repl/display.py +0 -36
  123. klaude_code/ui/repl/event_handler.py +0 -247
  124. klaude_code/ui/repl/input.py +0 -773
  125. klaude_code/ui/rich_ext/__init__.py +0 -1
  126. klaude_code-1.2.1.dist-info/RECORD +0 -151
  127. /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
  128. /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
  129. /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
  130. /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
  131. /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
  132. /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
  133. /klaude_code/ui/{base → core}/stage_manager.py +0 -0
  134. /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
  135. /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
  136. /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
  137. /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
  138. /klaude_code/ui/{base → utils}/debouncer.py +0 -0
  139. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
  140. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
@@ -1,49 +1,24 @@
1
1
  import json
2
- import time
3
2
  from collections.abc import AsyncGenerator
4
- from typing import Callable, Literal, ParamSpec, TypeVar, override
3
+ from typing import Literal, override
5
4
 
6
5
  import httpx
7
6
  import openai
8
7
  from openai import APIError, RateLimitError
9
8
 
10
- from klaude_code.llm.client import LLMClientABC
9
+ from klaude_code.llm.client import LLMClientABC, call_with_logged_payload
10
+ from klaude_code.llm.input_common import apply_config_defaults
11
11
  from klaude_code.llm.openai_compatible.input import convert_history_to_input, convert_tool_schema
12
12
  from klaude_code.llm.openai_compatible.tool_call_accumulator import BasicToolCallAccumulator, ToolCallAccumulatorABC
13
13
  from klaude_code.llm.registry import register
14
- from klaude_code.protocol import model
15
- from klaude_code.protocol.llm_parameter import (
16
- LLMCallParameter,
17
- LLMClientProtocol,
18
- LLMConfigParameter,
19
- apply_config_defaults,
20
- )
21
- from klaude_code.protocol.model import StreamErrorItem
14
+ from klaude_code.llm.usage import MetadataTracker, convert_usage
15
+ from klaude_code.protocol import llm_param, model
22
16
  from klaude_code.trace import DebugType, log_debug
23
17
 
24
- P = ParamSpec("P")
25
- R = TypeVar("R")
26
18
 
27
-
28
- def call_with_logged_payload(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
29
- """Call an SDK function while logging the JSON payload.
30
-
31
- The function reuses the original callable's type signature via ParamSpec
32
- so static type checkers can validate arguments at the call site.
33
- """
34
-
35
- payload = {k: v for k, v in kwargs.items() if v is not None}
36
- log_debug(
37
- json.dumps(payload, ensure_ascii=False, default=str),
38
- style="yellow",
39
- debug_type=DebugType.LLM_PAYLOAD,
40
- )
41
- return func(*args, **kwargs)
42
-
43
-
44
- @register(LLMClientProtocol.OPENAI)
19
+ @register(llm_param.LLMClientProtocol.OPENAI)
45
20
  class OpenAICompatibleClient(LLMClientABC):
46
- def __init__(self, config: LLMConfigParameter):
21
+ def __init__(self, config: llm_param.LLMConfigParameter):
47
22
  super().__init__(config)
48
23
  if config.is_azure:
49
24
  if not config.base_url:
@@ -64,18 +39,16 @@ class OpenAICompatibleClient(LLMClientABC):
64
39
 
65
40
  @classmethod
66
41
  @override
67
- def create(cls, config: LLMConfigParameter) -> "LLMClientABC":
42
+ def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
68
43
  return cls(config)
69
44
 
70
45
  @override
71
- async def call(self, param: LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
46
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
72
47
  param = apply_config_defaults(param, self.get_llm_config())
73
48
  messages = convert_history_to_input(param.input, param.system, param.model)
74
49
  tools = convert_tool_schema(param.tools)
75
50
 
76
- request_start_time = time.time()
77
- first_token_time: float | None = None
78
- last_token_time: float | None = None
51
+ metadata_tracker = MetadataTracker(cost_config=self._config.cost)
79
52
 
80
53
  extra_body = {}
81
54
  extra_headers = {"extra": json.dumps({"session_id": param.session_id})}
@@ -105,8 +78,8 @@ class OpenAICompatibleClient(LLMClientABC):
105
78
  accumulated_reasoning: list[str] = []
106
79
  accumulated_content: list[str] = []
107
80
  accumulated_tool_calls: ToolCallAccumulatorABC = BasicToolCallAccumulator()
81
+ emitted_tool_start_indices: set[int] = set()
108
82
  response_id: str | None = None
109
- metadata_item = model.ResponseMetadataItem()
110
83
 
111
84
  def flush_reasoning_items() -> list[model.ConversationItem]:
112
85
  nonlocal accumulated_reasoning
@@ -140,17 +113,23 @@ class OpenAICompatibleClient(LLMClientABC):
140
113
 
141
114
  try:
142
115
  async for event in await stream:
143
- log_debug(event.model_dump_json(exclude_none=True), style="blue", debug_type=DebugType.LLM_STREAM)
116
+ log_debug(
117
+ event.model_dump_json(exclude_none=True),
118
+ style="blue",
119
+ debug_type=DebugType.LLM_STREAM,
120
+ )
144
121
  if not response_id and event.id:
145
122
  response_id = event.id
146
123
  accumulated_tool_calls.response_id = response_id
147
124
  yield model.StartItem(response_id=response_id)
148
- if event.usage is not None and event.usage.completion_tokens is not None: # pyright: ignore[reportUnnecessaryComparison] gcp gemini will return None usage field
149
- metadata_item.usage = convert_usage(event.usage, param.context_limit)
125
+ if (
126
+ event.usage is not None and event.usage.completion_tokens is not None # pyright: ignore[reportUnnecessaryComparison] gcp gemini will return None usage field
127
+ ):
128
+ metadata_tracker.set_usage(convert_usage(event.usage, param.context_limit))
150
129
  if event.model:
151
- metadata_item.model_name = event.model
130
+ metadata_tracker.set_model_name(event.model)
152
131
  if provider := getattr(event, "provider", None):
153
- metadata_item.provider = str(provider)
132
+ metadata_tracker.set_provider(str(provider))
154
133
 
155
134
  if len(event.choices) == 0:
156
135
  continue
@@ -158,9 +137,11 @@ class OpenAICompatibleClient(LLMClientABC):
158
137
 
159
138
  # Support Kimi K2's usage field in choice
160
139
  if hasattr(event.choices[0], "usage") and getattr(event.choices[0], "usage"):
161
- metadata_item.usage = convert_usage(
162
- openai.types.CompletionUsage.model_validate(getattr(event.choices[0], "usage")),
163
- param.context_limit,
140
+ metadata_tracker.set_usage(
141
+ convert_usage(
142
+ openai.types.CompletionUsage.model_validate(getattr(event.choices[0], "usage")),
143
+ param.context_limit,
144
+ )
164
145
  )
165
146
 
166
147
  # Reasoning
@@ -170,9 +151,7 @@ class OpenAICompatibleClient(LLMClientABC):
170
151
  if hasattr(delta, "reasoning_content") and getattr(delta, "reasoning_content"):
171
152
  reasoning_content = getattr(delta, "reasoning_content")
172
153
  if reasoning_content:
173
- if first_token_time is None:
174
- first_token_time = time.time()
175
- last_token_time = time.time()
154
+ metadata_tracker.record_token()
176
155
  stage = "reasoning"
177
156
  accumulated_reasoning.append(reasoning_content)
178
157
 
@@ -180,9 +159,7 @@ class OpenAICompatibleClient(LLMClientABC):
180
159
  if delta.content and (
181
160
  stage == "assistant" or delta.content.strip()
182
161
  ): # Process all content in assistant stage, filter empty content in reasoning stage
183
- if first_token_time is None:
184
- first_token_time = time.time()
185
- last_token_time = time.time()
162
+ metadata_tracker.record_token()
186
163
  if stage == "reasoning":
187
164
  for item in flush_reasoning_items():
188
165
  yield item
@@ -198,9 +175,7 @@ class OpenAICompatibleClient(LLMClientABC):
198
175
 
199
176
  # Tool
200
177
  if delta.tool_calls and len(delta.tool_calls) > 0:
201
- if first_token_time is None:
202
- first_token_time = time.time()
203
- last_token_time = time.time()
178
+ metadata_tracker.record_token()
204
179
  if stage == "reasoning":
205
180
  for item in flush_reasoning_items():
206
181
  yield item
@@ -208,9 +183,18 @@ class OpenAICompatibleClient(LLMClientABC):
208
183
  for item in flush_assistant_items():
209
184
  yield item
210
185
  stage = "tool"
186
+ # Emit ToolCallStartItem for new tool calls
187
+ for tc in delta.tool_calls:
188
+ if tc.index not in emitted_tool_start_indices and tc.function and tc.function.name:
189
+ emitted_tool_start_indices.add(tc.index)
190
+ yield model.ToolCallStartItem(
191
+ response_id=response_id,
192
+ call_id=tc.id or "",
193
+ name=tc.function.name,
194
+ )
211
195
  accumulated_tool_calls.add(delta.tool_calls)
212
196
  except (RateLimitError, APIError) as e:
213
- yield StreamErrorItem(error=f"{e.__class__.__name__} {str(e)}")
197
+ yield model.StreamErrorItem(error=f"{e.__class__.__name__} {str(e)}")
214
198
 
215
199
  # Finalize
216
200
  for item in flush_reasoning_items():
@@ -223,31 +207,5 @@ class OpenAICompatibleClient(LLMClientABC):
223
207
  for tool_call_item in flush_tool_call_items():
224
208
  yield tool_call_item
225
209
 
226
- metadata_item.response_id = response_id
227
-
228
- # Calculate performance metrics if we have timing data
229
- if metadata_item.usage and first_token_time is not None:
230
- metadata_item.usage.first_token_latency_ms = (first_token_time - request_start_time) * 1000
231
-
232
- if last_token_time is not None and metadata_item.usage.output_tokens > 0:
233
- time_duration = last_token_time - first_token_time
234
- if time_duration >= 0.15:
235
- metadata_item.usage.throughput_tps = metadata_item.usage.output_tokens / time_duration
236
-
237
- yield metadata_item
238
-
239
-
240
- def convert_usage(usage: openai.types.CompletionUsage, context_limit: int | None = None) -> model.Usage:
241
- total_tokens = usage.total_tokens
242
- context_usage_percent = (total_tokens / context_limit) * 100 if context_limit else None
243
- return model.Usage(
244
- input_tokens=usage.prompt_tokens,
245
- cached_tokens=(usage.prompt_tokens_details.cached_tokens if usage.prompt_tokens_details else 0) or 0,
246
- reasoning_tokens=(usage.completion_tokens_details.reasoning_tokens if usage.completion_tokens_details else 0)
247
- or 0,
248
- output_tokens=usage.completion_tokens,
249
- total_tokens=total_tokens,
250
- context_usage_percent=context_usage_percent,
251
- throughput_tps=None,
252
- first_token_latency_ms=None,
253
- )
210
+ metadata_tracker.set_response_id(response_id)
211
+ yield metadata_tracker.finalize()
@@ -6,15 +6,8 @@
6
6
  from openai.types import chat
7
7
  from openai.types.chat import ChatCompletionContentPartParam
8
8
 
9
- from klaude_code.llm.input_common import (
10
- AssistantGroup,
11
- ToolGroup,
12
- UserGroup,
13
- merge_reminder_text,
14
- parse_message_groups,
15
- )
16
- from klaude_code.protocol.llm_parameter import ToolSchema
17
- from klaude_code.protocol.model import ConversationItem, ImageURLPart
9
+ from klaude_code.llm.input_common import AssistantGroup, ToolGroup, UserGroup, merge_reminder_text, parse_message_groups
10
+ from klaude_code.protocol import llm_param, model
18
11
 
19
12
 
20
13
  def _user_group_to_message(group: UserGroup) -> chat.ChatCompletionMessageParam:
@@ -39,7 +32,9 @@ def _tool_group_to_message(group: ToolGroup) -> chat.ChatCompletionMessageParam:
39
32
  }
40
33
 
41
34
 
42
- def _assistant_group_to_message(group: AssistantGroup) -> chat.ChatCompletionMessageParam:
35
+ def _assistant_group_to_message(
36
+ group: AssistantGroup,
37
+ ) -> chat.ChatCompletionMessageParam:
43
38
  assistant_message: dict[str, object] = {"role": "assistant"}
44
39
 
45
40
  if group.text_content:
@@ -61,13 +56,15 @@ def _assistant_group_to_message(group: AssistantGroup) -> chat.ChatCompletionMes
61
56
  return assistant_message
62
57
 
63
58
 
64
- def build_user_content_parts(images: list[ImageURLPart]) -> list[ChatCompletionContentPartParam]:
59
+ def build_user_content_parts(
60
+ images: list[model.ImageURLPart],
61
+ ) -> list[ChatCompletionContentPartParam]:
65
62
  """Build content parts for images only. Used by OpenRouter."""
66
63
  return [{"type": "image_url", "image_url": {"url": image.image_url.url}} for image in images]
67
64
 
68
65
 
69
66
  def convert_history_to_input(
70
- history: list[ConversationItem],
67
+ history: list[model.ConversationItem],
71
68
  system: str | None = None,
72
69
  model_name: str | None = None,
73
70
  ) -> list[chat.ChatCompletionMessageParam]:
@@ -79,9 +76,7 @@ def convert_history_to_input(
79
76
  system: System message.
80
77
  model_name: Model name. Not used in OpenAI-compatible, kept for API consistency.
81
78
  """
82
- messages: list[chat.ChatCompletionMessageParam] = (
83
- [{"role": "system", "content": system}] if system else []
84
- )
79
+ messages: list[chat.ChatCompletionMessageParam] = [{"role": "system", "content": system}] if system else []
85
80
 
86
81
  for group in parse_message_groups(history):
87
82
  match group:
@@ -96,7 +91,7 @@ def convert_history_to_input(
96
91
 
97
92
 
98
93
  def convert_tool_schema(
99
- tools: list[ToolSchema] | None,
94
+ tools: list[llm_param.ToolSchema] | None,
100
95
  ) -> list[chat.ChatCompletionToolParam]:
101
96
  if tools is None:
102
97
  return []
@@ -8,7 +8,7 @@ from klaude_code.protocol import model
8
8
 
9
9
  class ToolCallAccumulatorABC(ABC):
10
10
  @abstractmethod
11
- def add(self, chunks: list[ChoiceDeltaToolCall]):
11
+ def add(self, chunks: list[ChoiceDeltaToolCall]) -> None:
12
12
  pass
13
13
 
14
14
  @abstractmethod
@@ -50,7 +50,7 @@ class BasicToolCallAccumulator(ToolCallAccumulatorABC, BaseModel):
50
50
  chunks_by_step: list[list[ChoiceDeltaToolCall]] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
51
51
  response_id: str | None = None
52
52
 
53
- def add(self, chunks: list[ChoiceDeltaToolCall]):
53
+ def add(self, chunks: list[ChoiceDeltaToolCall]) -> None:
54
54
  self.chunks_by_step.append(chunks)
55
55
 
56
56
  def get(self) -> list[model.ToolCallItem]: