klaude-code 1.8.0__py3-none-any.whl → 2.0.0__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 (142) hide show
  1. klaude_code/auth/base.py +97 -0
  2. klaude_code/auth/claude/__init__.py +6 -0
  3. klaude_code/auth/claude/exceptions.py +9 -0
  4. klaude_code/auth/claude/oauth.py +172 -0
  5. klaude_code/auth/claude/token_manager.py +26 -0
  6. klaude_code/auth/codex/token_manager.py +10 -50
  7. klaude_code/cli/auth_cmd.py +127 -46
  8. klaude_code/cli/config_cmd.py +4 -2
  9. klaude_code/cli/cost_cmd.py +14 -9
  10. klaude_code/cli/list_model.py +248 -200
  11. klaude_code/cli/main.py +1 -1
  12. klaude_code/cli/runtime.py +7 -5
  13. klaude_code/cli/self_update.py +1 -1
  14. klaude_code/cli/session_cmd.py +1 -1
  15. klaude_code/command/clear_cmd.py +6 -2
  16. klaude_code/command/command_abc.py +2 -2
  17. klaude_code/command/debug_cmd.py +4 -4
  18. klaude_code/command/export_cmd.py +2 -2
  19. klaude_code/command/export_online_cmd.py +12 -12
  20. klaude_code/command/fork_session_cmd.py +29 -23
  21. klaude_code/command/help_cmd.py +4 -4
  22. klaude_code/command/model_cmd.py +4 -4
  23. klaude_code/command/model_select.py +1 -1
  24. klaude_code/command/prompt-commit.md +82 -0
  25. klaude_code/command/prompt_command.py +3 -3
  26. klaude_code/command/refresh_cmd.py +2 -2
  27. klaude_code/command/registry.py +7 -5
  28. klaude_code/command/release_notes_cmd.py +4 -4
  29. klaude_code/command/resume_cmd.py +15 -11
  30. klaude_code/command/status_cmd.py +4 -4
  31. klaude_code/command/terminal_setup_cmd.py +8 -8
  32. klaude_code/command/thinking_cmd.py +4 -4
  33. klaude_code/config/assets/builtin_config.yaml +52 -3
  34. klaude_code/config/builtin_config.py +16 -5
  35. klaude_code/config/config.py +31 -7
  36. klaude_code/config/thinking.py +4 -4
  37. klaude_code/const.py +146 -91
  38. klaude_code/core/agent.py +3 -12
  39. klaude_code/core/executor.py +21 -13
  40. klaude_code/core/manager/sub_agent_manager.py +71 -7
  41. klaude_code/core/prompt.py +1 -1
  42. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  43. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  44. klaude_code/core/reminders.py +88 -69
  45. klaude_code/core/task.py +44 -45
  46. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  47. klaude_code/core/tool/file/diff_builder.py +3 -5
  48. klaude_code/core/tool/file/edit_tool.py +23 -23
  49. klaude_code/core/tool/file/move_tool.py +43 -43
  50. klaude_code/core/tool/file/read_tool.py +44 -39
  51. klaude_code/core/tool/file/write_tool.py +14 -14
  52. klaude_code/core/tool/report_back_tool.py +4 -4
  53. klaude_code/core/tool/shell/bash_tool.py +23 -23
  54. klaude_code/core/tool/skill/skill_tool.py +7 -7
  55. klaude_code/core/tool/sub_agent_tool.py +38 -9
  56. klaude_code/core/tool/todo/todo_write_tool.py +8 -8
  57. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  58. klaude_code/core/tool/tool_abc.py +2 -2
  59. klaude_code/core/tool/tool_context.py +27 -0
  60. klaude_code/core/tool/tool_runner.py +88 -42
  61. klaude_code/core/tool/truncation.py +38 -20
  62. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  63. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  64. klaude_code/core/tool/web/web_search_tool.py +15 -17
  65. klaude_code/core/turn.py +120 -73
  66. klaude_code/llm/anthropic/client.py +104 -44
  67. klaude_code/llm/anthropic/input.py +116 -108
  68. klaude_code/llm/bedrock/client.py +8 -5
  69. klaude_code/llm/claude/__init__.py +3 -0
  70. klaude_code/llm/claude/client.py +105 -0
  71. klaude_code/llm/client.py +4 -3
  72. klaude_code/llm/codex/client.py +16 -10
  73. klaude_code/llm/google/client.py +122 -60
  74. klaude_code/llm/google/input.py +94 -108
  75. klaude_code/llm/image.py +123 -0
  76. klaude_code/llm/input_common.py +136 -189
  77. klaude_code/llm/openai_compatible/client.py +17 -7
  78. klaude_code/llm/openai_compatible/input.py +36 -66
  79. klaude_code/llm/openai_compatible/stream.py +119 -67
  80. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  81. klaude_code/llm/openrouter/client.py +34 -9
  82. klaude_code/llm/openrouter/input.py +63 -64
  83. klaude_code/llm/openrouter/reasoning.py +22 -24
  84. klaude_code/llm/registry.py +20 -15
  85. klaude_code/llm/responses/client.py +107 -45
  86. klaude_code/llm/responses/input.py +115 -98
  87. klaude_code/llm/usage.py +52 -25
  88. klaude_code/protocol/__init__.py +1 -0
  89. klaude_code/protocol/events.py +16 -12
  90. klaude_code/protocol/llm_param.py +22 -3
  91. klaude_code/protocol/message.py +250 -0
  92. klaude_code/protocol/model.py +94 -281
  93. klaude_code/protocol/op.py +2 -2
  94. klaude_code/protocol/sub_agent/__init__.py +2 -2
  95. klaude_code/protocol/sub_agent/explore.py +10 -0
  96. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  97. klaude_code/protocol/sub_agent/task.py +10 -0
  98. klaude_code/protocol/sub_agent/web.py +10 -0
  99. klaude_code/session/codec.py +6 -6
  100. klaude_code/session/export.py +261 -62
  101. klaude_code/session/selector.py +7 -24
  102. klaude_code/session/session.py +125 -53
  103. klaude_code/session/store.py +5 -32
  104. klaude_code/session/templates/export_session.html +1 -1
  105. klaude_code/session/templates/mermaid_viewer.html +1 -1
  106. klaude_code/trace/log.py +11 -6
  107. klaude_code/ui/core/input.py +1 -1
  108. klaude_code/ui/core/stage_manager.py +1 -8
  109. klaude_code/ui/modes/debug/display.py +2 -2
  110. klaude_code/ui/modes/repl/clipboard.py +2 -2
  111. klaude_code/ui/modes/repl/completers.py +18 -10
  112. klaude_code/ui/modes/repl/event_handler.py +136 -127
  113. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  114. klaude_code/ui/modes/repl/key_bindings.py +1 -1
  115. klaude_code/ui/modes/repl/renderer.py +107 -15
  116. klaude_code/ui/renderers/assistant.py +2 -2
  117. klaude_code/ui/renderers/common.py +65 -7
  118. klaude_code/ui/renderers/developer.py +7 -6
  119. klaude_code/ui/renderers/diffs.py +11 -11
  120. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  121. klaude_code/ui/renderers/metadata.py +39 -31
  122. klaude_code/ui/renderers/sub_agent.py +57 -16
  123. klaude_code/ui/renderers/thinking.py +37 -2
  124. klaude_code/ui/renderers/tools.py +180 -165
  125. klaude_code/ui/rich/live.py +3 -1
  126. klaude_code/ui/rich/markdown.py +39 -7
  127. klaude_code/ui/rich/quote.py +76 -1
  128. klaude_code/ui/rich/status.py +14 -8
  129. klaude_code/ui/rich/theme.py +13 -6
  130. klaude_code/ui/terminal/image.py +34 -0
  131. klaude_code/ui/terminal/notifier.py +2 -1
  132. klaude_code/ui/terminal/progress_bar.py +4 -4
  133. klaude_code/ui/terminal/selector.py +22 -4
  134. klaude_code/ui/utils/common.py +55 -0
  135. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +28 -6
  136. klaude_code-2.0.0.dist-info/RECORD +229 -0
  137. klaude_code/command/prompt-jj-describe.md +0 -32
  138. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
  139. klaude_code/protocol/sub_agent/oracle.py +0 -91
  140. klaude_code-1.8.0.dist-info/RECORD +0 -219
  141. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
  142. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,7 @@ from typing import Any, override
