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
klaude_code/cli/main.py CHANGED
@@ -7,19 +7,38 @@ from klaude_code.cli.auth_cmd import register_auth_commands
7
7
  from klaude_code.cli.config_cmd import register_config_commands
8
8
  from klaude_code.cli.cost_cmd import register_cost_commands
9
9
  from klaude_code.cli.debug import DEBUG_FILTER_HELP, prepare_debug_logging
10
- from klaude_code.cli.self_update import register_self_update_commands, version_option_callback
11
- from klaude_code.cli.session_cmd import register_session_commands
10
+ from klaude_code.cli.self_update import register_self_upgrade_commands, version_option_callback
12
11
  from klaude_code.session import Session
13
12
  from klaude_code.tui.command.resume_cmd import select_session_sync
14
13
  from klaude_code.ui.terminal.title import update_terminal_title
15
14
 
16
- ENV_HELP = """\
17
- Environment Variables:
18
15
 
19
- KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)
20
-
21
- KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)
22
- """
16
+ def _build_env_help() -> str:
17
+ from klaude_code.config.builtin_config import SUPPORTED_API_KEYS
18
+
19
+ lines = [
20
+ "Environment Variables:",
21
+ "",
22
+ "Provider API keys (built-in config):",
23
+ ]
24
+ # Calculate max env_var length for alignment
25
+ max_len = max(len(k.env_var) for k in SUPPORTED_API_KEYS)
26
+ for k in SUPPORTED_API_KEYS:
27
+ lines.append(f" {k.env_var:<{max_len}} {k.description}")
28
+ lines.extend(
29
+ [
30
+ "",
31
+ "Tool limits (Read):",
32
+ " KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)",
33
+ " KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)",
34
+ " KLAUDE_READ_MAX_IMAGE_BYTES Max image bytes to read (default: 4MB)",
35
+ " KLAUDE_IMAGE_OUTPUT_MAX_BYTES Max decoded image bytes (default: 64MB)",
36
+ ]
37
+ )
38
+ return "\n\n".join(lines)
39
+
40
+
41
+ ENV_HELP = _build_env_help()
23
42
 
