klaude-code 2.5.2__py3-none-any.whl → 2.6.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 (61) hide show
  1. klaude_code/auth/__init__.py +10 -0
  2. klaude_code/auth/env.py +77 -0
  3. klaude_code/cli/auth_cmd.py +89 -21
  4. klaude_code/cli/config_cmd.py +5 -5
  5. klaude_code/cli/cost_cmd.py +167 -68
  6. klaude_code/cli/main.py +51 -27
  7. klaude_code/cli/self_update.py +7 -7
  8. klaude_code/config/assets/builtin_config.yaml +45 -24
  9. klaude_code/config/builtin_config.py +23 -9
  10. klaude_code/config/config.py +19 -9
  11. klaude_code/config/model_matcher.py +1 -1
  12. klaude_code/const.py +2 -1
  13. klaude_code/core/tool/file/edit_tool.py +1 -1
  14. klaude_code/core/tool/file/read_tool.py +2 -2
  15. klaude_code/core/tool/file/write_tool.py +1 -1
  16. klaude_code/core/turn.py +21 -4
  17. klaude_code/llm/anthropic/client.py +75 -50
  18. klaude_code/llm/anthropic/input.py +20 -9
  19. klaude_code/llm/google/client.py +235 -148
  20. klaude_code/llm/google/input.py +44 -36
  21. klaude_code/llm/openai_compatible/stream.py +114 -100
  22. klaude_code/llm/openrouter/client.py +1 -0
  23. klaude_code/llm/openrouter/reasoning.py +4 -29
  24. klaude_code/llm/partial_message.py +2 -32
  25. klaude_code/llm/responses/client.py +99 -81
  26. klaude_code/llm/responses/input.py +11 -25
  27. klaude_code/llm/stream_parts.py +94 -0
  28. klaude_code/log.py +57 -0
  29. klaude_code/protocol/events.py +214 -0
  30. klaude_code/protocol/sub_agent/image_gen.py +0 -4
  31. klaude_code/session/session.py +51 -18
  32. klaude_code/tui/command/fork_session_cmd.py +14 -23
  33. klaude_code/tui/command/model_picker.py +2 -17
  34. klaude_code/tui/command/resume_cmd.py +2 -18
  35. klaude_code/tui/command/sub_agent_model_cmd.py +5 -19
  36. klaude_code/tui/command/thinking_cmd.py +2 -14
  37. klaude_code/tui/commands.py +0 -5
  38. klaude_code/tui/components/common.py +1 -1
  39. klaude_code/tui/components/metadata.py +21 -21
  40. klaude_code/tui/components/rich/quote.py +36 -8
  41. klaude_code/tui/components/rich/theme.py +2 -0
  42. klaude_code/tui/components/sub_agent.py +6 -0
  43. klaude_code/tui/display.py +11 -1
  44. klaude_code/tui/input/completers.py +11 -7
  45. klaude_code/tui/input/prompt_toolkit.py +3 -1
  46. klaude_code/tui/machine.py +108 -56
  47. klaude_code/tui/renderer.py +4 -65
  48. klaude_code/tui/terminal/selector.py +174 -31
  49. {klaude_code-2.5.2.dist-info → klaude_code-2.6.0.dist-info}/METADATA +23 -31
  50. {klaude_code-2.5.2.dist-info → klaude_code-2.6.0.dist-info}/RECORD +52 -58
  51. klaude_code/cli/session_cmd.py +0 -96
  52. klaude_code/protocol/events/__init__.py +0 -63
  53. klaude_code/protocol/events/base.py +0 -18
  54. klaude_code/protocol/events/chat.py +0 -30
  55. klaude_code/protocol/events/lifecycle.py +0 -23
  56. klaude_code/protocol/events/metadata.py +0 -16
  57. klaude_code/protocol/events/streaming.py +0 -43
  58. klaude_code/protocol/events/system.py +0 -56
  59. klaude_code/protocol/events/tools.py +0 -27
  60. {klaude_code-2.5.2.dist-info → klaude_code-2.6.0.dist-info}/WHEEL +0 -0
  61. {klaude_code-2.5.2.dist-info → klaude_code-2.6.0.dist-info}/entry_points.txt +0 -0