6
6
  import anthropic
7
7
  import httpx
8
8
  from anthropic import APIError
9
+ from anthropic.types.beta import BetaTextBlockParam
9
10
  from anthropic.types.beta.beta_input_json_delta import BetaInputJSONDelta
10
11
  from anthropic.types.beta.beta_raw_content_block_delta_event import BetaRawContentBlockDeltaEvent
11
12
  from anthropic.types.beta.beta_raw_content_block_start_event import BetaRawContentBlockStartEvent
@@ -18,21 +19,68 @@ from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
18
19
  from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
19
20
  from anthropic.types.beta.message_create_params import MessageCreateParamsStreaming
20
21
 
21
- from klaude_code import const
22
+ from klaude_code.const import (
23
+ ANTHROPIC_BETA_INTERLEAVED_THINKING,
24
+ CLAUDE_CODE_IDENTITY,
25
+ DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
26
+ DEFAULT_MAX_TOKENS,
27
+ DEFAULT_TEMPERATURE,
28
+ LLM_HTTP_TIMEOUT_CONNECT,
29
+ LLM_HTTP_TIMEOUT_READ,
30
+ LLM_HTTP_TIMEOUT_TOTAL,
31
+ )
22
32
  from klaude_code.llm.anthropic.input import convert_history_to_input, convert_system_to_input, convert_tool_schema
