klaude-code 2.2.0__py3-none-any.whl → 2.4.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 (82) hide show
  1. klaude_code/app/runtime.py +2 -15
  2. klaude_code/cli/list_model.py +30 -13
  3. klaude_code/cli/main.py +26 -10
  4. klaude_code/config/assets/builtin_config.yaml +177 -310
  5. klaude_code/config/config.py +158 -21
  6. klaude_code/config/{select_model.py → model_matcher.py} +41 -16
  7. klaude_code/config/sub_agent_model_helper.py +217 -0
  8. klaude_code/config/thinking.py +2 -2
  9. klaude_code/const.py +1 -1
  10. klaude_code/core/agent_profile.py +43 -5
  11. klaude_code/core/executor.py +129 -47
  12. klaude_code/core/manager/llm_clients_builder.py +17 -11
  13. klaude_code/core/prompts/prompt-nano-banana.md +1 -1
  14. klaude_code/core/tool/file/diff_builder.py +25 -18
  15. klaude_code/core/tool/sub_agent_tool.py +2 -1
  16. klaude_code/llm/anthropic/client.py +12 -9
  17. klaude_code/llm/anthropic/input.py +54 -29
  18. klaude_code/llm/client.py +1 -1
  19. klaude_code/llm/codex/client.py +2 -2
  20. klaude_code/llm/google/client.py +7 -7
  21. klaude_code/llm/google/input.py +23 -2
  22. klaude_code/llm/input_common.py +2 -2
  23. klaude_code/llm/openai_compatible/client.py +3 -3
  24. klaude_code/llm/openai_compatible/input.py +22 -13
  25. klaude_code/llm/openai_compatible/stream.py +1 -1
  26. klaude_code/llm/openrouter/client.py +4 -4
  27. klaude_code/llm/openrouter/input.py +35 -25
  28. klaude_code/llm/responses/client.py +5 -5
  29. klaude_code/llm/responses/input.py +96 -57
  30. klaude_code/protocol/commands.py +1 -2
  31. klaude_code/protocol/events/__init__.py +7 -1
  32. klaude_code/protocol/events/chat.py +10 -0
  33. klaude_code/protocol/events/system.py +4 -0
  34. klaude_code/protocol/llm_param.py +1 -1
  35. klaude_code/protocol/model.py +0 -26
  36. klaude_code/protocol/op.py +17 -5
  37. klaude_code/protocol/op_handler.py +5 -0
  38. klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  39. klaude_code/protocol/sub_agent/__init__.py +10 -14
  40. klaude_code/protocol/sub_agent/image_gen.py +2 -1
  41. klaude_code/session/codec.py +2 -6
  42. klaude_code/session/session.py +13 -3
  43. klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  44. klaude_code/tui/command/__init__.py +3 -6
  45. klaude_code/tui/command/clear_cmd.py +0 -1
  46. klaude_code/tui/command/command_abc.py +6 -4
  47. klaude_code/tui/command/copy_cmd.py +10 -10
  48. klaude_code/tui/command/debug_cmd.py +11 -10
  49. klaude_code/tui/command/export_online_cmd.py +18 -23
  50. klaude_code/tui/command/fork_session_cmd.py +39 -43
  51. klaude_code/tui/command/model_cmd.py +10 -49
  52. klaude_code/tui/command/model_picker.py +142 -0
  53. klaude_code/tui/command/refresh_cmd.py +0 -1
  54. klaude_code/tui/command/registry.py +15 -21
  55. klaude_code/tui/command/resume_cmd.py +10 -16
  56. klaude_code/tui/command/status_cmd.py +8 -12
  57. klaude_code/tui/command/sub_agent_model_cmd.py +185 -0
  58. klaude_code/tui/command/terminal_setup_cmd.py +8 -11
  59. klaude_code/tui/command/thinking_cmd.py +4 -6
  60. klaude_code/tui/commands.py +5 -0
  61. klaude_code/tui/components/bash_syntax.py +1 -1
  62. klaude_code/tui/components/command_output.py +96 -0
  63. klaude_code/tui/components/common.py +1 -1
  64. klaude_code/tui/components/developer.py +3 -115
  65. klaude_code/tui/components/metadata.py +1 -63
  66. klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  67. klaude_code/tui/components/rich/status.py +49 -3
  68. klaude_code/tui/components/rich/theme.py +2 -0
  69. klaude_code/tui/components/sub_agent.py +25 -46
  70. klaude_code/tui/components/welcome.py +99 -0
  71. klaude_code/tui/input/prompt_toolkit.py +19 -8
  72. klaude_code/tui/machine.py +5 -0
  73. klaude_code/tui/renderer.py +7 -8
  74. klaude_code/tui/runner.py +0 -6
  75. klaude_code/tui/terminal/selector.py +8 -6
  76. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/METADATA +21 -74
  77. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/RECORD +79 -76
  78. klaude_code/tui/command/help_cmd.py +0 -51
  79. klaude_code/tui/command/model_select.py +0 -84
  80. klaude_code/tui/command/release_notes_cmd.py +0 -85
  81. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/WHEEL +0 -0
  82. {klaude_code-2.2.0.dist-info → klaude_code-2.4.0.dist-info}/entry_points.txt +0 -0