@@ -33,8 +33,13 @@ from klaude_code.const import (
33
33
  from klaude_code.llm.anthropic.input import convert_history_to_input, convert_system_to_input, convert_tool_schema
34
34
  from klaude_code.llm.client import LLMClientABC, LLMStreamABC
35
35
  from klaude_code.llm.input_common import apply_config_defaults
36
- from klaude_code.llm.partial_message import degrade_thinking_to_text
37
36
  from klaude_code.llm.registry import register
37
+ from klaude_code.llm.stream_parts import (
38
+ append_text_part,
39
+ append_thinking_text_part,
40
+ build_partial_message,
41
+ build_partial_parts,
42
+ )
38
43
  from klaude_code.llm.usage import MetadataTracker, error_llm_stream
39
44
  from klaude_code.log import DebugType, log_debug
40
45
  from klaude_code.protocol import llm_param, message, model
@@ -64,11 +69,10 @@ class AnthropicStreamStateManager:
64
69
 
65
70
  def __init__(self, model_id: str) -> None:
66
71
  self.model_id = model_id
67
- self.accumulated_thinking: list[str] = []
68
- self.accumulated_content: list[str] = []
69
- self.parts: list[message.Part] = []
72
+ self.assistant_parts: list[message.Part] = []
70
73
  self.response_id: str | None = None
71
- self.pending_signature: str | None = None
74
+ self._pending_signature: str | None = None
75
+ self._pending_signature_thinking_index: int | None = None
72
76
  self.stop_reason: model.StopReason | None = None
73
77
 
74
78
  # Tool call state
@@ -80,34 +84,59 @@ class AnthropicStreamStateManager:
80
84
  self.input_token: int = 0
81
85
  self.cached_token: int = 0
82
86
 
83
- def flush_thinking(self) -> None:
84
- """Flush accumulated thinking content into parts."""
85
- if not self.accumulated_thinking:
87
+ def append_thinking_text(self, text: str) -> None:
88
+ """Append thinking text, merging with the previous ThinkingTextPart when possible."""
89
+ index = append_thinking_text_part(self.assistant_parts, text, model_id=self.model_id)
90
+ if index is not None:
91
+ self._pending_signature_thinking_index = index
92
+
93
+ def append_text(self, text: str) -> None:
94
+ """Append assistant text, merging with the previous TextPart when possible."""
95
+ append_text_part(self.assistant_parts, text)
96
+
97
+ def set_pending_signature(self, signature: str) -> None:
98
+ if signature:
99
+ self._pending_signature = signature
100
+
101
+ def flush_pending_signature(self) -> None:
102
+ """Attach any pending signature to the most recent thinking segment.
103
+
104
+ Anthropic's signature is semantically tied to its thinking content. The
105
+ signature delta may arrive slightly after thinking text, so we insert the
106
+ signature part adjacent to the thinking part it signs.
107
+ """
108
+
109
+ if not self._pending_signature:
110
+ return
111
+ if self._pending_signature_thinking_index is None:
112
+ # No thinking part seen for this signature; drop it.
113
+ self._pending_signature = None
86
114
  return
87
- full_thinking = "".join(self.accumulated_thinking)
88
- self.parts.append(message.ThinkingTextPart(text=full_thinking, model_id=self.model_id))
89
- if self.pending_signature:
90
- self.parts.append(
91
- message.ThinkingSignaturePart(
92
- signature=self.pending_signature,
93
- model_id=self.model_id,
94
- format="anthropic",
95
- )
96
- )
97
- self.accumulated_thinking.clear()
98
- self.pending_signature = None
99
115
 
100
- def flush_content(self) -> None:
101
- """Flush accumulated content into parts."""
102
- if not self.accumulated_content:
116
+ insert_at = self._pending_signature_thinking_index + 1
117
+ # Avoid inserting duplicates if flush is called multiple times.
118
+ if insert_at < len(self.assistant_parts) and isinstance(
119
+ self.assistant_parts[insert_at], message.ThinkingSignaturePart
120
+ ):
121
+ self._pending_signature = None
103
122
  return
104
- self.parts.append(message.TextPart(text="".join(self.accumulated_content)))
105
- self.accumulated_content.clear()
123
+
124
+ self.assistant_parts.insert(
125
+ insert_at,
126
+ message.ThinkingSignaturePart(
127
+ signature=self._pending_signature,
128
+ model_id=self.model_id,
129
+ format="anthropic",
130
+ ),
131
+ )
132
+
133
+ self._pending_signature = None
134
+ self._pending_signature_thinking_index = None
106
135
 
107
136
  def flush_tool_call(self) -> None:
108
137
  """Flush current tool call into parts."""
109
138
  if self.current_tool_name and self.current_tool_call_id:
110
- self.parts.append(
139
+ self.assistant_parts.append(
111
140
  message.ToolCallPart(
112
141
  call_id=self.current_tool_call_id,
113
142
  tool_name=self.current_tool_name,
@@ -119,11 +148,17 @@ class AnthropicStreamStateManager:
119
148
  self.current_tool_inputs = None
120
149
 
121
150
  def flush_all(self) -> list[message.Part]:
122
- """Flush all accumulated content in order and return parts."""
123
- self.flush_thinking()
124
- self.flush_content()
151
+ """Flush all pending content in order and return parts."""
152
+ self.flush_pending_signature()
125
153
  self.flush_tool_call()
126
- return list(self.parts)
154
+ return list(self.assistant_parts)
155
+
156
+ def get_partial_parts(self) -> list[message.Part]:
157
+ """Get accumulated parts excluding tool calls, with thinking degraded.
158
+
159
+ Filters out ToolCallPart and applies degrade_thinking_to_text.
160
+ """
161
+ return build_partial_parts(self.assistant_parts)
127
162
 
128
163
  def get_partial_message(self) -> message.AssistantMessage | None:
129
164
  """Build a partial AssistantMessage from accumulated state.
@@ -131,16 +166,7 @@ class AnthropicStreamStateManager:
131
166
  Flushes all accumulated content and returns the message with
132
167
  stop_reason="aborted". Returns None if no content has been accumulated.
133
168
  """
134
- self.flush_thinking()
135
- self.flush_content()
136
- parts = degrade_thinking_to_text(list(self.parts))
137
- if not parts:
138
- return None
139
- return message.AssistantMessage(
140
- parts=parts,
141
- response_id=self.response_id,
142
- stop_reason="aborted",
143
- )
169
+ return build_partial_message(self.assistant_parts, response_id=self.response_id)
144
170
 
145
171
 
146
172
  def build_payload(
@@ -226,17 +252,18 @@ async def parse_anthropic_stream(
226
252
  case BetaThinkingDelta() as delta:
227
253
  if delta.thinking:
228
254
  metadata_tracker.record_token()
229
- state.accumulated_thinking.append(delta.thinking)
255
+ state.append_thinking_text(delta.thinking)
230
256
  yield message.ThinkingTextDelta(
231
257
  content=delta.thinking,
232
258
  response_id=state.response_id,
233
259
  )
234
260
  case BetaSignatureDelta() as delta:
235
- state.pending_signature = delta.signature
261
+ state.set_pending_signature(delta.signature)
236
262
  case BetaTextDelta() as delta:
237
263
  if delta.text:
238
264
  metadata_tracker.record_token()
239
- state.accumulated_content.append(delta.text)
265
+ state.flush_pending_signature()
266
+ state.append_text(delta.text)
240
267
  yield message.AssistantTextDelta(
241
268
  content=delta.text,
242
269
  response_id=state.response_id,
@@ -251,6 +278,7 @@ async def parse_anthropic_stream(
251
278
  match event.content_block:
252
279
  case BetaToolUseBlock() as block:
253
280
  metadata_tracker.record_token()
281
+ state.flush_pending_signature()
254
282
  yield message.ToolCallStartDelta(
255
283
  response_id=state.response_id,
256
284
  call_id=block.id,
@@ -262,12 +290,7 @@ async def parse_anthropic_stream(
262
290
  case _:
263
291
  pass
264
292
  case BetaRawContentBlockStopEvent():
265
- if state.accumulated_thinking:
266
- metadata_tracker.record_token()
267
- state.flush_thinking()
268
- if state.accumulated_content:
269
- metadata_tracker.record_token()
270
- state.flush_content()
293
+ state.flush_pending_signature()
271
294
  if state.current_tool_name and state.current_tool_call_id:
272
295
  metadata_tracker.record_token()
273
296
  state.flush_tool_call()
@@ -337,8 +360,10 @@ class AnthropicLLMStream(LLMStreamABC):
337
360
  self._metadata_tracker.set_model_name(str(self._param.model_id))
338
361
  self._metadata_tracker.set_response_id(self._state.response_id)
339
362
  metadata = self._metadata_tracker.finalize()
363
+ # Use accumulated parts for potential prefill on retry
364
+ self._state.flush_all()
340
365
  yield message.AssistantMessage(
341
- parts=[],
366
+ parts=self._state.get_partial_parts(),
342
367
  response_id=self._state.response_id,
343
368
  usage=metadata,
344
369
  stop_reason="error",
@@ -109,19 +109,34 @@ def _tool_blocks_to_message(blocks: list[BetaToolResultBlockParam]) -> BetaMessa
109
109
  def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str | None) -> BetaMessageParam:
110
110
  content: list[BetaContentBlockParam] = []
111
111
  current_thinking_content: str | None = None
112
- native_thinking_parts, degraded_thinking_texts = split_thinking_parts(msg, model_name)
112
+ native_thinking_parts, _ = split_thinking_parts(msg, model_name)
113
113
  native_thinking_ids = {id(part) for part in native_thinking_parts}
114
114
 
115
- def _flush_thinking() -> None:
115
+ def _degraded_thinking_block(text: str) -> BetaTextBlockParam | None:
116
+ stripped = text.strip()
117
+ if not stripped:
118
+ return None
119
+ return cast(
120
+ BetaTextBlockParam,
121
+ {
122
+ "type": "text",
123
+ "text": f"<thinking>\n{stripped}\n</thinking>",
124
+ },
125
+ )
126
+
127
+ def _flush_thinking_as_text_block() -> None:
116
128
  nonlocal current_thinking_content
117
129
  if current_thinking_content is None:
118
130
  return
119
- degraded_thinking_texts.append(current_thinking_content)
131
+ if block := _degraded_thinking_block(current_thinking_content):
132
+ content.append(block)
120
133
  current_thinking_content = None
121
134
 
122
135
  for part in msg.parts:
123
136
  if isinstance(part, message.ThinkingTextPart):
124
137
  if id(part) not in native_thinking_ids:
138
+ if block := _degraded_thinking_block(part.text):
139
+ content.append(block)
125
140
  continue
126
141
  current_thinking_content = part.text
127
142
  continue
@@ -142,7 +157,7 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
142
157
  current_thinking_content = None
143
158
  continue
144
159
 
145
- _flush_thinking()
160
+ _flush_thinking_as_text_block()
146
161
  if isinstance(part, message.TextPart):
147
162
  content.append(cast(BetaTextBlockParam, {"type": "text", "text": part.text}))
148
163
  elif isinstance(part, message.ToolCallPart):
@@ -166,11 +181,7 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
166
181
  )
167
182
  )
168
183
 
169
- _flush_thinking()
170
-
171
- if degraded_thinking_texts:
172
- degraded_text = "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"
173
- content.insert(0, cast(BetaTextBlockParam, {"type": "text", "text": degraded_text}))
184
+ _flush_thinking_as_text_block()
174
185
 
175
186
  return {"role": "assistant", "content": content}
176
187