23
33
  from klaude_code.llm.client import LLMClientABC
24
34
  from klaude_code.llm.input_common import apply_config_defaults
25
35
  from klaude_code.llm.registry import register
26
- from klaude_code.llm.usage import MetadataTracker
27
- from klaude_code.protocol import llm_param, model
36
+ from klaude_code.llm.usage import MetadataTracker, error_stream_items
37
+ from klaude_code.protocol import llm_param, message, model
28
38
  from klaude_code.trace import DebugType, log_debug
29
39
 
30
40
 
31
- def build_payload(param: llm_param.LLMCallParameter) -> MessageCreateParamsStreaming:
32
- """Build Anthropic API request parameters."""
41
+ def _map_anthropic_stop_reason(reason: str) -> model.StopReason | None:
42
+ mapping: dict[str, model.StopReason] = {
43
+ "end_turn": "stop",
44
+ "stop_sequence": "stop",
45
+ "max_tokens": "length",
46
+ "tool_use": "tool_use",
47
+ "content_filter": "error",
48
+ "error": "error",
49
+ "cancelled": "aborted",
50
+ "canceled": "aborted",
51
+ "aborted": "aborted",
52
+ }
53
+ return mapping.get(reason)
54
+
55
+
56
+ def build_payload(
57
+ param: llm_param.LLMCallParameter,
58
+ *,
59
+ extra_betas: list[str] | None = None,
60
+ ) -> MessageCreateParamsStreaming:
61
+ """Build Anthropic API request parameters.
62
+
63
+ Args:
64
+ param: LLM call parameters.
65
+ extra_betas: Additional beta flags to prepend to the betas list.
66
+ """
33
67
  messages = convert_history_to_input(param.input, param.model)
34
68
  tools = convert_tool_schema(param.tools)
35
- system = convert_system_to_input(param.system)
69
+ system_messages = [msg for msg in param.input if isinstance(msg, message.SystemMessage)]
70
+ system = convert_system_to_input(param.system, system_messages)
71
+
72
+ # Add identity block at the beginning of the system prompt
73
+ identity_block: BetaTextBlockParam = {
74
+ "type": "text",
75
+ "text": CLAUDE_CODE_IDENTITY,
76
+ "cache_control": {"type": "ephemeral"},
77
+ }
78
+ system = [identity_block, *system]
79
+
80
+ betas = [ANTHROPIC_BETA_INTERLEAVED_THINKING]
81
+ if extra_betas:
82
+ # Prepend extra betas, avoiding duplicates
83
+ betas = [b for b in extra_betas if b not in betas] + betas
36
84
 
37
85
  payload: MessageCreateParamsStreaming = {
38
86
  "model": str(param.model),
@@ -41,18 +89,18 @@ def build_payload(param: llm_param.LLMCallParameter) -> MessageCreateParamsStrea
41
89
  "disable_parallel_tool_use": False,
42
90
  },
43
91
  "stream": True,
44
- "max_tokens": param.max_tokens or const.DEFAULT_MAX_TOKENS,
45
- "temperature": param.temperature or const.DEFAULT_TEMPERATURE,
92
+ "max_tokens": param.max_tokens or DEFAULT_MAX_TOKENS,
93
+ "temperature": param.temperature or DEFAULT_TEMPERATURE,
46
94
  "messages": messages,
