klaude-code 2.1.1__py3-none-any.whl → 2.3.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 (72) hide show
  1. klaude_code/app/__init__.py +1 -2
  2. klaude_code/app/runtime.py +13 -41
  3. klaude_code/cli/list_model.py +27 -10
  4. klaude_code/cli/main.py +42 -159
  5. klaude_code/config/assets/builtin_config.yaml +36 -14
  6. klaude_code/config/config.py +144 -7
  7. klaude_code/config/select_model.py +38 -13
  8. klaude_code/config/sub_agent_model_helper.py +217 -0
  9. klaude_code/const.py +2 -2
  10. klaude_code/core/agent_profile.py +71 -5
  11. klaude_code/core/executor.py +75 -0
  12. klaude_code/core/manager/llm_clients_builder.py +18 -12
  13. klaude_code/core/prompts/prompt-nano-banana.md +1 -0
  14. klaude_code/core/tool/shell/command_safety.py +4 -189
  15. klaude_code/core/tool/sub_agent_tool.py +2 -1
  16. klaude_code/core/turn.py +1 -1
  17. klaude_code/llm/anthropic/client.py +8 -5
  18. klaude_code/llm/anthropic/input.py +54 -29
  19. klaude_code/llm/google/client.py +2 -2
  20. klaude_code/llm/google/input.py +23 -2
  21. klaude_code/llm/openai_compatible/input.py +22 -13
  22. klaude_code/llm/openai_compatible/stream.py +1 -1
  23. klaude_code/llm/openrouter/input.py +37 -25
  24. klaude_code/llm/responses/client.py +1 -1
  25. klaude_code/llm/responses/input.py +96 -57
  26. klaude_code/protocol/commands.py +1 -2
  27. klaude_code/protocol/events/system.py +4 -0
  28. klaude_code/protocol/message.py +2 -2
  29. klaude_code/protocol/op.py +17 -0
  30. klaude_code/protocol/op_handler.py +5 -0
  31. klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  32. klaude_code/protocol/sub_agent/__init__.py +10 -14
  33. klaude_code/protocol/sub_agent/image_gen.py +2 -1
  34. klaude_code/session/codec.py +2 -6
  35. klaude_code/session/session.py +9 -1
  36. klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  37. klaude_code/tui/command/__init__.py +7 -10
  38. klaude_code/tui/command/clear_cmd.py +1 -1
  39. klaude_code/tui/command/command_abc.py +1 -2
  40. klaude_code/tui/command/copy_cmd.py +1 -2
  41. klaude_code/tui/command/fork_session_cmd.py +4 -4
  42. klaude_code/tui/command/model_cmd.py +6 -43
  43. klaude_code/tui/command/model_select.py +75 -15
  44. klaude_code/tui/command/refresh_cmd.py +1 -2
  45. klaude_code/tui/command/resume_cmd.py +3 -4
  46. klaude_code/tui/command/status_cmd.py +1 -1
  47. klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
  48. klaude_code/tui/components/bash_syntax.py +1 -1
  49. klaude_code/tui/components/common.py +1 -1
  50. klaude_code/tui/components/developer.py +10 -15
  51. klaude_code/tui/components/metadata.py +2 -64
  52. klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  53. klaude_code/tui/components/rich/status.py +49 -3
  54. klaude_code/tui/components/rich/theme.py +4 -2
  55. klaude_code/tui/components/sub_agent.py +25 -46
  56. klaude_code/tui/components/user_input.py +9 -21
  57. klaude_code/tui/components/welcome.py +99 -0
  58. klaude_code/tui/input/prompt_toolkit.py +14 -1
  59. klaude_code/tui/renderer.py +2 -3
  60. klaude_code/tui/runner.py +2 -2
  61. klaude_code/tui/terminal/selector.py +8 -18
  62. klaude_code/ui/__init__.py +0 -24
  63. klaude_code/ui/common.py +3 -2
  64. klaude_code/ui/core/display.py +2 -2
  65. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/METADATA +16 -81
  66. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/RECORD +68 -67
  67. klaude_code/tui/command/help_cmd.py +0 -51
  68. klaude_code/tui/command/prompt-commit.md +0 -82
  69. klaude_code/tui/command/release_notes_cmd.py +0 -85
  70. klaude_code/ui/exec_mode.py +0 -60
  71. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
  72. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -15,6 +15,7 @@ from dataclasses import dataclass
