klaude-code 1.2.6__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 (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,221 @@
1
+ import json
2
+ import time
3
+ from collections.abc import AsyncGenerator
4
+ from typing import override
5
+
6
+ import anthropic
7
+ import httpx
8
+ from anthropic import RateLimitError
9
+ from anthropic.types.beta.beta_input_json_delta import BetaInputJSONDelta
10
+ from anthropic.types.beta.beta_raw_content_block_delta_event import BetaRawContentBlockDeltaEvent
11
+ from anthropic.types.beta.beta_raw_content_block_start_event import BetaRawContentBlockStartEvent
12
+ from anthropic.types.beta.beta_raw_content_block_stop_event import BetaRawContentBlockStopEvent
13
+ from anthropic.types.beta.beta_raw_message_delta_event import BetaRawMessageDeltaEvent
14
+ from anthropic.types.beta.beta_raw_message_start_event import BetaRawMessageStartEvent
15
+ from anthropic.types.beta.beta_signature_delta import BetaSignatureDelta
16
+ from anthropic.types.beta.beta_text_delta import BetaTextDelta
17
+ from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
18
+ from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
19
+
20
+ from klaude_code import const
21
+ from klaude_code.llm.anthropic.input import convert_history_to_input, convert_system_to_input, convert_tool_schema
22
+ from klaude_code.llm.client import LLMClientABC, call_with_logged_payload
23
+ from klaude_code.llm.input_common import apply_config_defaults
24
+ from klaude_code.llm.registry import register
25
+ from klaude_code.llm.usage import calculate_cost
26
+ from klaude_code.protocol import llm_param, model
27
+ from klaude_code.trace import DebugType, log_debug
28
+
29
+
30
+ @register(llm_param.LLMClientProtocol.ANTHROPIC)
31
+ class AnthropicClient(LLMClientABC):
32
+ def __init__(self, config: llm_param.LLMConfigParameter):
33
+ super().__init__(config)
34
+ client = anthropic.AsyncAnthropic(
35
+ api_key=config.api_key,
36
+ base_url=config.base_url,
37
+ timeout=httpx.Timeout(300.0, connect=15.0, read=285.0),
38
+ )
39
+ self.client: anthropic.AsyncAnthropic = client
40
+
41
+ @classmethod
42
+ @override
43
+ def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
44
+ return cls(config)
45
+
46
+ @override
47
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
48
+ param = apply_config_defaults(param, self.get_llm_config())
49
+
50
+ request_start_time = time.time()
51
+ first_token_time: float | None = None
52
+ last_token_time: float | None = None
53
+
54
+ messages = convert_history_to_input(param.input, param.model)
55
+ tools = convert_tool_schema(param.tools)
56
+ system = convert_system_to_input(param.system)
57
+
58
+ stream = call_with_logged_payload(
59
+ self.client.beta.messages.create,
60
+ model=str(param.model),
61
+ tool_choice={
62
+ "type": "auto",
63
+ "disable_parallel_tool_use": False,
64
+ },
65
+ stream=True,
66
+ max_tokens=param.max_tokens or const.DEFAULT_MAX_TOKENS,
67
+ temperature=param.temperature or const.DEFAULT_TEMPERATURE,
68
+ messages=messages,
69
+ system=system,
70
+ tools=tools,
71
+ betas=["interleaved-thinking-2025-05-14", "context-1m-2025-08-07"],
72
+ thinking=anthropic.types.ThinkingConfigEnabledParam(
73
+ type=param.thinking.type,
74
+ budget_tokens=param.thinking.budget_tokens or const.DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
75
+ )
76
+ if param.thinking and param.thinking.type == "enabled"
77
+ else anthropic.types.ThinkingConfigDisabledParam(
78
+ type="disabled",
79
+ ),
80
+ extra_headers={"extra": json.dumps({"session_id": param.session_id})},
81
+ )
82
+
83
+ accumulated_thinking: list[str] = []
84
+ accumulated_content: list[str] = []
85
+ response_id: str | None = None
86
+
87
+ current_tool_name: str | None = None
88
+ current_tool_call_id: str | None = None
89
+ current_tool_inputs: list[str] | None = None
90
+
91
+ input_tokens = 0
92
+ cached_tokens = 0
93
+ output_tokens = 0
94
+
95
+ try:
96
+ async for event in await stream:
97
+ log_debug(
98
+ f"[{event.type}]",
99
+ event.model_dump_json(exclude_none=True),
100
+ style="blue",
101
+ debug_type=DebugType.LLM_STREAM,
102
+ )
103
+ match event:
104
+ case BetaRawMessageStartEvent() as event:
105
+ response_id = event.message.id
106
+ cached_tokens = event.message.usage.cache_read_input_tokens or 0
107
+ input_tokens = (event.message.usage.input_tokens or 0) + (
108
+ event.message.usage.cache_creation_input_tokens or 0
109
+ )
110
+ output_tokens = event.message.usage.output_tokens or 0
111
+ yield model.StartItem(response_id=response_id)
112
+ case BetaRawContentBlockDeltaEvent() as event:
113
+ match event.delta:
114
+ case BetaThinkingDelta() as delta:
115
+ if first_token_time is None:
116
+ first_token_time = time.time()
117
+ last_token_time = time.time()
118
+ accumulated_thinking.append(delta.thinking)
119
+ case BetaSignatureDelta() as delta:
120
+ if first_token_time is None:
121
+ first_token_time = time.time()
122
+ last_token_time = time.time()
123
+ yield model.ReasoningEncryptedItem(
124
+ encrypted_content=delta.signature,
125
+ response_id=response_id,
126
+ model=str(param.model),
127
+ )
128
+ case BetaTextDelta() as delta:
129
+ if first_token_time is None:
130
+ first_token_time = time.time()
131
+ last_token_time = time.time()
132
+ accumulated_content.append(delta.text)
133
+ yield model.AssistantMessageDelta(
134
+ content=delta.text,
135
+ response_id=response_id,
136
+ )
137
+ case BetaInputJSONDelta() as delta:
138
+ if first_token_time is None:
139
+ first_token_time = time.time()
140
+ last_token_time = time.time()
141
+ if current_tool_inputs is not None:
142
+ current_tool_inputs.append(delta.partial_json)
143
+ case _:
144
+ pass
145
+ case BetaRawContentBlockStartEvent() as event:
146
+ match event.content_block:
147
+ case BetaToolUseBlock() as block:
148
+ yield model.ToolCallStartItem(
149
+ response_id=response_id,
150
+ call_id=block.id,
151
+ name=block.name,
152
+ )
153
+ current_tool_name = block.name
154
+ current_tool_call_id = block.id
155
+ current_tool_inputs = []
156
+ case _:
157
+ pass
158
+ case BetaRawContentBlockStopEvent() as event:
159
+ if len(accumulated_thinking) > 0:
160
+ full_thinking = "".join(accumulated_thinking)
161
+ yield model.ReasoningTextItem(
162
+ content=full_thinking,
163
+ response_id=response_id,
164
+ model=str(param.model),
165
+ )
166
+ accumulated_thinking.clear()
167
+ if len(accumulated_content) > 0:
168
+ yield model.AssistantMessageItem(
169
+ content="".join(accumulated_content),
170
+ response_id=response_id,
171
+ )
172
+ accumulated_content.clear()
173
+ if current_tool_name and current_tool_call_id:
174
+ yield model.ToolCallItem(
175
+ name=current_tool_name,
176
+ call_id=current_tool_call_id,
177
+ arguments="".join(current_tool_inputs) if current_tool_inputs else "",
178
+ response_id=response_id,
179
+ )
180
+ current_tool_name = None
181
+ current_tool_call_id = None
182
+ current_tool_inputs = None
183
+ case BetaRawMessageDeltaEvent() as event:
184
+ input_tokens += (event.usage.input_tokens or 0) + (event.usage.cache_creation_input_tokens or 0)
185
+ output_tokens += event.usage.output_tokens or 0
186
+ cached_tokens += event.usage.cache_read_input_tokens or 0
187
+ total_tokens = input_tokens + cached_tokens + output_tokens
188
+ context_usage_percent = (
189
+ (total_tokens / param.context_limit) * 100 if param.context_limit else None
190
+ )
191
+
192
+ throughput_tps: float | None = None
193
+ first_token_latency_ms: float | None = None
194
+
195
+ if first_token_time is not None:
196
+ first_token_latency_ms = (first_token_time - request_start_time) * 1000
197
+
198
+ if first_token_time is not None and last_token_time is not None and output_tokens > 0:
199
+ time_duration = last_token_time - first_token_time
200
+ if time_duration >= 0.15:
201
+ throughput_tps = output_tokens / time_duration
202
+
203
+ usage = model.Usage(
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)
213
+ yield model.ResponseMetadataItem(
214
+ usage=usage,
215
+ response_id=response_id,
216
+ model_name=str(param.model),
217
+ )
218
+ case _:
219
+ pass
220
+ except RateLimitError as e:
221
+ yield model.StreamErrorItem(error=f"{e.__class__.__name__} {str(e)}")
@@ -0,0 +1,200 @@
1
+ # pyright: reportReturnType=false
2
+ # pyright: reportArgumentType=false
3
+ # pyright: reportUnknownMemberType=false
4
+ # pyright: reportAttributeAccessIssue=false
5
+ # pyright: reportUnknownVariableType=false
6
+
7
+
8
+ import json
9
+ from base64 import b64decode
10
+ from binascii import Error as BinasciiError
11
+ from typing import Literal, cast
12
+
13
+ from anthropic.types.beta.beta_base64_image_source_param import BetaBase64ImageSourceParam
14
+ from anthropic.types.beta.beta_image_block_param import BetaImageBlockParam
15
+ from anthropic.types.beta.beta_message_param import BetaMessageParam
16
+ from anthropic.types.beta.beta_text_block_param import BetaTextBlockParam
17
+ from anthropic.types.beta.beta_tool_param import BetaToolParam
18
+ from anthropic.types.beta.beta_url_image_source_param import BetaURLImageSourceParam
19
+
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
22
+
23
+ AllowedMediaType = Literal["image/png", "image/jpeg", "image/gif", "image/webp"]
24
+ _INLINE_IMAGE_MEDIA_TYPES: tuple[AllowedMediaType, ...] = (
25
+ "image/png",
26
+ "image/jpeg",
27
+ "image/gif",
28
+ "image/webp",
29
+ )
30
+
31
+
32
+ def _image_part_to_block(image: model.ImageURLPart) -> BetaImageBlockParam:
33
+ url = image.image_url.url
34
+ if url.startswith("data:"):
35
+ header_and_media = url.split(",", 1)
36
+ if len(header_and_media) != 2:
37
+ raise ValueError("Invalid data URL for image: missing comma separator")
38
+ header, base64_data = header_and_media
39
+ if ";base64" not in header:
40
+ raise ValueError("Invalid data URL for image: missing base64 marker")
41
+ media_type = header[5:].split(";", 1)[0]
42
+ if media_type not in _INLINE_IMAGE_MEDIA_TYPES:
43
+ raise ValueError(f"Unsupported inline image media type: {media_type}")
44
+ base64_payload = base64_data.strip()
45
+ if base64_payload == "":
46
+ raise ValueError("Inline image data is empty")
47
+ try:
48
+ b64decode(base64_payload, validate=True)
49
+ except (BinasciiError, ValueError) as exc:
50
+ raise ValueError("Inline image data is not valid base64") from exc
51
+ source = cast(
52
+ BetaBase64ImageSourceParam,
53
+ {
54
+ "type": "base64",
55
+ "media_type": media_type,
56
+ "data": base64_payload,
57
+ },
58
+ )
59
+ return {"type": "image", "source": source}
60
+
61
+ source_url: BetaURLImageSourceParam = {"type": "url", "url": url}
62
+ return {"type": "image", "source": source_url}
63
+
64
+
65
+ def _user_group_to_message(group: UserGroup) -> BetaMessageParam:
66
+ blocks: list[BetaTextBlockParam | BetaImageBlockParam] = []
67
+ for text in group.text_parts:
68
+ blocks.append({"type": "text", "text": text + "\n"})
69
+ for image in group.images:
70
+ blocks.append(_image_part_to_block(image))
71
+ if not blocks:
72
+ blocks.append({"type": "text", "text": ""})
73
+ return {"role": "user", "content": blocks}
74
+
75
+
76
+ def _tool_group_to_message(group: ToolGroup) -> BetaMessageParam:
77
+ tool_content: list[BetaTextBlockParam | BetaImageBlockParam] = []
78
+ merged_text = merge_reminder_text(
79
+ group.tool_result.output or "<system-reminder>Tool ran without output or errors</system-reminder>",
80
+ group.reminder_texts,
81
+ )
82
+ tool_content.append({"type": "text", "text": merged_text})
83
+ for image in group.tool_result.images or []:
84
+ tool_content.append(_image_part_to_block(image))
85
+ for image in group.reminder_images:
86
+ tool_content.append(_image_part_to_block(image))
87
+ return {
88
+ "role": "user",
89
+ "content": [
90
+ {
91
+ "type": "tool_result",
92
+ "tool_use_id": group.tool_result.call_id,
93
+ "is_error": group.tool_result.status == "error",
94
+ "content": tool_content,
95
+ }
96
+ ],
97
+ }
98
+
99
+
100
+ def _assistant_group_to_message(group: AssistantGroup, model_name: str | None) -> BetaMessageParam:
101
+ content: list[dict[str, object]] = []
102
+ current_reasoning_content: str | None = None
103
+
104
+ # Process reasoning items in original order so that text and
105
+ # encrypted parts are paired correctly for the given model.
106
+ for item in group.reasoning_items:
107
+ if isinstance(item, model.ReasoningTextItem):
108
+ if model_name != item.model:
109
+ continue
110
+ current_reasoning_content = item.content
111
+ else:
112
+ if model_name != item.model:
113
+ continue
114
+ if item.encrypted_content and len(item.encrypted_content) > 0:
115
+ content.append(
116
+ {
117
+ "type": "thinking",
118
+ "thinking": current_reasoning_content or "",
119
+ "signature": item.encrypted_content,
120
+ }
121
+ )
122
+ current_reasoning_content = None
123
+
124
+ # Moonshot.ai's Kimi does not always send reasoning signatures;
125
+ # if we saw reasoning text without any matching encrypted item,
126
+ # emit it as a plain thinking block.
127
+ if len(current_reasoning_content or "") > 0:
128
+ content.insert(0, {"type": "thinking", "thinking": current_reasoning_content})
129
+
130
+ if group.text_content:
131
+ content.append({"type": "text", "text": group.text_content})
132
+
133
+ for tc in group.tool_calls:
134
+ content.append(
135
+ {
136
+ "type": "tool_use",
137
+ "id": tc.call_id,
138
+ "name": tc.name,
139
+ "input": json.loads(tc.arguments) if tc.arguments else None,
140
+ }
141
+ )
142
+
143
+ return {"role": "assistant", "content": content}
144
+
145
+
146
+ def _add_cache_control(messages: list[BetaMessageParam]) -> None:
147
+ if len(messages) > 0:
148
+ last_message = messages[-1]
149
+ content_list = list(last_message.get("content", []))
150
+ if content_list:
151
+ last_content_part = content_list[-1]
152
+ if last_content_part.get("type", "") in ["text", "tool_result", "tool_use"]:
153
+ last_content_part["cache_control"] = {"type": "ephemeral"} # type: ignore
154
+
155
+
156
+ def convert_history_to_input(
157
+ history: list[model.ConversationItem],
158
+ model_name: str | None,
159
+ ) -> list[BetaMessageParam]:
160
+ """
161
+ Convert a list of conversation items to a list of beta message params.
162
+
163
+ Args:
164
+ history: List of conversation items.
165
+ model_name: Model name. Used to verify that signatures are valid for the same model
166
+ """
167
+ messages: list[BetaMessageParam] = []
168
+ for group in parse_message_groups(history):
169
+ match group:
170
+ case UserGroup():
171
+ messages.append(_user_group_to_message(group))
172
+ case ToolGroup():
173
+ messages.append(_tool_group_to_message(group))
174
+ case AssistantGroup():
175
+ messages.append(_assistant_group_to_message(group, model_name))
176
+
177
+ _add_cache_control(messages)
178
+ return messages
179
+
180
+
181
+ def convert_system_to_input(system: str | None) -> list[BetaTextBlockParam]:
182
+ if system is None:
183
+ return []
184
+ return [{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}]
185
+
186
+
187
+ def convert_tool_schema(
188
+ tools: list[llm_param.ToolSchema] | None,
189
+ ) -> list[BetaToolParam]:
190
+ if tools is None:
191
+ return []
192
+ return [
193
+ {
194
+ "input_schema": tool.parameters,
195
+ "type": "custom",
196
+ "name": tool.name,
197
+ "description": tool.description,
198
+ }
199
+ for tool in tools
200
+ ]
@@ -0,0 +1,49 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from collections.abc import AsyncGenerator
4
+ from typing import Callable, ParamSpec, TypeVar, cast
5
+
6
+ from klaude_code.protocol import llm_param, model
7
+ from klaude_code.trace import DebugType, log_debug
8
+
9
+
10
+ class LLMClientABC(ABC):
11
+ def __init__(self, config: llm_param.LLMConfigParameter) -> None:
12
+ self._config = config
13
+
14
+ @classmethod
15
+ @abstractmethod
16
+ def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
17
+ pass
18
+
19
+ @abstractmethod
20
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
21
+ raise NotImplementedError
22
+ yield cast(model.ConversationItem, None) # pyright: ignore[reportUnreachable]
23
+
24
+ def get_llm_config(self) -> llm_param.LLMConfigParameter:
25
+ return self._config
26
+
27
+ @property
28
+ def model_name(self) -> str:
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)