47
95
  "system": system,
48
96
  "tools": tools,
49
- "betas": ["interleaved-thinking-2025-05-14", "context-1m-2025-08-07"],
97
+ "betas": betas,
50
98
  }
51
99
 
52
100
  if param.thinking and param.thinking.type == "enabled":
53
101
  payload["thinking"] = anthropic.types.ThinkingConfigEnabledParam(
54
102
  type="enabled",
55
- budget_tokens=param.thinking.budget_tokens or const.DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
103
+ budget_tokens=param.thinking.budget_tokens or DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
56
104
  )
57
105
 
58
106
  return payload
@@ -62,14 +110,14 @@ async def parse_anthropic_stream(
62
110
  stream: Any,
63
111
  param: llm_param.LLMCallParameter,
64
112
  metadata_tracker: MetadataTracker,
65
- ) -> AsyncGenerator[model.ConversationItem]:
66
- """Parse Anthropic beta messages stream and yield conversation items.
67
-
68
- This function is shared between AnthropicClient and BedrockClient.
69
- """
113
+ ) -> AsyncGenerator[message.LLMStreamItem]:
114
+ """Parse Anthropic beta messages stream and yield stream items."""
70
115
  accumulated_thinking: list[str] = []
71
116
  accumulated_content: list[str] = []
117
+ parts: list[message.Part] = []
72
118
  response_id: str | None = None
119
+ stop_reason: model.StopReason | None = None
120
+ pending_signature: str | None = None
73
121
 
74
122
  current_tool_name: str | None = None
75
123
  current_tool_call_id: str | None = None