15
15
  from pathlib import Path
16
16
 
17
17
  from klaude_code.config import load_config
18
+ from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
18
19
  from klaude_code.core.agent import Agent
19
20
  from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
20
21
  from klaude_code.core.manager import LLMClients, SubAgentManager
@@ -109,6 +110,15 @@ class AgentRuntime:
109
110
  def current_agent(self) -> Agent | None:
110
111
  return self._agent
111
112
 
113
+ def _get_sub_agent_models(self) -> dict[str, LLMConfigParameter]:
114
+ """Build a dict of sub-agent type to LLMConfigParameter."""
115
+ enabled = self._model_profile_provider.enabled_sub_agent_types()
116
+ return {
117
+ sub_agent_type: client.get_llm_config()
118
+ for sub_agent_type, client in self._llm_clients.sub_clients.items()
119
+ if sub_agent_type in enabled
120
+ }
121
+
112
122
  async def ensure_agent(self, session_id: str | None = None) -> Agent:
113
123
  """Return the active agent, creating or loading a session as needed."""
114
124
 
@@ -135,6 +145,7 @@ class AgentRuntime:
135
145
  session_id=session.id,
136
146
  work_dir=str(session.work_dir),
137
147
  llm_config=self._llm_clients.main.get_llm_config(),
148
+ sub_agent_models=self._get_sub_agent_models(),
138
149
  )
139
150
  )
140
151
 
@@ -200,6 +211,7 @@ class AgentRuntime:
200
211
  session_id=agent.session.id,
201
212
  work_dir=str(agent.session.work_dir),
202
213
  llm_config=self._llm_clients.main.get_llm_config(),
214
+ sub_agent_models=self._get_sub_agent_models(),
203
215
  )
204
216
  )
205
217
 
@@ -223,6 +235,7 @@ class AgentRuntime:
223
235
  session_id=target_session.id,
224
236
  work_dir=str(target_session.work_dir),
225
237
  llm_config=self._llm_clients.main.get_llm_config(),
238
+ sub_agent_models=self._get_sub_agent_models(),
226
239
  )
227
240
  )
228
241
 
@@ -406,6 +419,15 @@ class ExecutorContext:
406
419
  """Emit an event to the UI display system."""
407
420
  await self.event_queue.put(event)
408
421
 
422
+ def _get_sub_agent_models(self) -> dict[str, LLMConfigParameter]:
423
+ """Build a dict of sub-agent type to LLMConfigParameter."""
424
+ enabled = self.model_profile_provider.enabled_sub_agent_types()
425
+ return {
426
+ sub_agent_type: client.get_llm_config()
427
+ for sub_agent_type, client in self.llm_clients.sub_clients.items()
428
+ if sub_agent_type in enabled
429
+ }
430
+
409
431
  def current_session_id(self) -> str | None:
410
432
  """Return the primary active session id, if any.
411
433
 
@@ -455,6 +477,7 @@ class ExecutorContext:
455
477
  llm_config=llm_config,
456
478
  work_dir=str(agent.session.work_dir),
457
479
  show_klaude_code_info=False,
480
+ show_sub_agent_models=False,
458
481
  )
459
482
  )
460
483
 
@@ -501,9 +524,61 @@ class ExecutorContext:
501
524
  work_dir=str(agent.session.work_dir),
502
525
  llm_config=agent.profile.llm_client.get_llm_config(),
503
526
  show_klaude_code_info=False,
527
+ show_sub_agent_models=False,
504
528
  )
505
529
  )
506
530
 
531
+ async def handle_change_sub_agent_model(self, operation: op.ChangeSubAgentModelOperation) -> None:
532
+ """Handle a change sub-agent model operation."""
533
+ agent = await self._agent_runtime.ensure_agent(operation.session_id)
534
+ config = load_config()
535
+
536
+ helper = SubAgentModelHelper(config)
537
+
538
+ sub_agent_type = operation.sub_agent_type
539
+ model_name = operation.model_name
540
+
541
+ if model_name is None:
542
+ # Clear explicit override and revert to sub-agent default behavior.
543
+ behavior = helper.describe_empty_model_config_behavior(
544
+ sub_agent_type,
545
+ main_model_name=self.llm_clients.main.model_name,
546
+ )
547
+
548
+ resolved = helper.resolve_default_model_override(sub_agent_type)
549
+ if resolved is None:
550
+ # Default: inherit from main client.
551
+ self.llm_clients.sub_clients.pop(sub_agent_type, None)
552
+ else:
553
+ # Default: use a dedicated model (e.g. first available image model).
554
+ llm_config = config.get_model_config(resolved)
555
+ new_client = create_llm_client(llm_config)
556
+ self.llm_clients.sub_clients[sub_agent_type] = new_client
557
+
558
+ display_model = f"({behavior.description})"
559
+ else:
560
+ # Create new client for the sub-agent
561
+ llm_config = config.get_model_config(model_name)
562
+ new_client = create_llm_client(llm_config)
563
+ self.llm_clients.sub_clients[sub_agent_type] = new_client
564
+ display_model = new_client.model_name
565
+
566
+ if operation.save_as_default:
567
+ if model_name is None:
568
+ # Remove from config to inherit
569
+ config.sub_agent_models.pop(sub_agent_type, None)
570
+ else:
571
+ config.sub_agent_models[sub_agent_type] = model_name
572
+ await config.save()
573
+
574
+ saved_note = " (saved in ~/.klaude/klaude-config.yaml)" if operation.save_as_default else ""
575
+ developer_item = message.DeveloperMessage(
576
+ parts=message.text_parts_from_str(f"{sub_agent_type} model: {display_model}{saved_note}"),
577
+ ui_extra=model.build_command_output_extra(commands.CommandName.SUB_AGENT_MODEL),
578
+ )
579
+ agent.session.append_history([developer_item])
580
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
581
+
507
582
  async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
508
583
  await self._agent_runtime.clear_session(operation.session_id)
509
584
 
@@ -3,11 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from klaude_code.config import Config
6
+ from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
6
7
  from klaude_code.core.manager.llm_clients import LLMClients
7
8
  from klaude_code.llm.client import LLMClientABC
8
9
  from klaude_code.llm.registry import create_llm_client
9
10
  from klaude_code.log import DebugType, log_debug
10
- from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
11
11
  from klaude_code.protocol.tools import SubAgentType
12
12
 
13
13
 
@@ -15,13 +15,20 @@ def build_llm_clients(
15
15
  config: Config,
16
16
  *,
17
17
  model_override: str | None = None,
18
+ skip_sub_agents: bool = False,
18
19
  ) -> LLMClients:
19
- """Create an ``LLMClients`` bundle driven by application config."""
20
+ """Create an ``LLMClients`` bundle driven by application config.
21
+
22
+ Args:
23
+ config: Application configuration.
24
+ model_override: Override for the main model name.
25
+ skip_sub_agents: If True, skip initializing sub-agent clients (e.g., for vanilla/banana modes).
26
+ """
20
27
 
21
28
  # Resolve main agent LLM config
22
29
  model_name = model_override or config.main_model
23
30
  if model_name is None:
24
- raise ValueError("No model specified. Use --model or --select-model to specify a model.")
31
+ raise ValueError("No model specified. Set main_model in the config or pass --model.")
25
32
  llm_config = config.get_model_config(model_name)
26
33
 
27
34
  log_debug(
@@ -32,17 +39,16 @@ def build_llm_clients(
32
39
  )
33
40
 
34
41
  main_client = create_llm_client(llm_config)
35
- sub_clients: dict[SubAgentType, LLMClientABC] = {}
36
42
 
37
- for profile in iter_sub_agent_profiles():
38
- model_name = config.sub_agent_models.get(profile.name)
39
- if not model_name:
40
- continue
43
+ if skip_sub_agents:
44
+ return LLMClients(main=main_client)
41
45
 
42
- if not profile.enabled_for_model(main_client.model_name):
43
- continue
46
+ helper = SubAgentModelHelper(config)
47
+ sub_agent_configs = helper.build_sub_agent_client_configs()
44
48
 
45
- sub_llm_config = config.get_model_config(model_name)
46
- sub_clients[profile.name] = create_llm_client(sub_llm_config)
49
+ sub_clients: dict[SubAgentType, LLMClientABC] = {}
50
+ for sub_agent_type, sub_model_name in sub_agent_configs.items():
51
+ sub_llm_config = config.get_model_config(sub_model_name)
52
+ sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
47
53
 
48
54
  return LLMClients(main=main_client, sub_clients=sub_clients)
@@ -0,0 +1 @@
1
+ You're a helpful assistant with capabilities to generate images and edit images.
@@ -1,5 +1,4 @@
1
1
  import os
2
- import re
3
2
  import shlex
4
3
 
5
4
 
@@ -11,76 +10,6 @@ class SafetyCheckResult:
11
10
  self.error_msg = error_msg
12
11
 
13
12
 
14
- def _is_valid_sed_n_arg(s: str | None) -> bool:
15
- if not s:
16
- return False
17
- # Matches: Np or M,Np where M,N are positive integers
18
- return bool(re.fullmatch(r"\d+(,\d+)?p", s))
19
-
20
-
21
- def _is_safe_awk_program(program: str) -> SafetyCheckResult:
22
- lowered = program.lower()
23
-
24
- if "`" in program:
25
- return SafetyCheckResult(False, "awk: backticks not allowed in program")
26
- if "$(" in program:
27
- return SafetyCheckResult(False, "awk: command substitution not allowed in program")
28
- if "|&" in program:
29
- return SafetyCheckResult(False, "awk: background pipeline not allowed in program")
30
-
31
- if "system(" in lowered:
32
- return SafetyCheckResult(False, "awk: system() call not allowed in program")
33
-
34
- if re.search(r"(?<![|&>])\bprint\s*\|", program, re.IGNORECASE):
35
- return SafetyCheckResult(False, "awk: piping output to external command not allowed")
36
- if re.search(r"\bprintf\s*\|", program, re.IGNORECASE):
37
- return SafetyCheckResult(False, "awk: piping output to external command not allowed")
38
-
39
- return SafetyCheckResult(True)
40
-
41
-
42
- def _is_safe_awk_argv(argv: list[str]) -> SafetyCheckResult:
43
- if len(argv) < 2:
44
- return SafetyCheckResult(False, "awk: Missing program")
45
-
46
- program: str | None = None
47
-
48
- i = 1
49
- while i < len(argv):
50
- arg = argv[i]
51
-
52
- if arg in {"-f", "--file", "--source"} or arg.startswith("-f"):
53
- return SafetyCheckResult(False, "awk: -f/--file not allowed")
54
-
55
- if arg in {"-e", "--exec"}:
56
- if i + 1 >= len(argv):
57
- return SafetyCheckResult(False, "awk: Missing program for -e")
58
- script = argv[i + 1]
59
- program_check = _is_safe_awk_program(script)
60
- if not program_check.is_safe:
61
- return program_check
62
- if program is None:
63
- program = script
64
- i += 2
65
- continue
66
-
67
- if arg.startswith("-"):
68
- i += 1
69
- continue
70
-
71
- if program is None:
72
- program_check = _is_safe_awk_program(arg)
73
- if not program_check.is_safe:
74
- return program_check
75
- program = arg
76
- i += 1
77
-
78
- if program is None:
79
- return SafetyCheckResult(False, "awk: Missing program")
80
-
81
- return SafetyCheckResult(True)
82
-
83
-
84
13
  def _is_safe_rm_argv(argv: list[str]) -> SafetyCheckResult:
85
14
  """Check safety of rm command arguments."""
86
15
  # Enforce strict safety rules for rm operands
@@ -217,112 +146,12 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
217
146
 
218
147
  cmd0 = argv[0]
219
148
 
220
- # if _has_shell_redirection(argv):
221
- # return SafetyCheckResult(False, "Shell redirection and pipelines are not allowed in single commands")
222
-
223
- # Special handling for rm to prevent dangerous operations
224
149
  if cmd0 == "rm":
225
150
  return _is_safe_rm_argv(argv)
226
151
 
227
- # Special handling for trash to prevent dangerous operations
228
152
  if cmd0 == "trash":
229
153
  return _is_safe_trash_argv(argv)
230
154
 
231
- if cmd0 == "find":
232
- unsafe_opts = {
233
- "-exec": "command execution",
234
- "-execdir": "command execution",
235
- "-ok": "interactive command execution",
236
- "-okdir": "interactive command execution",
237
- "-delete": "file deletion",
238
- "-fls": "file output",
239
- "-fprint": "file output",
240
- "-fprint0": "file output",
241
- "-fprintf": "formatted file output",
242
- }
243
- for arg in argv[1:]:
244
- if arg in unsafe_opts:
245
- return SafetyCheckResult(False, f"find: {unsafe_opts[arg]} option '{arg}' not allowed")
246
- return SafetyCheckResult(True)
247
-
248
- if cmd0 == "git":
249
- sub = argv[1] if len(argv) > 1 else None
250
- if not sub:
251
- return SafetyCheckResult(False, "git: Missing subcommand")
252
-
253
- # Allow most local git operations, but block remote operations
254
- allowed_git_cmds = {
255
- "add",
256
- "branch",
257
- "checkout",
258
- "commit",
259
- "config",
260
- "diff",
261
- "fetch",
262
- "init",
263
- "log",
264
- "merge",
265
- "mv",
266
- "rebase",
267
- "reset",
268
- "restore",
269
- "revert",
270
- "rm",
271
- "show",
272
- "stash",
273
- "status",
274
- "switch",
275
- "tag",
276
- "clone",
277
- "worktree",
278
- "push",
279
- "pull",
280
- "remote",
281
- }
282
- if sub not in allowed_git_cmds:
283
- return SafetyCheckResult(False, f"git: Subcommand '{sub}' not in allow list")
284
- return SafetyCheckResult(True)
285
-
286
- # Build tools and linters - allow all subcommands
287
- if cmd0 in {
288
- "cargo",
289
- "uv",
290
- "go",
291
- "ruff",
292
- "pyright",
293
- "make",
294
- "npm",
295
- "pnpm",
296
- "bun",
297
- }:
298
- return SafetyCheckResult(True)
299
-
300
- if cmd0 == "sed":
301
- # Allow sed -n patterns (line printing)
302
- if len(argv) >= 3 and argv[1] == "-n" and _is_valid_sed_n_arg(argv[2]):
303
- return SafetyCheckResult(True)
304
- # Allow simple text replacement: sed 's/old/new/g' file
305
- # or sed -i 's/old/new/g' file for in-place editing
306
- if len(argv) >= 3:
307
- # Find the sed script argument (usually starts with 's/')
308
- for arg in argv[1:]:
309
- if arg.startswith("s/") or arg.startswith("s|"):
310
- # Basic safety check: no command execution in replacement
311
- if ";" in arg:
312
- return SafetyCheckResult(False, f"sed: Command separator ';' not allowed in '{arg}'")
313
- if "`" in arg:
314
- return SafetyCheckResult(False, f"sed: Backticks not allowed in '{arg}'")
315
- if "$(" in arg:
316
- return SafetyCheckResult(False, f"sed: Command substitution not allowed in '{arg}'")
317
- return SafetyCheckResult(True)
318
- return SafetyCheckResult(
319
- False,
320
- "sed: Only text replacement (s/old/new/) or line printing (-n 'Np') is allowed",
321
- )
322
-
323
- if cmd0 == "awk":
324
- return _is_safe_awk_argv(argv)
325
-
326
155
  # Default allow when command is not explicitly restricted
327
156
  return SafetyCheckResult(True)
328
157
 
@@ -330,30 +159,16 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
330
159
  def is_safe_command(command: str) -> SafetyCheckResult:
331
160
  """Determine if a command is safe enough to run.
