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
@@ -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.llm_parameter import ToolSchema
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": "default"},
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.llm_parameter import ToolSchema
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.tool_abc import ToolABC
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.client import LLMClientABC
16
- from klaude_code.protocol import events, llm_parameter, model
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[llm_parameter.ToolSchema]
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
- llm_parameter.LLMCallParameter(
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("[StreamError]", item.error, style="red", debug_type=DebugType.RESPONSE)
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 _:
@@ -1,19 +1,18 @@
1
1
  """LLM package init.
2
2
 
3
- Ensures built-in clients are imported so their `@register` decorators run
4
- and they become available via the registry.
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 LLMClients, create_llm_client
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 Callable, ParamSpec, TypeVar, override
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.protocol import llm_parameter, model
24
- from klaude_code.protocol.llm_parameter import (
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 llm_parameter.DEFAULT_MAX_TOKENS,
90
- temperature=param.temperature or llm_parameter.DEFAULT_TEMPERATURE,
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 llm_parameter.DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
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=model.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
- AssistantGroup,
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, protocol_model.ReasoningTextItem):
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[llm_parameter.ToolSchema] | None,
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.llm_parameter import LLMCallParameter, LLMConfigParameter
6
- from klaude_code.protocol.model import ConversationItem
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)
@@ -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.protocol.model import (
14
- AssistantMessageItem,
15
- ConversationItem,
16
- DeveloperMessageItem,
17
- ImageURLPart,
18
- ReasoningEncryptedItem,
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(item, (ReasoningTextItem, ReasoningEncryptedItem, AssistantMessageItem, ToolCallItem)):
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