@@ -90,28 +138,23 @@ async def parse_anthropic_stream(
90
138
  response_id = event.message.id
91
139
  cached_token = event.message.usage.cache_read_input_tokens or 0
92
140
  input_token = event.message.usage.input_tokens
93
- yield model.StartItem(response_id=response_id)
94
141
  case BetaRawContentBlockDeltaEvent() as event:
95
142
  match event.delta:
96
143
  case BetaThinkingDelta() as delta:
97
144
  if delta.thinking:
98
145
  metadata_tracker.record_token()
99
146
  accumulated_thinking.append(delta.thinking)
100
- yield model.ReasoningTextDelta(
147
+ yield message.ThinkingTextDelta(
101
148
  content=delta.thinking,
102
149
  response_id=response_id,
103
150
  )
104
151
  case BetaSignatureDelta() as delta:
105
- yield model.ReasoningEncryptedItem(
106
- encrypted_content=delta.signature,
107
- response_id=response_id,
108
- model=str(param.model),
109
- )
152
+ pending_signature = delta.signature
110
153
  case BetaTextDelta() as delta:
111
154
  if delta.text:
112
155
  metadata_tracker.record_token()
113
156
  accumulated_content.append(delta.text)
114
- yield model.AssistantMessageDelta(
157
+ yield message.AssistantTextDelta(
115
158
  content=delta.text,
116
159
  response_id=response_id,
117
160
  )
@@ -126,7 +169,7 @@ async def parse_anthropic_stream(
126
169
  match event.content_block:
127
170
  case BetaToolUseBlock() as block:
128
171
  metadata_tracker.record_token()
129
- yield model.ToolCallStartItem(
172
+ yield message.ToolCallStartItem(
130
173
  response_id=response_id,
131
174
  call_id=block.id,
132
175
  name=block.name,
@@ -137,29 +180,32 @@ async def parse_anthropic_stream(
137
180
  case _:
138
181
  pass
139
182
  case BetaRawContentBlockStopEvent():
140
- if len(accumulated_thinking) > 0:
183
+ if accumulated_thinking:
141
184
  metadata_tracker.record_token()
142
185
  full_thinking = "".join(accumulated_thinking)
143
- yield model.ReasoningTextItem(
144
- content=full_thinking,
145
- response_id=response_id,
146
- model=str(param.model),
147
- )
186
+ parts.append(message.ThinkingTextPart(text=full_thinking, model_id=str(param.model)))
187
+ if pending_signature:
188
+ parts.append(
189
+ message.ThinkingSignaturePart(
190
+ signature=pending_signature,
191
+ model_id=str(param.model),
192
+ format="anthropic",
193
+ )
194
+ )
148
195
  accumulated_thinking.clear()
149
- if len(accumulated_content) > 0:
196
+ pending_signature = None
197
+ if accumulated_content:
150
198
  metadata_tracker.record_token()
151
- yield model.AssistantMessageItem(
152
- content="".join(accumulated_content),
153
- response_id=response_id,
154
- )
199
+ parts.append(message.TextPart(text="".join(accumulated_content)))
155
200
  accumulated_content.clear()
156
201
  if current_tool_name and current_tool_call_id:
157
202
  metadata_tracker.record_token()
158
- yield model.ToolCallItem(
159
- name=current_tool_name,
160
- call_id=current_tool_call_id,
161
- arguments="".join(current_tool_inputs) if current_tool_inputs else "",
162
- response_id=response_id,
203
+ parts.append(
204
+ message.ToolCallPart(
205
+ call_id=current_tool_call_id,
206
+ tool_name=current_tool_name,
207
+ arguments_json="".join(current_tool_inputs) if current_tool_inputs else "",
208
+ )
163
209
  )
164
210
  current_tool_name = None
165
211
  current_tool_call_id = None
@@ -177,10 +223,20 @@ async def parse_anthropic_stream(
177
223
  )
178
224
  metadata_tracker.set_model_name(str(param.model))
179
225
  metadata_tracker.set_response_id(response_id)
180
- yield metadata_tracker.finalize()
226
+ raw_stop_reason = getattr(event, "stop_reason", None)
227
+ if isinstance(raw_stop_reason, str):
228
+ stop_reason = _map_anthropic_stop_reason(raw_stop_reason)
181
229
  case _:
182
230
  pass
183
231
 
232
+ metadata = metadata_tracker.finalize()
233
+ yield message.AssistantMessage(
234
+ parts=parts,
235
+ response_id=response_id,
236
+ usage=metadata,
237
+ stop_reason=stop_reason,
238
+ )
239
+
184
240
 
185
241
  @register(llm_param.LLMClientProtocol.ANTHROPIC)
186
242
  class AnthropicClient(LLMClientABC):
@@ -195,7 +251,9 @@ class AnthropicClient(LLMClientABC):
195
251
  client = anthropic.AsyncAnthropic(
196
252
  api_key=config.api_key,
197
253
  base_url=config.base_url,
198
- timeout=httpx.Timeout(300.0, connect=15.0, read=285.0),
254
+ timeout=httpx.Timeout(
255
+ LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ
256
+ ),
199
257
  )
200
258
  finally:
201
259
  if saved_auth_token is not None:
@@ -208,7 +266,7 @@ class AnthropicClient(LLMClientABC):
208
266
  return cls(config)
209
267
 
210
268
  @override
211
- async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
269
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
212
270
  param = apply_config_defaults(param, self.get_llm_config())
213
271
 
214
272
  metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
@@ -230,4 +288,6 @@ class AnthropicClient(LLMClientABC):
230
288
  async for item in parse_anthropic_stream(stream, param, metadata_tracker):
231
289
  yield item
232
290
  except (APIError, httpx.HTTPError) as e:
233
- yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
291
+ error_message = f"{e.__class__.__name__} {e!s}"
292
+ for item in error_stream_items(metadata_tracker, error=error_message):
293
+ yield item
@@ -4,10 +4,7 @@
4
4
  # pyright: reportAttributeAccessIssue=false
5
5
  # pyright: reportUnknownVariableType=false
6
6
 
7
-
8
7
  import json
9
- from base64 import b64decode
10
- from binascii import Error as BinasciiError
11
8
  from typing import Literal, cast
12
9
 
13
10
  from anthropic.types.beta.beta_base64_image_source_param import BetaBase64ImageSourceParam
@@ -17,8 +14,15 @@ from anthropic.types.beta.beta_text_block_param import BetaTextBlockParam
17
14
  from anthropic.types.beta.beta_tool_param import BetaToolParam
18
15
  from anthropic.types.beta.beta_url_image_source_param import BetaURLImageSourceParam
19
16
 
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
17
+ from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
18
+ from klaude_code.llm.image import parse_data_url
19
+ from klaude_code.llm.input_common import (
20
+ DeveloperAttachment,
21
+ attach_developer_messages,
22
+ merge_reminder_text,
23
+ split_thinking_parts,
24
+ )
25
+ from klaude_code.protocol import llm_param, message
22
26
 
23
27
  AllowedMediaType = Literal["image/png", "image/jpeg", "image/gif", "image/webp"]
24
28
  _INLINE_IMAGE_MEDIA_TYPES: tuple[AllowedMediaType, ...] = (
@@ -29,25 +33,12 @@ _INLINE_IMAGE_MEDIA_TYPES: tuple[AllowedMediaType, ...] = (
29
33
  )
30
34
 
31
35
 
32
- def _image_part_to_block(image: model.ImageURLPart) -> BetaImageBlockParam:
33
- url = image.image_url.url
36
+ def _image_part_to_block(image: message.ImageURLPart) -> BetaImageBlockParam:
37
+ url = image.url
34
38
  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]
39
+ media_type, base64_payload, _ = parse_data_url(url)
42
40
  if media_type not in _INLINE_IMAGE_MEDIA_TYPES:
43
41
  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
42
  source = cast(
52
43
  BetaBase64ImageSourceParam,
53
44
  {
@@ -62,97 +53,108 @@ def _image_part_to_block(image: model.ImageURLPart) -> BetaImageBlockParam:
62
53
  return {"type": "image", "source": source_url}
63
54
 
64
55
 
65
- def _user_group_to_message(group: UserGroup) -> BetaMessageParam:
56
+ def _user_message_to_message(
57
+ msg: message.UserMessage,
58
+ attachment: DeveloperAttachment,
59
+ ) -> BetaMessageParam:
66
60
  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:
61
+ for part in msg.parts:
62
+ if isinstance(part, message.TextPart):
63
+ blocks.append({"type": "text", "text": part.text})
64
+ elif isinstance(part, message.ImageURLPart):
65
+ blocks.append(_image_part_to_block(part))
66
+ if attachment.text:
67
+ blocks.append({"type": "text", "text": attachment.text})
68
+ for image in attachment.images:
70
69
  blocks.append(_image_part_to_block(image))
71
70
  if not blocks:
72
71
  blocks.append({"type": "text", "text": ""})
73
72
  return {"role": "user", "content": blocks}
74
73
 
75
74
 
76
- def _tool_group_to_block(group: ToolGroup) -> dict[str, object]:
77
- """Convert a single ToolGroup to a tool_result block."""
75
+ def _tool_message_to_block(
76
+ msg: message.ToolResultMessage,
77
+ attachment: DeveloperAttachment,
78
+ ) -> dict[str, object]:
79
+ """Convert a single tool result message to a tool_result block."""
78
80
  tool_content: list[BetaTextBlockParam | BetaImageBlockParam] = []
79
81
  merged_text = merge_reminder_text(
80
- group.tool_result.output or "<system-reminder>Tool ran without output or errors</system-reminder>",
81
- group.reminder_texts,
82
+ msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
83
+ attachment.text,
82
84
  )
83
85
  tool_content.append({"type": "text", "text": merged_text})
84
- for image in group.tool_result.images or []:
86
+ for image in [part for part in msg.parts if isinstance(part, message.ImageURLPart)]:
85
87
  tool_content.append(_image_part_to_block(image))
86
- for image in group.reminder_images:
88
+ for image in attachment.images:
87
89
  tool_content.append(_image_part_to_block(image))
88
90
  return {
89
91
  "type": "tool_result",
90
- "tool_use_id": group.tool_result.call_id,
91
- "is_error": group.tool_result.status == "error",
92
+ "tool_use_id": msg.call_id,
93
+ "is_error": msg.status != "success",
92
94
  "content": tool_content,
93
95
  }
94
96
 
95
97
 
96
- def _tool_groups_to_message(groups: list[ToolGroup]) -> BetaMessageParam:
97
- """Convert one or more ToolGroups to a single user message with multiple tool_result blocks."""
98
+ def _tool_blocks_to_message(blocks: list[dict[str, object]]) -> BetaMessageParam:
99
+ """Convert one or more tool_result blocks to a single user message."""
98
100
  return {
99
101
  "role": "user",
100
- "content": [_tool_group_to_block(group) for group in groups],
102
+ "content": blocks,
101
103
  }
102
104
 
103
105
 
104
- def _assistant_group_to_message(group: AssistantGroup, model_name: str | None) -> BetaMessageParam:
106
+ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str | None) -> BetaMessageParam:
105
107
  content: list[dict[str, object]] = []
106
- current_reasoning_content: str | None = None
107
- degraded_thinking_texts: list[str] = []
108
-
109
- # Process reasoning items in original order so that text and
110
- # encrypted parts are paired correctly for the given model.
111
- # For cross-model scenarios, degrade thinking to plain text.
112
- for item in group.reasoning_items:
113
- if isinstance(item, model.ReasoningTextItem):
114
- if model_name != item.model:
115
- # Cross-model: collect thinking text for degradation
116
- if item.content:
117
- degraded_thinking_texts.append(item.content)
118
- else:
119
- current_reasoning_content = item.content
120
- else:
121
- # Same model: preserve signature
122
- if model_name == item.model and item.encrypted_content and len(item.encrypted_content) > 0:
108
+ current_thinking_content: str | None = None
109
+ native_thinking_parts, degraded_thinking_texts = split_thinking_parts(msg, model_name)
110
+ native_thinking_ids = {id(part) for part in native_thinking_parts}
111
+
112
+ def _flush_thinking() -> None:
113
+ nonlocal current_thinking_content
114
+ if current_thinking_content is None:
115
+ return
116
+ content.append({"type": "thinking", "thinking": current_thinking_content})
117
+ current_thinking_content = None
118
+
119
+ for part in msg.parts:
120
+ if isinstance(part, message.ThinkingTextPart):
121
+ if id(part) not in native_thinking_ids:
122
+ continue
123
+ current_thinking_content = part.text
124
+ continue
125
+ if isinstance(part, message.ThinkingSignaturePart):
126
+ if id(part) not in native_thinking_ids:
127
+ continue
128
+ if part.signature:
123
129
  content.append(
124
130
  {
125
131
  "type": "thinking",
126
- "thinking": current_reasoning_content or "",
127
- "signature": item.encrypted_content,
132
+ "thinking": current_thinking_content or "",
133
+ "signature": part.signature,
128
134
  }
129
135
  )
130
- current_reasoning_content = None
136
+ current_thinking_content = None
137
+ continue
138
+
139
+ _flush_thinking()
140
+ if isinstance(part, message.TextPart):
141
+ content.append({"type": "text", "text": part.text})
142
+ elif isinstance(part, message.ToolCallPart):
143
+ content.append(
144
+ {
145
+ "type": "tool_use",
146
+ "id": part.call_id,
147
+ "name": part.tool_name,
148
+ "input": json.loads(part.arguments_json) if part.arguments_json else None,
149
+ }
150
+ )
151
+
152
+ _flush_thinking()
131
153
 
132
- # Moonshot.ai's Kimi does not always send reasoning signatures;
133
- # if we saw reasoning text without any matching encrypted item,
134
- # emit it as a plain thinking block.
135
- if len(current_reasoning_content or "") > 0:
136
- content.insert(0, {"type": "thinking", "thinking": current_reasoning_content})
137
-
138
- # Cross-model: degrade thinking to plain text with <thinking> tags
139
154
  if degraded_thinking_texts:
140
155
  degraded_text = "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"
141
156
  content.insert(0, {"type": "text", "text": degraded_text})
142
157
 
143
- if group.text_content:
144
- content.append({"type": "text", "text": group.text_content})
145
-
146
- for tc in group.tool_calls:
147
- content.append(
148
- {
149
- "type": "tool_use",
150
- "id": tc.call_id,
151
- "name": tc.name,
152
- "input": json.loads(tc.arguments) if tc.arguments else None,
153
- }
154
- )
155
-
156
158
  return {"role": "assistant", "content": content}
157
159
 
158
160
 
@@ -167,45 +169,51 @@ def _add_cache_control(messages: list[BetaMessageParam]) -> None:
167
169
 
168
170
 
169
171
  def convert_history_to_input(
170
- history: list[model.ConversationItem],
172
+ history: list[message.Message],
171
173
  model_name: str | None,
172
174
  ) -> list[BetaMessageParam]:
173
- """
174
- Convert a list of conversation items to a list of beta message params.
175
-
176
- Args:
177
- history: List of conversation items.
178
- model_name: Model name. Used to verify that signatures are valid for the same model
179
- """
175
+ """Convert a list of messages to beta message params."""
180
176
  messages: list[BetaMessageParam] = []
181
- pending_tool_groups: list[ToolGroup] = []
182
-
183
- def flush_tool_groups() -> None:
184
- nonlocal pending_tool_groups
185
- if pending_tool_groups:
186
- messages.append(_tool_groups_to_message(pending_tool_groups))
187
- pending_tool_groups = []
188
-
189
- for group in parse_message_groups(history):
190
- match group:
191
- case UserGroup():
192
- flush_tool_groups()
193
- messages.append(_user_group_to_message(group))
194
- case ToolGroup():
195
- pending_tool_groups.append(group)
196
- case AssistantGroup():
197
- flush_tool_groups()
198
- messages.append(_assistant_group_to_message(group, model_name))
199
-
200
- flush_tool_groups()
177
+ pending_tool_blocks: list[dict[str, object]] = []
178
+
179
+ def flush_tool_blocks() -> None:
180
+ nonlocal pending_tool_blocks
181
+ if pending_tool_blocks:
182
+ messages.append(_tool_blocks_to_message(pending_tool_blocks))
183
+ pending_tool_blocks = []
184
+
185
+ for msg, attachment in attach_developer_messages(history):
186
+ match msg:
187
+ case message.ToolResultMessage():
188
+ pending_tool_blocks.append(_tool_message_to_block(msg, attachment))
189
+ case message.UserMessage():
190
+ flush_tool_blocks()
191
+ messages.append(_user_message_to_message(msg, attachment))
192
+ case message.AssistantMessage():
193
+ flush_tool_blocks()
194
+ messages.append(_assistant_message_to_message(msg, model_name))
195
+ case message.SystemMessage():
196
+ continue
197
+ case _:
198
+ continue
199
+
200
+ flush_tool_blocks()
201
201
  _add_cache_control(messages)
202
202
  return messages
203
203
 
204
204
 
205
- def convert_system_to_input(system: str | None) -> list[BetaTextBlockParam]:
206
- if system is None:
205
+ def convert_system_to_input(
206
+ system: str | None, system_messages: list[message.SystemMessage] | None = None
207
+ ) -> list[BetaTextBlockParam]:
208
+ parts: list[str] = []
209
+ if system:
210
+ parts.append(system)
211
+ if system_messages:
212
+ for msg in system_messages:
213
+ parts.append("\n".join(part.text for part in msg.parts))
214
+ if not parts:
207
215
  return []
208
- return [{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}]
216
+ return [{"type": "text", "text": "\n".join(parts), "cache_control": {"type": "ephemeral"}}]
209
217
 
210
218
 
211
219
  def convert_tool_schema(
@@ -8,12 +8,13 @@ import anthropic
8
8
  import httpx
9
9
  from anthropic import APIError
10
10
 
11
+ from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
11
12
  from klaude_code.llm.anthropic.client import build_payload, parse_anthropic_stream
12
13
  from klaude_code.llm.client import LLMClientABC
13
14
  from klaude_code.llm.input_common import apply_config_defaults
14
15
  from klaude_code.llm.registry import register
15
- from klaude_code.llm.usage import MetadataTracker
16
- from klaude_code.protocol import llm_param, model
16
+ from klaude_code.llm.usage import MetadataTracker, error_stream_items
17
+ from klaude_code.protocol import llm_param, message
17
18
  from klaude_code.trace import DebugType, log_debug
18
19
 
19
20
 
@@ -29,7 +30,7 @@ class BedrockClient(LLMClientABC):
29
30
  aws_region=config.aws_region,
30
31
  aws_session_token=config.aws_session_token,
31
32
  aws_profile=config.aws_profile,
32
- timeout=httpx.Timeout(300.0, connect=15.0, read=285.0),
33
+ timeout=httpx.Timeout(LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ),
33
34
  )
34
35
 
35
36
  @classmethod
@@ -38,7 +39,7 @@ class BedrockClient(LLMClientABC):
38
39
  return cls(config)
39
40
 
40
41
  @override
41
- async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
42
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
42
43
  param = apply_config_defaults(param, self.get_llm_config())
43
44
 
44
45
  metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
@@ -57,4 +58,6 @@ class BedrockClient(LLMClientABC):
57
58
  async for item in parse_anthropic_stream(stream, param, metadata_tracker):
58
59
  yield item
59
60
  except (APIError, httpx.HTTPError) as e:
60
- yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
61
+ error_message = f"{e.__class__.__name__} {e!s}"
62
+ for item in error_stream_items(metadata_tracker, error=error_message):
63
+ yield item
@@ -0,0 +1,3 @@
1
+ from .client import ClaudeClient
2
+
3
+ __all__ = ["ClaudeClient"]