332
161
 
333
- The check is intentionally lightweight: it blocks only a small set of
334
- obviously dangerous patterns (rm/trash/git remotes, unsafe sed/awk,
335
- find -exec/-delete, etc.) and otherwise lets the real shell surface
336
- syntax errors (for example, unmatched quotes in complex multiline
337
- scripts).
162
+ Only rm and trash commands are checked for safety. All other commands
163
+ are allowed by default.
338
164
  """
339
-
340
- # Try to parse into an argv-style list first. If this fails (e.g. due
341
- # to unterminated quotes in a complex heredoc), treat the command as
342
- # safe here and let bash itself perform syntax checking instead of
343
- # blocking execution pre-emptively.
344
165
  try:
345
166
  argv = shlex.split(command, posix=True)
346
167
  except ValueError:
347
- # If we cannot reliably parse the command (e.g. due to unterminated
348
- # quotes in a complex heredoc), treat it as safe here and let the
349
- # real shell surface any syntax errors instead of blocking execution
350
- # pre-emptively.
168
+ # If we cannot reliably parse the command, treat it as safe here
169
+ # and let the real shell surface any syntax errors
351
170
  return SafetyCheckResult(True)
352
171
 
353
- # All further safety checks are done directly on the parsed argv via
354
- # _is_safe_argv. We intentionally avoid trying to re-interpret complex
355
- # shell sequences here and rely on the real shell to handle syntax.
356
-
357
172
  if not argv:
358
173
  return SafetyCheckResult(False, "Empty command")
359
174
 
@@ -31,11 +31,12 @@ class SubAgentTool(ToolABC):
31
31
  @classmethod
32
32
  def for_profile(cls, profile: SubAgentProfile) -> type[SubAgentTool]:
33
33
  """Create a tool class for a specific sub-agent profile."""
34
- return type(
34
+ tool_cls = type(
35
35
  f"{profile.name}Tool",
36
36
  (SubAgentTool,),
37
37
  {"_profile": profile},
38
38
  )
39
+ return cast(type[SubAgentTool], tool_cls)
39
40
 
40
41
  @classmethod
41
42
  def metadata(cls) -> ToolMetadata:
klaude_code/core/turn.py CHANGED
@@ -348,7 +348,7 @@ class TurnExecutor:
348
348
  style="red",
349
349
  debug_type=DebugType.RESPONSE,
350
350
  )
351
- case message.ToolCallStartItem() as msg:
351
+ case message.ToolCallStartDelta() as msg:
352
352
  if thinking_active:
353
353
  thinking_active = False
354
354
  yield events.ThinkingEndEvent(
@@ -16,6 +16,7 @@ from anthropic.types.beta.beta_raw_message_start_event import BetaRawMessageStar
16
16
  from anthropic.types.beta.beta_signature_delta import BetaSignatureDelta
17
17
  from anthropic.types.beta.beta_text_delta import BetaTextDelta
18
18
  from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
19
+ from anthropic.types.beta.beta_tool_choice_auto_param import BetaToolChoiceAutoParam
19
20
  from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
20
21
  from anthropic.types.beta.message_create_params import MessageCreateParamsStreaming
21
22
 
@@ -82,12 +83,14 @@ def build_payload(
82
83
  # Prepend extra betas, avoiding duplicates
83
84
  betas = [b for b in extra_betas if b not in betas] + betas
84
85
 
86
+ tool_choice: BetaToolChoiceAutoParam = {
87
+ "type": "auto",
88
+ "disable_parallel_tool_use": False,
89
+ }
90
+
85
91
  payload: MessageCreateParamsStreaming = {
86
92
  "model": str(param.model),
87
- "tool_choice": {
88
- "type": "auto",
89
- "disable_parallel_tool_use": False,
90
- },
93
+ "tool_choice": tool_choice,
91
94
  "stream": True,
92
95
  "max_tokens": param.max_tokens or DEFAULT_MAX_TOKENS,
93
96
  "temperature": param.temperature or DEFAULT_TEMPERATURE,
@@ -169,7 +172,7 @@ async def parse_anthropic_stream(
169
172
  match event.content_block:
170
173
  case BetaToolUseBlock() as block:
171
174
  metadata_tracker.record_token()
172
- yield message.ToolCallStartItem(
175
+ yield message.ToolCallStartDelta(
173
176
  response_id=response_id,
174
177
  call_id=block.id,
175
178
  name=block.name,
@@ -8,10 +8,13 @@ import json
8
8
  from typing import Literal, cast
9
9
 
10
10
  from anthropic.types.beta.beta_base64_image_source_param import BetaBase64ImageSourceParam
11
+ from anthropic.types.beta.beta_content_block_param import BetaContentBlockParam
11
12
  from anthropic.types.beta.beta_image_block_param import BetaImageBlockParam
12
13
  from anthropic.types.beta.beta_message_param import BetaMessageParam
13
14
  from anthropic.types.beta.beta_text_block_param import BetaTextBlockParam
14
15
  from anthropic.types.beta.beta_tool_param import BetaToolParam
16
+ from anthropic.types.beta.beta_tool_result_block_param import BetaToolResultBlockParam
17
+ from anthropic.types.beta.beta_tool_use_block_param import BetaToolUseBlockParam
15
18
  from anthropic.types.beta.beta_url_image_source_param import BetaURLImageSourceParam
16
19
 
17
20
  from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
@@ -60,29 +63,29 @@ def _user_message_to_message(
60
63
  blocks: list[BetaTextBlockParam | BetaImageBlockParam] = []
61
64
  for part in msg.parts:
62
65
  if isinstance(part, message.TextPart):
63
- blocks.append({"type": "text", "text": part.text})
66
+ blocks.append(cast(BetaTextBlockParam, {"type": "text", "text": part.text}))
64
67
  elif isinstance(part, message.ImageURLPart):
65
68
  blocks.append(_image_part_to_block(part))
66
69
  if attachment.text:
67
- blocks.append({"type": "text", "text": attachment.text})
70
+ blocks.append(cast(BetaTextBlockParam, {"type": "text", "text": attachment.text}))
68
71
  for image in attachment.images:
69
72
  blocks.append(_image_part_to_block(image))
70
73
  if not blocks:
71
- blocks.append({"type": "text", "text": ""})
74
+ blocks.append(cast(BetaTextBlockParam, {"type": "text", "text": ""}))
72
75
  return {"role": "user", "content": blocks}
73
76
 
74
77
 
75
78
  def _tool_message_to_block(
76
79
  msg: message.ToolResultMessage,
77
80
  attachment: DeveloperAttachment,
78
- ) -> dict[str, object]:
81
+ ) -> BetaToolResultBlockParam:
79
82
  """Convert a single tool result message to a tool_result block."""
80
83
  tool_content: list[BetaTextBlockParam | BetaImageBlockParam] = []
81
84
  merged_text = merge_reminder_text(
82
85
  msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
83
86
  attachment.text,
84
87
  )
85
- tool_content.append({"type": "text", "text": merged_text})
88
+ tool_content.append(cast(BetaTextBlockParam, {"type": "text", "text": merged_text}))
86
89
  for image in [part for part in msg.parts if isinstance(part, message.ImageURLPart)]:
87
90
  tool_content.append(_image_part_to_block(image))
88
91
  for image in attachment.images:
@@ -95,7 +98,7 @@ def _tool_message_to_block(
95
98
  }
96
99
 
97
100
 
98
- def _tool_blocks_to_message(blocks: list[dict[str, object]]) -> BetaMessageParam:
101
+ def _tool_blocks_to_message(blocks: list[BetaToolResultBlockParam]) -> BetaMessageParam:
99
102
  """Convert one or more tool_result blocks to a single user message."""
100
103
  return {
101
104
  "role": "user",
@@ -104,7 +107,7 @@ def _tool_blocks_to_message(blocks: list[dict[str, object]]) -> BetaMessageParam
104
107
 
105
108
 
106
109
  def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str | None) -> BetaMessageParam:
107
- content: list[dict[str, object]] = []
110
+ content: list[BetaContentBlockParam] = []
108
111
  current_thinking_content: str | None = None
109
112
  native_thinking_parts, degraded_thinking_texts = split_thinking_parts(msg, model_name)
110
113
  native_thinking_ids = {id(part) for part in native_thinking_parts}
@@ -113,7 +116,7 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
113
116
  nonlocal current_thinking_content
114
117
  if current_thinking_content is None:
115
118
  return
116
- content.append({"type": "thinking", "thinking": current_thinking_content})
119
+ degraded_thinking_texts.append(current_thinking_content)
117
120
  current_thinking_content = None
118
121
 
119
122
  for part in msg.parts:
@@ -127,33 +130,47 @@ def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str
127
130
  continue
128
131
  if part.signature:
129
132
  content.append(
130
- {
131
- "type": "thinking",
132
- "thinking": current_thinking_content or "",
133
- "signature": part.signature,
134
- }
133
+ cast(
134
+ BetaContentBlockParam,
135
+ {
136
+ "type": "thinking",
137
+ "thinking": current_thinking_content or "",
138
+ "signature": part.signature,
139
+ },
140
+ )
135
141
  )
136
142
  current_thinking_content = None
137
143
  continue
138
144
 
139
145
  _flush_thinking()
140
146
  if isinstance(part, message.TextPart):
141
- content.append({"type": "text", "text": part.text})
147
+ content.append(cast(BetaTextBlockParam, {"type": "text", "text": part.text}))
142
148
  elif isinstance(part, message.ToolCallPart):
149
+ tool_input: dict[str, object] = {}
150
+ if part.arguments_json:
151
+ try:
152
+ parsed = json.loads(part.arguments_json)
153
+ except json.JSONDecodeError:
154
+ parsed = {"_raw": part.arguments_json}
155
+ tool_input = cast(dict[str, object], parsed) if isinstance(parsed, dict) else {"_value": parsed}
156
+
143
157
  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
- }
158
+ cast(
159
+ BetaToolUseBlockParam,
160
+ {
161
+ "type": "tool_use",
162
+ "id": part.call_id,
163
+ "name": part.tool_name,
164
+ "input": tool_input,
165
+ },
166
+ )
150
167
  )
151
168
 
152
169
  _flush_thinking()
153
170
 
154
171
  if degraded_thinking_texts:
155
172
  degraded_text = "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"
156
- content.insert(0, {"type": "text", "text": degraded_text})
173
+ content.insert(0, cast(BetaTextBlockParam, {"type": "text", "text": degraded_text}))
157
174
 
158
175
  return {"role": "assistant", "content": content}
159
176
 
@@ -174,7 +191,7 @@ def convert_history_to_input(
174
191
  ) -> list[BetaMessageParam]:
175
192
  """Convert a list of messages to beta message params."""
176
193
  messages: list[BetaMessageParam] = []
177
- pending_tool_blocks: list[dict[str, object]] = []
194
+ pending_tool_blocks: list[BetaToolResultBlockParam] = []
178
195
 
179
196
  def flush_tool_blocks() -> None:
180
197
  nonlocal pending_tool_blocks
@@ -213,7 +230,12 @@ def convert_system_to_input(
213
230
  parts.append("\n".join(part.text for part in msg.parts))
214
231
  if not parts:
215
232
  return []
216
- return [{"type": "text", "text": "\n".join(parts), "cache_control": {"type": "ephemeral"}}]
233
+ block: BetaTextBlockParam = {
234
+ "type": "text",
235
+ "text": "\n".join(parts),
236
+ "cache_control": {"type": "ephemeral"},
237
+ }
238
+ return [block]
217
239
 
218
240
 
219
241
  def convert_tool_schema(
@@ -222,11 +244,14 @@ def convert_tool_schema(
222
244
  if tools is None:
223
245
  return []
224
246
  return [
225
- {
226
- "input_schema": tool.parameters,
227
- "type": "custom",
228
- "name": tool.name,
229
- "description": tool.description,
230
- }
247
+ cast(
248
+ BetaToolParam,
249
+ {
250
+ "input_schema": tool.parameters,
251
+ "type": "custom",
252
+ "name": tool.name,
253
+ "description": tool.description,
254
+ },
255
+ )
231
256
  for tool in tools
232
257
  ]