24
43
  app = typer.Typer(
25
44
  add_completion=False,
@@ -27,55 +46,51 @@ app = typer.Typer(
27
46
  no_args_is_help=False,
28
47
  rich_markup_mode="rich",
29
48
  epilog=ENV_HELP,
49
+ context_settings={"help_option_names": ["-h", "--help"]},
30
50
  )
31
51
 
32
52
  # Register subcommands from modules
33
- register_session_commands(app)
34
53
  register_auth_commands(app)
35
54
  register_config_commands(app)
36
55
  register_cost_commands(app)
56
+ register_self_upgrade_commands(app)
57
+
37
58
 
38
- register_self_update_commands(app)
59
+ @app.command("help", hidden=True)
60
+ def help_command(ctx: typer.Context) -> None:
61
+ """Show help message."""
62
+ print(ctx.parent.get_help() if ctx.parent else ctx.get_help())
39
63
 
40
64
 
41
65
  @app.callback(invoke_without_command=True)
42
66
  def main_callback(
43
67
  ctx: typer.Context,
44
- version: bool = typer.Option(
45
- False,
46
- "--version",
47
- "-V",
48
- "-v",
49
- help="Show version and exit",
50
- callback=version_option_callback,
51
- is_eager=True,
52
- ),
53
68
  model: str | None = typer.Option(
54
69
  None,
55
70
  "--model",
56
71
  "-m",
57
- help="Override model config name (uses main model by default)",
72
+ help="Select model by name",
58
73
  rich_help_panel="LLM",
59
74
  ),
60
- continue_: bool = typer.Option(False, "--continue", "-c", help="Continue from latest session"),
61
- resume: bool = typer.Option(False, "--resume", "-r", help="Select a session to resume for this project"),
75
+ continue_: bool = typer.Option(False, "--continue", "-c", help="Resume latest session"),
76
+ resume: bool = typer.Option(False, "--resume", "-r", help="Pick a session to resume"),
62
77
  resume_by_id: str | None = typer.Option(
63
78
  None,
64
79
  "--resume-by-id",
65
- help="Resume a session by its ID (must exist)",
80
+ help="Resume session by ID",
66
81
  ),
67
82
  select_model: bool = typer.Option(
68
83
  False,
69
84
  "--select-model",
70
85
  "-s",
71
- help="Interactively choose a model at startup",
86
+ help="Choose model interactively",
72
87
  rich_help_panel="LLM",
73
88
  ),
74
89
  debug: bool = typer.Option(
75
90
  False,
76
91
  "--debug",
77
92
  "-d",
78
- help="Enable debug mode",
93
+ help="Enable debug logging",
79
94
  rich_help_panel="Debug",
80
95
  ),
81
96
  debug_filter: str | None = typer.Option(
@@ -87,14 +102,23 @@ def main_callback(
87
102
  vanilla: bool = typer.Option(
88
103
  False,
89
104
  "--vanilla",
90
- help="Vanilla mode exposes the model's raw API behavior: it provides only minimal tools (Bash, Read, Write & Edit) and omits system prompts and reminders.",
105
+ help="Minimal mode: basic tools only, no system prompts",
91
106
  ),
92
107
  banana: bool = typer.Option(
93
108
  False,
94
109
  "--banana",
95
- help="Image generation mode with Nano Banana",
110
+ help="Image generation mode",
96
111
  rich_help_panel="LLM",
97
112
  ),
113
+ version: bool = typer.Option(
114
+ False,
115
+ "--version",
116
+ "-V",
117
+ "-v",
118
+ help="Show version and exit",
119
+ callback=version_option_callback,
120
+ is_eager=True,
121
+ ),
98
122
  ) -> None:
99
123
  # Only run interactive mode when no subcommand is invoked
100
124
  if ctx.invoked_subcommand is None:
@@ -35,14 +35,14 @@ def version_command() -> None:
35
35
  _print_version()
36
36
 
37
37
 
38
- def update_command(
38
+ def upgrade_command(
39
39
  check: bool = typer.Option(
40
40
  False,
41
41
  "--check",
42
- help="Check for updates and exit without upgrading",
42
+ help="Check only, don't upgrade",
43
43
  ),
44
44
  ) -> None:
45
- """Upgrade klaude-code when installed via `uv tool`."""
45
+ """Upgrade to latest version"""
46
46
 
47
47
  info = check_for_updates_blocking()
48
48
 
@@ -79,9 +79,9 @@ def update_command(
79
79
  log("Update complete. Please re-run `klaude` to use the new version.")
80
80
 
81
81
 
82
- def register_self_update_commands(app: typer.Typer) -> None:
82
+ def register_self_upgrade_commands(app: typer.Typer) -> None:
83
83
  """Register self-update and version subcommands to the given Typer app."""
84
84
 
85
- app.command("update")(update_command)
86
- app.command("upgrade", help="Alias for `klaude update`.")(update_command)
87
- app.command("version", help="Alias for `klaude --version`.")(version_command)
85
+ app.command("upgrade")(upgrade_command)
86
+ app.command("update", hidden=True)(upgrade_command)
87
+ app.command("version", hidden=True)(version_command)
@@ -1,4 +1,3 @@
1
- ---
2
1
  # Built-in provider and model configurations
3
2
  # Users can start using klaude by simply setting environment variables
4
3
  # (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) without manual configuration.
@@ -25,32 +24,58 @@ provider_list:
25
24
  protocol: responses
26
25
  api_key: ${OPENAI_API_KEY}
27
26
  model_list:
28
- - model_name: gpt-5.2
27
+ - model_name: gpt-5.2-high
29
28
  model_id: gpt-5.2
30
29
  max_tokens: 128000
31
30
  context_limit: 400000
32
31
  verbosity: high
33
32
  thinking:
34
33
  reasoning_effort: high
34
+ reasoning_summary: detailed
35
+ cost: {input: 1.75, output: 14, cache_read: 0.17}
36
+ - model_name: gpt-5.2-medium
37
+ model_id: gpt-5.2
38
+ context_limit: 400000
39
+ verbosity: high
40
+ thinking:
41
+ reasoning_effort: medium
42
+ reasoning_summary: concise
43
+ cost: {input: 1.75, output: 14, cache_read: 0.17}
44
+ - model_name: gpt-5.2-low
45
+ model_id: gpt-5.2
46
+ context_limit: 400000
47
+ verbosity: low
48
+ thinking:
49
+ reasoning_effort: low
50
+ reasoning_summary: concise
51
+ cost: {input: 1.75, output: 14, cache_read: 0.17}
52
+ - model_name: gpt-5.2-fast
53
+ model_id: gpt-5.2
54
+ context_limit: 400000
55
+ verbosity: low
56
+ thinking:
57
+ reasoning_effort: none
35
58
  cost: {input: 1.75, output: 14, cache_read: 0.17}
36
- - provider_name: openrouter
37
- protocol: openrouter
38
- api_key: ${OPENROUTER_API_KEY}
39
- model_list:
40
59
  - model_name: gpt-5.1-codex-max
41
- model_id: openai/gpt-5.1-codex-max
60
+ model_id: gpt-5.1-codex-max
42
61
  max_tokens: 128000
43
62
  context_limit: 400000
44
63
  thinking:
45
64
  reasoning_effort: medium
65
+ reasoning_summary: detailed
46
66
  cost: {input: 1.25, output: 10, cache_read: 0.13}
47
- - model_name: gpt-5.2
67
+ - provider_name: openrouter
68
+ protocol: openrouter
69
+ api_key: ${OPENROUTER_API_KEY}
70
+ model_list:
71
+ - model_name: gpt-5.2-high
48
72
  model_id: openai/gpt-5.2
49
73
  max_tokens: 128000
50
74
  context_limit: 400000
51
75
  verbosity: high
52
76
  thinking:
53
77
  reasoning_effort: high
78
+ reasoning_summary: detailed
54
79
  cost: {input: 1.75, output: 14, cache_read: 0.17}
55
80
  - model_name: gpt-5.2-medium
56
81
  model_id: openai/gpt-5.2
@@ -59,22 +84,7 @@ provider_list:
59
84
  verbosity: high
60
85
  thinking:
61
86
  reasoning_effort: medium
62
- cost: {input: 1.75, output: 14, cache_read: 0.17}
63
- - model_name: gpt-5.2-low
64
- model_id: openai/gpt-5.2
65
- max_tokens: 128000
66
- context_limit: 400000
67
- verbosity: low
68
- thinking:
69
- reasoning_effort: low
70
- cost: {input: 1.75, output: 14, cache_read: 0.17}
71
- - model_name: gpt-5.2-fast
72
- model_id: openai/gpt-5.2
73
- max_tokens: 128000
74
- context_limit: 400000
75
- verbosity: low
76
- thinking:
77
- reasoning_effort: none
87
+ reasoning_summary: concise
78
88
  cost: {input: 1.75, output: 14, cache_read: 0.17}
79
89
  - model_name: kimi
80
90
  model_id: moonshotai/kimi-k2-thinking
@@ -165,10 +175,14 @@ provider_list:
165
175
  - model_name: gemini-pro
166
176
  model_id: gemini-3-pro-preview
167
177
  context_limit: 1048576
178
+ thinking:
179
+ reasoning_effort: high
168
180
  cost: {input: 2, output: 12, cache_read: 0.2}
169
181
  - model_name: gemini-flash
170
182
  model_id: gemini-3-flash-preview
171
183
  context_limit: 1048576
184
+ thinking:
185
+ reasoning_effort: medium
172
186
  cost: {input: 0.5, output: 3, cache_read: 0.05}
173
187
  - model_name: nano-banana-pro
174
188
  model_id: gemini-3-pro-image-preview
@@ -177,6 +191,13 @@ provider_list:
177
191
  - image
178
192
  - text
179
193
  cost: {input: 2, output: 12, cache_read: 0.2, image: 120}
194
+ - model_name: nano-banana
195
+ model_id: gemini-2.5-flash-image
196
+ context_limit: 33000
197
+ modalities:
198
+ - image
199
+ - text
200
+ cost: {input: 0.3, output: 2.5, cache_read: 0.03, image: 30}
180
201
  - provider_name: bedrock
181
202
  protocol: bedrock
182
203
  aws_access_key: ${AWS_ACCESS_KEY_ID}
@@ -5,6 +5,7 @@ environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) without
5
5
  manually configuring providers.
6
6
  """
7
7
 
8
+ from dataclasses import dataclass
8
9
  from functools import lru_cache
9
10
  from importlib import resources
10
11
  from typing import TYPE_CHECKING, Any
@@ -14,15 +15,28 @@ import yaml
14
15
  if TYPE_CHECKING:
15
16
  from klaude_code.config.config import ProviderConfig
16
17
 
17
- # All supported API key environment variables
18
- SUPPORTED_API_KEY_ENVS = [
19
- "ANTHROPIC_API_KEY",
20
- "GOOGLE_API_KEY",
21
- "OPENAI_API_KEY",
22
- "OPENROUTER_API_KEY",
23
- "DEEPSEEK_API_KEY",
24
- "MOONSHOT_API_KEY",
25
- ]
18
+
19
+ @dataclass(frozen=True)
20
+ class ApiKeyInfo:
21
+ """Information about a supported API key."""
22
+
23
+ env_var: str
24
+ name: str
25
+ description: str
26
+
27
+
28
+ # All supported API keys with their metadata
29
+ SUPPORTED_API_KEYS: tuple[ApiKeyInfo, ...] = (
30
+ ApiKeyInfo("ANTHROPIC_API_KEY", "Anthropic", "Anthropic API key"),
31
+ ApiKeyInfo("OPENAI_API_KEY", "OpenAI", "OpenAI API key"),
32
+ ApiKeyInfo("OPENROUTER_API_KEY", "OpenRouter", "OpenRouter API key"),
33
+ ApiKeyInfo("GOOGLE_API_KEY", "Google Gemini", "Google API key (Gemini)"),
34
+ ApiKeyInfo("DEEPSEEK_API_KEY", "DeepSeek", "DeepSeek API key"),
35
+ ApiKeyInfo("MOONSHOT_API_KEY", "Moonshot Kimi", "Moonshot API key (Kimi)"),
36
+ )
37
+
38
+ # For backwards compatibility
39
+ SUPPORTED_API_KEY_ENVS = [k.env_var for k in SUPPORTED_API_KEYS]
26
40
 
27
41
 
28
42
  @lru_cache(maxsize=1)
@@ -8,8 +8,9 @@ from typing import Any, cast
8
8
  import yaml
9
9
  from pydantic import BaseModel, Field, ValidationError, model_validator
10
10
 
11
+ from klaude_code.auth.env import get_auth_env
11
12
  from klaude_code.config.builtin_config import (
12
- SUPPORTED_API_KEY_ENVS,
13
+ SUPPORTED_API_KEYS,
13
14
  get_builtin_provider_configs,
14
15
  get_builtin_sub_agent_models,
15
16
  )
@@ -26,7 +27,8 @@ def parse_env_var_syntax(value: str | None) -> tuple[str | None, str | None]:
26
27
 
27
28
  Returns:
28
29
  A tuple of (env_var_name, resolved_value).
29
- - If value uses ${ENV_VAR} syntax: (env_var_name, os.environ.get(env_var_name))
30
+ - If value uses ${ENV_VAR} syntax: (env_var_name, resolved_value)
31
+ Priority: os.environ > klaude-auth.json env section
30
32
  - If value is a plain string: (None, value)
31
33
  - If value is None: (None, None)
32
34
  """
@@ -36,7 +38,9 @@ def parse_env_var_syntax(value: str | None) -> tuple[str | None, str | None]:
36
38
  match = _ENV_VAR_PATTERN.match(value)
37
39
  if match:
38
40
  env_var_name = match.group(1)
39
- return env_var_name, os.environ.get(env_var_name)
41
+ # Priority: real env var > auth.json env section
42
+ resolved = os.environ.get(env_var_name) or get_auth_env(env_var_name)
43
+ return env_var_name, resolved
40
44
 
41
45
  return None, value
42
46
 
@@ -613,17 +617,23 @@ def load_config() -> Config:
613
617
 
614
618
  def print_no_available_models_hint() -> None:
615
619
  """Print helpful message when no models are available due to missing API keys."""
616
- log("No available models. Please set one of the following environment variables:", style="yellow")
620
+ log("No available models. Configure an API key using one of these methods:", style="yellow")
617
621
  log("")
618
- for env_var in SUPPORTED_API_KEY_ENVS:
619
- current_value = os.environ.get(env_var)
622
+ log("Option 1: Use klaude auth login", style="bold")
623
+ # Use first word of name for brevity
624
+ names = [k.name.split()[0].lower() for k in SUPPORTED_API_KEYS]
625
+ log(f" klaude auth login <provider> (providers: {', '.join(names)})", style="dim")
626
+ log("")
627
+ log("Option 2: Set environment variables", style="bold")
628
+ max_len = max(len(k.env_var) for k in SUPPORTED_API_KEYS)
629
+ for key_info in SUPPORTED_API_KEYS:
630
+ current_value = os.environ.get(key_info.env_var) or get_auth_env(key_info.env_var)
620
631
  if current_value:
621
- log(f" {env_var} = (set)", style="green")
632
+ log(f" {key_info.env_var:<{max_len}} (set)", style="green")
622
633
  else:
623
- log(f" export {env_var}=<your-api-key>", style="dim")
634
+ log(f" {key_info.env_var:<{max_len}} {key_info.description}", style="dim")
624
635
  log("")
625
636
  log(f"Or add custom providers in: {config_path}", style="dim")
626
- log(f"See example config: {example_config_path}", style="dim")
627
637
 
628
638
 
629
639
  # Expose cache control for tests and callers that need to invalidate the cache.
@@ -102,6 +102,7 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
102
102
  )
103
103
 
104
104
  # Normalized matching (e.g. gpt52 == gpt-5.2, gpt52 in gpt-5.2-2025-...)
105
+ # Only match selector/model_name exactly; model_id is checked via substring match below
105
106
  preferred_norm = _normalize_model_key(preferred)
106
107
  normalized_matches: list[ModelEntry] = []
107
108
  if preferred_norm:
@@ -110,7 +111,6 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
110
111
  for m in models
111
112
  if preferred_norm == _normalize_model_key(m.selector)
112
113
  or preferred_norm == _normalize_model_key(m.model_name)
113
- or preferred_norm == _normalize_model_key(m.model_id or "")
114
114
  ]
115
115
  if len(normalized_matches) == 1:
116
116
  return ModelMatchResult(
klaude_code/const.py CHANGED
@@ -27,6 +27,7 @@ def _get_int_env(name: str, default: int) -> int:
27
27
  # =============================================================================
28
28
 
29
29
  MAX_FAILED_TURN_RETRIES = 10 # Maximum retry attempts for failed turns
30
+ RETRY_PRESERVE_PARTIAL_MESSAGE = True # Preserve partial message on stream error for retry prefill
30
31
  LLM_HTTP_TIMEOUT_TOTAL = 300.0 # HTTP timeout for LLM API requests (seconds)
31
32
  LLM_HTTP_TIMEOUT_CONNECT = 15.0 # HTTP connect timeout (seconds)
32
33
  LLM_HTTP_TIMEOUT_READ = 285.0 # HTTP read timeout (seconds)
@@ -157,7 +158,7 @@ MARKDOWN_RIGHT_MARGIN = 2 # Right margin (columns) for markdown rendering
157
158
  STATUS_HINT_TEXT = " (esc to interrupt)" # Status hint text shown after spinner
158
159
 
159
160
  # Spinner status texts
160
- STATUS_WAITING_TEXT = "Connecting …"
161
+ STATUS_WAITING_TEXT = "Loading …"
161
162
  STATUS_THINKING_TEXT = "Thinking …"
162
163
  STATUS_COMPOSING_TEXT = "Composing"
163
164
 
@@ -98,7 +98,7 @@ class EditTool(ToolABC):
98
98
  if is_directory(file_path):
99
99
  return message.ToolResultMessage(
100
100
  status="error",
101
- output_text="<tool_use_error>Illegal operation on a directory. edit</tool_use_error>",
101
+ output_text="<tool_use_error>Illegal operation on a directory: edit</tool_use_error>",
102
102
  )
103
103
 
104
104
  if args.old_string == "":
@@ -210,7 +210,7 @@ class ReadTool(ToolABC):
210
210
  if is_directory(file_path):
211
211
  return message.ToolResultMessage(
212
212
  status="error",
213
- output_text="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
213
+ output_text="<tool_use_error>Illegal operation on a directory: read</tool_use_error>",
214
214
  )
215
215
  if not file_exists(file_path):
216
216
  return message.ToolResultMessage(
@@ -308,7 +308,7 @@ class ReadTool(ToolABC):
308
308
  except IsADirectoryError:
309
309
  return message.ToolResultMessage(
310
310
  status="error",
311
- output_text="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
311
+ output_text="<tool_use_error>Illegal operation on a directory: read</tool_use_error>",
312
312
  )
313
313
 
314
314
  if offset > max(read_result.total_lines, 0):
@@ -57,7 +57,7 @@ class WriteTool(ToolABC):
57
57
  if is_directory(file_path):
58
58
  return message.ToolResultMessage(
59
59
  status="error",
60
- output_text="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
60
+ output_text="<tool_use_error>Illegal operation on a directory: write</tool_use_error>",
61
61
  )
62
62
 
63
63
  file_tracker = context.file_tracker
klaude_code/core/turn.py CHANGED
@@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator
4
4
  from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from klaude_code.const import SUPPORTED_IMAGE_SIZES
7
+ from klaude_code.const import RETRY_PRESERVE_PARTIAL_MESSAGE, SUPPORTED_IMAGE_SIZES
8
8
  from klaude_code.core.tool import ToolABC
9
9
  from klaude_code.core.tool.context import SubAgentResumeClaims, ToolContext
10
10
 
@@ -24,6 +24,14 @@ from klaude_code.llm.client import LLMStreamABC
24
24
  from klaude_code.log import DebugType, log_debug
25
25
  from klaude_code.protocol import events, llm_param, message, model, tools
26
26
 
27
+ # Protocols that support prefill (continuing from partial assistant message)
28
+ _PREFILL_SUPPORTED_PROTOCOLS = frozenset(
29
+ {
30
+ "anthropic",
31
+ "claude_oauth",
32
+ }
33
+ )
34
+
27
35
 
28
36
  class TurnError(Exception):
29
37
  """Raised when a turn fails and should be retried."""
@@ -176,7 +184,19 @@ class TurnExecutor:
176
184
  yield event
177
185
 
178
186
  if self._turn_result.stream_error is not None:
187
+ # Save accumulated content for potential prefill on retry (only for supported protocols)
179
188
  session_ctx.append_history([self._turn_result.stream_error])
189
+ protocol = ctx.llm_client.get_llm_config().protocol
190
+ supports_prefill = protocol.value in _PREFILL_SUPPORTED_PROTOCOLS
191
+ if (
192
+ RETRY_PRESERVE_PARTIAL_MESSAGE
193
+ and supports_prefill
194
+ and self._turn_result.assistant_message is not None
195
+ and self._turn_result.assistant_message.parts
196
+ ):
197
+ session_ctx.append_history([self._turn_result.assistant_message])
198
+ # Add continuation prompt to avoid Anthropic thinking block requirement
199
+ session_ctx.append_history([message.UserMessage(parts=[message.TextPart(text="continue")])])
180
200
  yield events.TurnEndEvent(session_id=session_ctx.session_id)
181
201
  raise TurnError(self._turn_result.stream_error.error)
182
202
 
@@ -231,9 +251,6 @@ class TurnExecutor:
231
251
  image_size = generation.get("image_size")
232
252
  if image_size in SUPPORTED_IMAGE_SIZES:
233
253
  image_config.image_size = image_size
234
- extra = generation.get("extra")
235
- if isinstance(extra, dict) and extra:
236
- image_config.extra = extra
237
254
  if image_config.model_dump(exclude_none=True):
238
255
  call_param.image_config = image_config
239
256