@@ -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
  ]
klaude_code/llm/client.py CHANGED
@@ -25,7 +25,7 @@ class LLMClientABC(ABC):
25
25
 
26
26
  @property
27
27
  def model_name(self) -> str:
28
- return self._config.model or ""
28
+ return self._config.model_id or ""
29
29
 
30
30
  @property
31
31
  def protocol(self) -> llm_param.LLMClientProtocol:
@@ -31,13 +31,13 @@ from klaude_code.protocol import llm_param, message
31
31
 
32
32
  def build_payload(param: llm_param.LLMCallParameter) -> ResponseCreateParamsStreaming:
33
33
  """Build Codex API request parameters."""
34
- inputs = convert_history_to_input(param.input, param.model)
34
+ inputs = convert_history_to_input(param.input, param.model_id)
35
35
  tools = convert_tool_schema(param.tools)
36
36
 
37
37
  session_id = param.session_id or ""
38
38
 
39
39
  payload: ResponseCreateParamsStreaming = {
40
- "model": str(param.model),
40
+ "model": str(param.model_id),
41
41
  "tool_choice": "auto",
42
42
  "parallel_tool_calls": True,
43
43
  "include": [
@@ -54,7 +54,7 @@ def _build_config(param: llm_param.LLMCallParameter) -> GenerateContentConfig:
54
54
  system_instruction=param.system,
55
55
  temperature=param.temperature,
56
56
  max_output_tokens=param.max_tokens,
57
- tools=tool_list or None,
57
+ tools=cast(Any, tool_list) if tool_list else None,
58
58
  tool_config=tool_config,
59
59
  thinking_config=thinking_config,
60
60
  )
@@ -163,7 +163,7 @@ async def parse_google_stream(
163
163
  assistant_parts.append(
164
164
  message.ThinkingTextPart(
165
165
  text="".join(accumulated_thoughts),
166
- model_id=str(param.model),
166
+ model_id=str(param.model_id),
167
167
  )
168
168
  )
169
169
  accumulated_thoughts.clear()
@@ -171,7 +171,7 @@ async def parse_google_stream(
171
171
  assistant_parts.append(
172
172
  message.ThinkingSignaturePart(
173
173
  signature=thought_signature,
174
- model_id=str(param.model),
174
+ model_id=str(param.model_id),
175
175
  format="google_thought_signature",
176
176
  )
177
177
  )
@@ -301,7 +301,7 @@ async def parse_google_stream(
301
301
  usage = _usage_from_metadata(last_usage_metadata, context_limit=param.context_limit, max_tokens=param.max_tokens)
302
302
  if usage is not None:
303
303
  metadata_tracker.set_usage(usage)
304
- metadata_tracker.set_model_name(str(param.model))
304
+ metadata_tracker.set_model_name(str(param.model_id))
305
305
  metadata_tracker.set_response_id(response_id)
306
306
  metadata = metadata_tracker.finalize()
307
307
  yield message.AssistantMessage(
@@ -336,13 +336,13 @@ class GoogleClient(LLMClientABC):
336
336
  param = apply_config_defaults(param, self.get_llm_config())
337
337
  metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
338
338
 
339
- contents = convert_history_to_contents(param.input, model_name=str(param.model))
339
+ contents = convert_history_to_contents(param.input, model_name=str(param.model_id))
340
340
  config = _build_config(param)
341
341
 
342
342
  log_debug(
343
343
  json.dumps(
344
344
  {
345
- "model": str(param.model),
345
+ "model": str(param.model_id),
346
346
  "contents": [c.model_dump(exclude_none=True) for c in contents],
347
347
  "config": config.model_dump(exclude_none=True),
348
348
  },
@@ -354,7 +354,7 @@ class GoogleClient(LLMClientABC):
354
354
 
355
355
  try:
356
356
  stream = await self.client.aio.models.generate_content_stream(
357
- model=str(param.model),
357
+ model=str(param.model_id),
358
358
  contents=cast(Any, contents),
359
359
  config=config,
360
360
  )
@@ -4,6 +4,8 @@
4
4
  # pyright: reportAttributeAccessIssue=false
5
5
 
6
6
  import json
7
+ from base64 import b64decode
8
+ from binascii import Error as BinasciiError
7
9
  from typing import Any
8
10
 
9
11
  from google.genai import types
@@ -32,6 +34,14 @@ def _image_part_to_part(image: message.ImageURLPart) -> types.Part:
32
34
  return types.Part(file_data=types.FileData(file_uri=url))
33
35
 
34
36
 
37
+ def _image_part_to_function_response_part(image: message.ImageURLPart) -> types.FunctionResponsePart:
38
+ url = image.url
39
+ if url.startswith("data:"):
40
+ media_type, _, decoded = parse_data_url(url)
41
+ return types.FunctionResponsePart.from_bytes(data=decoded, mime_type=media_type)
42
+ return types.FunctionResponsePart.from_uri(file_uri=url)
43
+
44
+
35
45
  def _user_message_to_content(msg: message.UserMessage, attachment: DeveloperAttachment) -> types.Content:
36
46
  parts: list[types.Part] = []
37
47
  for part in msg.parts:
@@ -65,9 +75,12 @@ def _tool_messages_to_contents(
65
75
 
66
76
  images = [part for part in msg.parts if isinstance(part, message.ImageURLPart)] + attachment.images
67
77
  image_parts: list[types.Part] = []
78
+ function_response_parts: list[types.FunctionResponsePart] = []
79
+
68
80
  for image in images:
69
81
  try:
70
82
  image_parts.append(_image_part_to_part(image))
83
+ function_response_parts.append(_image_part_to_function_response_part(image))
71
84
  except ValueError:
72
85
  continue
73
86
 
@@ -79,7 +92,7 @@ def _tool_messages_to_contents(
79
92
  id=msg.call_id,
80
93
  name=msg.tool_name,
81
94
  response=response_payload,
82
- parts=image_parts if (has_images and supports_multimodal_function_response) else None,
95
+ parts=function_response_parts if (has_images and supports_multimodal_function_response) else None,
83
96
  )
84
97
  response_parts.append(types.Part(function_response=function_response))
85
98
 
@@ -106,11 +119,19 @@ def _assistant_message_to_content(msg: message.AssistantMessage, model_name: str
106
119
  nonlocal pending_thought_text, pending_thought_signature
107
120
  if pending_thought_text is None and pending_thought_signature is None:
108
121
  return
122
+
123
+ signature_bytes: bytes | None = None
124
+ if pending_thought_signature:
125
+ try:
126
+ signature_bytes = b64decode(pending_thought_signature)
127
+ except (BinasciiError, ValueError):
128
+ signature_bytes = None
129
+
109
130
  parts.append(
110
131
  types.Part(
111
132
  text=pending_thought_text or "",
112
133
  thought=True,
113
- thought_signature=pending_thought_signature,
134
+ thought_signature=signature_bytes,
114
135
  )
115
136
  )
116
137
  pending_thought_text = None
@@ -165,8 +165,8 @@ def split_thinking_parts(
165
165
 
166
166
  def apply_config_defaults(param: "LLMCallParameter", config: "LLMConfigParameter") -> "LLMCallParameter":
167
167
  """Apply config defaults to LLM call parameters."""
168
- if param.model is None:
169
- param.model = config.model
168
+ if param.model_id is None:
169
+ param.model_id = config.model_id
170
170
  if param.temperature is None:
171
171
  param.temperature = config.temperature
172
172
  if param.max_tokens is None:
@@ -19,7 +19,7 @@ from klaude_code.protocol import llm_param, message
19
19
 
20
20
  def build_payload(param: llm_param.LLMCallParameter) -> tuple[CompletionCreateParamsStreaming, dict[str, object]]:
21
21
  """Build OpenAI API request parameters."""
22
- messages = convert_history_to_input(param.input, param.system, param.model)
22
+ messages = convert_history_to_input(param.input, param.system, param.model_id)
23
23
  tools = convert_tool_schema(param.tools)
24
24
 
25
25
  extra_body: dict[str, object] = {}
@@ -31,7 +31,7 @@ def build_payload(param: llm_param.LLMCallParameter) -> tuple[CompletionCreatePa
31
31
  }
32
32
 
33
33
  payload: CompletionCreateParamsStreaming = {
34
- "model": str(param.model),
34
+ "model": str(param.model_id),
35
35
  "tool_choice": "auto",
36
36
  "parallel_tool_calls": True,
37
37
  "stream": True,
@@ -108,7 +108,7 @@ class OpenAICompatibleClient(LLMClientABC):
108
108
  return
109
109
 
110
110
  reasoning_handler = DefaultReasoningHandler(
111
- param_model=str(param.model),
111
+ param_model=str(param.model_id),
112
112
  response_id=None,
113
113
  )
114
114
 
@@ -3,6 +3,8 @@
3
3
  # pyright: reportUnknownMemberType=false
4
4
  # pyright: reportAttributeAccessIssue=false
5
5
 
6
+ from typing import cast
7
+
6
8
  from openai.types import chat
7
9
  from openai.types.chat import ChatCompletionContentPartParam
8
10
 
@@ -25,14 +27,16 @@ def _assistant_message_to_openai(msg: message.AssistantMessage) -> chat.ChatComp
25
27
  assistant_message["content"] = text_content
26
28
 
27
29
  assistant_message.update(build_assistant_common_fields(msg, image_to_data_url=assistant_image_to_data_url))
28
- return assistant_message
30
+ return cast(chat.ChatCompletionMessageParam, assistant_message)
29
31
 
30
32
 
31
33
  def build_user_content_parts(
32
34
  images: list[message.ImageURLPart],
33
35
  ) -> list[ChatCompletionContentPartParam]:
34
36
  """Build content parts for images only. Used by OpenRouter."""
35
- return [{"type": "image_url", "image_url": {"url": image.url}} for image in images]
37
+ return [
38
+ cast(ChatCompletionContentPartParam, {"type": "image_url", "image_url": {"url": image.url}}) for image in images
39
+ ]
36
40
 
37
41
 
38
42
  def convert_history_to_input(
@@ -42,19 +46,21 @@ def convert_history_to_input(
42
46
  ) -> list[chat.ChatCompletionMessageParam]:
43
47
  """Convert a list of messages to chat completion params."""
44
48
  del model_name
45
- messages: list[chat.ChatCompletionMessageParam] = [{"role": "system", "content": system}] if system else []
49
+ messages: list[chat.ChatCompletionMessageParam] = (
50
+ [cast(chat.ChatCompletionMessageParam, {"role": "system", "content": system})] if system else []
51
+ )
46
52
 
47
53
  for msg, attachment in attach_developer_messages(history):
48
54
  match msg:
49
55
  case message.SystemMessage():
50
56
  system_text = "\n".join(part.text for part in msg.parts)
51
57
  if system_text:
52
- messages.append({"role": "system", "content": system_text})
58
+ messages.append(cast(chat.ChatCompletionMessageParam, {"role": "system", "content": system_text}))
53
59
  case message.UserMessage():
54
60
  parts = build_chat_content_parts(msg, attachment)
55
- messages.append({"role": "user", "content": parts})
61
+ messages.append(cast(chat.ChatCompletionMessageParam, {"role": "user", "content": parts}))
56
62
  case message.ToolResultMessage():
57
- messages.append(build_tool_message(msg, attachment))
63
+ messages.append(cast(chat.ChatCompletionMessageParam, build_tool_message(msg, attachment)))
58
64
  case message.AssistantMessage():
59
65
  messages.append(_assistant_message_to_openai(msg))
60
66
  case _:
@@ -69,13 +75,16 @@ def convert_tool_schema(
69
75
  if tools is None:
70
76
  return []
71
77
  return [
72
- {
73
- "type": "function",
74
- "function": {
75
- "name": tool.name,
76
- "description": tool.description,
77
- "parameters": tool.parameters,
78
+ cast(
79
+ chat.ChatCompletionToolParam,
80
+ {
81
+ "type": "function",
82
+ "function": {
83
+ "name": tool.name,
84
+ "description": tool.description,
85
+ "parameters": tool.parameters,
86
+ },
78
87
  },
79
- }
88
+ )
80
89
  for tool in tools
81
90
  ]
@@ -179,7 +179,7 @@ async def parse_chat_completions_stream(
179
179
  """
180
180
 
181
181
  state = StreamStateManager(
182
- param_model=str(param.model),
182
+ param_model=str(param.model_id),
183
183
  reasoning_flusher=reasoning_handler.flush,
184
184
  )
185
185
 
@@ -30,7 +30,7 @@ def build_payload(
30
30
  param: llm_param.LLMCallParameter,
31
31
  ) -> tuple[CompletionCreateParamsStreaming, dict[str, object], dict[str, str]]:
32
32
  """Build OpenRouter API request parameters."""
33
- messages = convert_history_to_input(param.input, param.system, param.model)
33
+ messages = convert_history_to_input(param.input, param.system, param.model_id)
34
34
  tools = convert_tool_schema(param.tools)
35
35
 
36
36
  extra_body: dict[str, object] = {
@@ -66,13 +66,13 @@ def build_payload(
66
66
  if param.provider_routing:
67
67
  extra_body["provider"] = param.provider_routing.model_dump(exclude_none=True)
68
68
 
69
- if is_claude_model(param.model):
69
+ if is_claude_model(param.model_id):
70
70
  extra_headers["x-anthropic-beta"] = (
71
71
  f"{ANTHROPIC_BETA_FINE_GRAINED_TOOL_STREAMING},{ANTHROPIC_BETA_INTERLEAVED_THINKING}"
72
72
  )
73
73
 
74
74
  payload: CompletionCreateParamsStreaming = {
75
- "model": str(param.model),
75
+ "model": str(param.model_id),
76
76
  "tool_choice": "auto",
77
77
  "parallel_tool_calls": True,
78
78
  "stream": True,
@@ -133,7 +133,7 @@ class OpenRouterClient(LLMClientABC):
133
133
  return
134
134
 
135
135
  reasoning_handler = ReasoningStreamHandler(
136
- param_model=str(param.model),
136
+ param_model=str(param.model_id),
137
137
  response_id=None,
138
138
  )
139
139
 
@@ -6,6 +6,8 @@
6
6
  # pyright: reportUnnecessaryIsInstance=false
7
7
  # pyright: reportGeneralTypeIssues=false
8
8
 
9
+ from typing import cast
10
+
9
11
  from openai.types import chat
10
12
 
11
13
  from klaude_code.llm.image import assistant_image_to_data_url
@@ -71,7 +73,7 @@ def _assistant_message_to_openrouter(
71
73
  if content_parts:
72
74
  assistant_message["content"] = "\n".join(content_parts)
73
75
 
74
- return assistant_message
76
+ return cast(chat.ChatCompletionMessageParam, assistant_message)
75
77
 
76
78
 
77
79
  def _add_cache_control(messages: list[chat.ChatCompletionMessageParam], use_cache_control: bool) -> None:
@@ -98,19 +100,22 @@ def convert_history_to_input(
98
100
 
99
101
  messages: list[chat.ChatCompletionMessageParam] = (
100
102
  [
101
- {
102
- "role": "system",
103
- "content": [
104
- {
105
- "type": "text",
106
- "text": system,
107
- "cache_control": {"type": "ephemeral"},
108
- }
109
- ],
110
- }
103
+ cast(
104
+ chat.ChatCompletionMessageParam,
105
+ {
106
+ "role": "system",
107
+ "content": [
108
+ {
109
+ "type": "text",
110
+ "text": system,
111
+ "cache_control": {"type": "ephemeral"},
112
+ }
113
+ ],
114
+ },
115
+ )
111
116
  ]
112
117
  if system and use_cache_control
113
- else ([{"role": "system", "content": system}] if system else [])
118
+ else ([cast(chat.ChatCompletionMessageParam, {"role": "system", "content": system})] if system else [])
114
119
  )
115
120
 
116
121
  for msg, attachment in attach_developer_messages(history):
@@ -120,24 +125,29 @@ def convert_history_to_input(
120
125
  if system_text:
121
126
  if use_cache_control:
122
127
  messages.append(
123
- {
124
- "role": "system",
125
- "content": [
126
- {
127
- "type": "text",
128
- "text": system_text,
129
- "cache_control": {"type": "ephemeral"},
130
- }
131
- ],
132
- }
128
+ cast(
129
+ chat.ChatCompletionMessageParam,
130
+ {
131
+ "role": "system",
132
+ "content": [
133
+ {
134
+ "type": "text",
135
+ "text": system_text,
136
+ "cache_control": {"type": "ephemeral"},
137
+ }
138
+ ],
139
+ },
140
+ )
133
141
  )
134
142
  else:
135
- messages.append({"role": "system", "content": system_text})
143
+ messages.append(
144
+ cast(chat.ChatCompletionMessageParam, {"role": "system", "content": system_text})
145
+ )
136
146
  case message.UserMessage():
137
147
  parts = build_chat_content_parts(msg, attachment)
138
- messages.append({"role": "user", "content": parts})
148
+ messages.append(cast(chat.ChatCompletionMessageParam, {"role": "user", "content": parts}))
139
149
  case message.ToolResultMessage():
140
- messages.append(build_tool_message(msg, attachment))
150
+ messages.append(cast(chat.ChatCompletionMessageParam, build_tool_message(msg, attachment)))
141
151
  case message.AssistantMessage():
142
152
  messages.append(_assistant_message_to_openrouter(msg, model_name))
143
153
  case _:
@@ -24,11 +24,11 @@ if TYPE_CHECKING:
24
24
 
25
25
  def build_payload(param: llm_param.LLMCallParameter) -> ResponseCreateParamsStreaming:
26
26
  """Build OpenAI Responses API request parameters."""
27
- inputs = convert_history_to_input(param.input, param.model)
27
+ inputs = convert_history_to_input(param.input, param.model_id)
28
28
  tools = convert_tool_schema(param.tools)
29
29
 
30
30
  payload: ResponseCreateParamsStreaming = {
31
- "model": str(param.model),
31
+ "model": str(param.model_id),
32
32
  "tool_choice": "auto",
33
33
  "parallel_tool_calls": True,
34
34
  "include": [
@@ -77,7 +77,7 @@ async def parse_responses_stream(
77
77
  assistant_parts.append(
78
78
  message.ThinkingTextPart(
79
79
  text="".join(accumulated_thinking),
80
- model_id=str(param.model),
80
+ model_id=str(param.model_id),
81
81
  )
82
82
  )
83
83
  accumulated_thinking.clear()
@@ -85,7 +85,7 @@ async def parse_responses_stream(
85
85
  assistant_parts.append(
86
86
  message.ThinkingSignaturePart(
87
87
  signature=pending_signature,
88
- model_id=str(param.model),
88
+ model_id=str(param.model_id),
89
89
  format="openai_reasoning",
90
90
  )
91
91
  )
@@ -197,7 +197,7 @@ async def parse_responses_stream(
197
197
  max_tokens=param.max_tokens,
198
198
  )
199
199
  )
200
- metadata_tracker.set_model_name(str(param.model))
200
+ metadata_tracker.set_model_name(str(param.model_id))
201
201
  metadata_tracker.set_response_id(response_id)
202
202
  stop_reason = map_stop_reason(event.response.status, error_reason)
203
203
  if event.response.status != "completed":