klaude-code 2.5.1__py3-none-any.whl → 2.5.3__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 (58) hide show
  1. klaude_code/.DS_Store +0 -0
  2. klaude_code/cli/auth_cmd.py +2 -13
  3. klaude_code/cli/cost_cmd.py +10 -10
  4. klaude_code/cli/list_model.py +8 -0
  5. klaude_code/cli/main.py +41 -8
  6. klaude_code/cli/session_cmd.py +2 -11
  7. klaude_code/config/assets/builtin_config.yaml +45 -26
  8. klaude_code/config/config.py +30 -7
  9. klaude_code/config/model_matcher.py +3 -3
  10. klaude_code/config/sub_agent_model_helper.py +1 -1
  11. klaude_code/const.py +2 -1
  12. klaude_code/core/agent_profile.py +1 -0
  13. klaude_code/core/executor.py +4 -0
  14. klaude_code/core/loaded_skills.py +36 -0
  15. klaude_code/core/tool/context.py +1 -3
  16. klaude_code/core/tool/file/edit_tool.py +1 -1
  17. klaude_code/core/tool/file/read_tool.py +2 -2
  18. klaude_code/core/tool/file/write_tool.py +1 -1
  19. klaude_code/core/turn.py +19 -7
  20. klaude_code/llm/anthropic/client.py +97 -60
  21. klaude_code/llm/anthropic/input.py +20 -9
  22. klaude_code/llm/google/client.py +223 -148
  23. klaude_code/llm/google/input.py +44 -36
  24. klaude_code/llm/openai_compatible/stream.py +109 -99
  25. klaude_code/llm/openrouter/reasoning.py +4 -29
  26. klaude_code/llm/partial_message.py +2 -32
  27. klaude_code/llm/responses/client.py +99 -81
  28. klaude_code/llm/responses/input.py +11 -25
  29. klaude_code/llm/stream_parts.py +94 -0
  30. klaude_code/log.py +57 -0
  31. klaude_code/protocol/events/system.py +3 -0
  32. klaude_code/protocol/llm_param.py +1 -0
  33. klaude_code/session/export.py +259 -91
  34. klaude_code/session/templates/export_session.html +141 -59
  35. klaude_code/skill/.DS_Store +0 -0
  36. klaude_code/skill/assets/.DS_Store +0 -0
  37. klaude_code/skill/loader.py +1 -0
  38. klaude_code/tui/command/fork_session_cmd.py +14 -23
  39. klaude_code/tui/command/model_picker.py +2 -17
  40. klaude_code/tui/command/refresh_cmd.py +2 -0
  41. klaude_code/tui/command/resume_cmd.py +2 -18
  42. klaude_code/tui/command/sub_agent_model_cmd.py +5 -19
  43. klaude_code/tui/command/thinking_cmd.py +2 -14
  44. klaude_code/tui/components/common.py +1 -1
  45. klaude_code/tui/components/metadata.py +22 -21
  46. klaude_code/tui/components/rich/markdown.py +8 -0
  47. klaude_code/tui/components/rich/quote.py +36 -8
  48. klaude_code/tui/components/rich/theme.py +2 -0
  49. klaude_code/tui/components/welcome.py +32 -0
  50. klaude_code/tui/input/prompt_toolkit.py +3 -1
  51. klaude_code/tui/machine.py +19 -1
  52. klaude_code/tui/renderer.py +3 -4
  53. klaude_code/tui/terminal/selector.py +174 -31
  54. {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/METADATA +1 -1
  55. {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/RECORD +57 -53
  56. klaude_code/skill/assets/jj-workspace/SKILL.md +0 -20
  57. {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/WHEEL +0 -0
  58. {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from collections.abc import AsyncGenerator
3
- from typing import TYPE_CHECKING, Literal, override
3
+ from typing import TYPE_CHECKING, override
4
4
 
5
5
  import httpx
6
6
  import openai
@@ -11,9 +11,14 @@ from openai.types.responses.response_create_params import ResponseCreateParamsSt
11
11
  from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
12
12
  from klaude_code.llm.client import LLMClientABC, LLMStreamABC
13
13
  from klaude_code.llm.input_common import apply_config_defaults
14
- from klaude_code.llm.partial_message import degrade_thinking_to_text
15
14
  from klaude_code.llm.registry import register
16
15
  from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
16
+ from klaude_code.llm.stream_parts import (
17
+ append_text_part,
18
+ append_thinking_text_part,
19
+ build_partial_message,
20
+ build_partial_parts,
21
+ )
17
22
  from klaude_code.llm.usage import MetadataTracker, error_llm_stream
18
23
  from klaude_code.log import DebugType, log_debug
19
24
  from klaude_code.protocol import llm_param, message, model
@@ -58,68 +63,82 @@ def build_payload(param: llm_param.LLMCallParameter) -> ResponseCreateParamsStre
58
63
 
59
64
 
60
65
  class ResponsesStreamStateManager:
61
- """Manages streaming state for Responses API and provides partial message access."""
66
+ """Manages streaming state for Responses API and provides partial message access.
67
+
68
+ Accumulates parts directly during streaming to support get_partial_message()
69
+ for cancellation scenarios. Merges consecutive text parts of the same type.
70
+ Each reasoning summary is kept as a separate ThinkingTextPart.
71
+ """
62
72
 
63
73
  def __init__(self, model_id: str) -> None:
64
74
  self.model_id = model_id
65
75
  self.response_id: str | None = None
66
- self.stage: Literal["waiting", "thinking", "assistant", "tool"] = "waiting"
67
- self.accumulated_thinking: list[str] = []
68
- self.accumulated_text: list[str] = []
69
- self.pending_signature: str | None = None
70
76
  self.assistant_parts: list[message.Part] = []
71
77
  self.stop_reason: model.StopReason | None = None
72
-
73
- def flush_thinking(self) -> None:
74
- """Flush accumulated thinking content into parts."""
75
- if self.accumulated_thinking:
76
- self.assistant_parts.append(
77
- message.ThinkingTextPart(
78
- text="".join(self.accumulated_thinking),
79
- model_id=self.model_id,
80
- )
78
+ self._new_thinking_part: bool = True # Start fresh for first thinking part
79
+ self._summary_count: int = 0 # Track number of summary parts seen
80
+
81
+ def start_new_thinking_part(self) -> bool:
82
+ """Mark that the next thinking text should create a new ThinkingTextPart.
83
+
84
+ Returns True if this is not the first summary part (needs separator).
85
+ """
86
+ self._new_thinking_part = True
87
+ needs_separator = self._summary_count > 0
88
+ self._summary_count += 1
89
+ return needs_separator
90
+
91
+ def append_thinking_text(self, text: str) -> None:
92
+ """Append thinking text, merging with previous ThinkingTextPart if in same summary."""
93
+ if (
94
+ append_thinking_text_part(
95
+ self.assistant_parts,
96
+ text,
97
+ model_id=self.model_id,
98
+ force_new=self._new_thinking_part,
81
99
  )
82
- self.accumulated_thinking.clear()
83
- if self.pending_signature:
84
- self.assistant_parts.append(
85
- message.ThinkingSignaturePart(
86
- signature=self.pending_signature,
87
- model_id=self.model_id,
88
- format="openai_reasoning",
89
- )
100
+ is not None
101
+ ):
102
+ self._new_thinking_part = False
103
+
104
+ def append_text(self, text: str) -> None:
105
+ """Append text, merging with previous TextPart if possible."""
106
+ append_text_part(self.assistant_parts, text)
107
+
108
+ def append_thinking_signature(self, signature: str) -> None:
109
+ """Append a ThinkingSignaturePart after the current part."""
110
+ self.assistant_parts.append(
111
+ message.ThinkingSignaturePart(
112
+ signature=signature,
113
+ model_id=self.model_id,
114
+ format="openai-responses",
90
115
  )
91
- self.pending_signature = None
116
+ )
92
117
 
93
- def flush_text(self) -> None:
94
- """Flush accumulated text content into parts."""
95
- if not self.accumulated_text:
96
- return
97
- self.assistant_parts.append(message.TextPart(text="".join(self.accumulated_text)))
98
- self.accumulated_text.clear()
118
+ def append_tool_call(self, call_id: str, item_id: str | None, name: str, arguments_json: str) -> None:
119
+ """Append a ToolCallPart."""
120
+ self.assistant_parts.append(
121
+ message.ToolCallPart(
122
+ call_id=call_id,
123
+ id=item_id,
124
+ tool_name=name,
125
+ arguments_json=arguments_json,
126
+ )
127
+ )
99
128
 
100
- def flush_all(self) -> list[message.Part]:
101
- """Flush all accumulated content and return parts."""
102
- self.flush_thinking()
103
- self.flush_text()
104
- return list(self.assistant_parts)
129
+ def get_partial_parts(self) -> list[message.Part]:
130
+ """Get accumulated parts excluding tool calls, with thinking degraded.
131
+
132
+ Filters out ToolCallPart and applies degrade_thinking_to_text.
133
+ """
134
+ return build_partial_parts(self.assistant_parts)
105
135
 
106
136
  def get_partial_message(self) -> message.AssistantMessage | None:
107
- """Build a partial AssistantMessage from accumulated state."""
108
- parts = self.flush_all()
109
- filtered_parts: list[message.Part] = []
110
- for part in parts:
111
- if isinstance(part, message.ToolCallPart):
112
- continue
113
- filtered_parts.append(part)
114
-
115
- filtered_parts = degrade_thinking_to_text(filtered_parts)
116
- if not filtered_parts:
117
- return None
118
- return message.AssistantMessage(
119
- parts=filtered_parts,
120
- response_id=self.response_id,
121
- stop_reason="aborted",
122
- )
137
+ """Build a partial AssistantMessage from accumulated state.
138
+
139
+ Returns None if no content has been accumulated yet.
140
+ """
141
+ return build_partial_message(self.assistant_parts, response_id=self.response_id)
123
142
 
124
143
 
125
144
  async def parse_responses_stream(
@@ -157,24 +176,28 @@ async def parse_responses_stream(
157
176
  match event:
158
177
  case responses.ResponseCreatedEvent() as event:
159
178
  state.response_id = event.response.id
179
+ case responses.ResponseReasoningSummaryPartAddedEvent():
180
+ # New reasoning summary part started, ensure it becomes a new ThinkingTextPart
181
+ needs_separator = state.start_new_thinking_part()
182
+ if needs_separator:
183
+ # Add blank lines between summary parts for visual separation
184
+ yield message.ThinkingTextDelta(content=" \n \n", response_id=state.response_id)
160
185
  case responses.ResponseReasoningSummaryTextDeltaEvent() as event:
161
186
  if event.delta:
162
187
  metadata_tracker.record_token()
163
- if state.stage == "assistant":
164
- state.flush_text()
165
- state.stage = "thinking"
166
- state.accumulated_thinking.append(event.delta)
188
+ state.append_thinking_text(event.delta)
167
189
  yield message.ThinkingTextDelta(content=event.delta, response_id=state.response_id)
168
190
  case responses.ResponseReasoningSummaryTextDoneEvent() as event:
169
- if event.text and not state.accumulated_thinking:
170
- state.accumulated_thinking.append(event.text)
191
+ # Fallback: if no delta was received but done has full text, use it
192
+ if event.text:
193
+ # Check if we already have content for this summary by seeing if last part matches
194
+ last_part = state.assistant_parts[-1] if state.assistant_parts else None
195
+ if not isinstance(last_part, message.ThinkingTextPart) or not last_part.text:
196
+ state.append_thinking_text(event.text)
171
197
  case responses.ResponseTextDeltaEvent() as event:
172
198
  if event.delta:
173
199
  metadata_tracker.record_token()
174
- if state.stage == "thinking":
175
- state.flush_thinking()
176
- state.stage = "assistant"
177
- state.accumulated_text.append(event.delta)
200
+ state.append_text(event.delta)
178
201
  yield message.AssistantTextDelta(content=event.delta, response_id=state.response_id)
179
202
  case responses.ResponseOutputItemAddedEvent() as event:
180
203
  if isinstance(event.item, responses.ResponseFunctionToolCall):
@@ -188,30 +211,23 @@ async def parse_responses_stream(
188
211
  match event.item:
189
212
  case responses.ResponseReasoningItem() as item:
190
213
  if item.encrypted_content:
191
- state.pending_signature = item.encrypted_content
214
+ state.append_thinking_signature(item.encrypted_content)
192
215
  case responses.ResponseOutputMessage() as item:
193
- if not state.accumulated_text:
216
+ # Fallback: if no text delta was received, extract from final message
217
+ has_text = any(isinstance(p, message.TextPart) for p in state.assistant_parts)
218
+ if not has_text:
194
219
  text_content = "\n".join(
195
- [
196
- part.text
197
- for part in item.content
198
- if isinstance(part, responses.ResponseOutputText)
199
- ]
220
+ part.text for part in item.content if isinstance(part, responses.ResponseOutputText)
200
221
  )
201
222
  if text_content:
202
- state.accumulated_text.append(text_content)
223
+ state.append_text(text_content)
203
224
  case responses.ResponseFunctionToolCall() as item:
204
225
  metadata_tracker.record_token()
205
- state.flush_thinking()
206
- state.flush_text()
207
- state.stage = "tool"
208
- state.assistant_parts.append(
209
- message.ToolCallPart(
210
- call_id=item.call_id,
211
- id=item.id,
212
- tool_name=item.name,
213
- arguments_json=item.arguments.strip(),
214
- )
226
+ state.append_tool_call(
227
+ call_id=item.call_id,
228
+ item_id=item.id,
229
+ name=item.name,
230
+ arguments_json=item.arguments.strip(),
215
231
  )
216
232
  case _:
217
233
  pass
@@ -254,10 +270,12 @@ async def parse_responses_stream(
254
270
  )
255
271
  except (openai.OpenAIError, httpx.HTTPError) as e:
256
272
  yield message.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
273
+ state.stop_reason = "error"
257
274
 
258
- parts = state.flush_all()
259
275
  metadata_tracker.set_response_id(state.response_id)
260
276
  metadata = metadata_tracker.finalize()
277
+ # On error, use partial parts (excluding incomplete tool calls) for potential prefill on retry
278
+ parts = state.get_partial_parts() if state.stop_reason == "error" else list(state.assistant_parts)
261
279
  yield message.AssistantMessage(
262
280
  parts=parts,
263
281
  response_id=state.response_id,
@@ -80,8 +80,6 @@ def convert_history_to_input(
80
80
  """Convert a list of messages to response input params."""
81
81
  items: list[responses.ResponseInputItemParam] = []
82
82
 
83
- degraded_thinking_texts: list[str] = []
84
-
85
83
  for msg, attachment in attach_developer_messages(history):
86
84
  match msg:
87
85
  case message.SystemMessage():
@@ -116,12 +114,19 @@ def convert_history_to_input(
116
114
  case message.ToolResultMessage():
117
115
  items.append(_build_tool_result_item(msg, attachment))
118
116
  case message.AssistantMessage():
119
- assistant_text_parts: list[responses.ResponseInputContentParam] = []
117
+ assistant_text_parts: list[responses.ResponseOutputTextParam] = []
120
118
  pending_thinking_text: str | None = None
121
119
  pending_signature: str | None = None
122
120
  native_thinking_parts, degraded_for_message = split_thinking_parts(msg, model_name)
123
121
  native_thinking_ids = {id(part) for part in native_thinking_parts}
124
- degraded_thinking_texts.extend(degraded_for_message)
122
+ if degraded_for_message:
123
+ degraded_text = "<thinking>\n" + "\n".join(degraded_for_message) + "\n</thinking>"
124
+ assistant_text_parts.append(
125
+ cast(
126
+ responses.ResponseOutputTextParam,
127
+ {"type": "output_text", "text": degraded_text},
128
+ )
129
+ )
125
130
 
126
131
  def flush_text() -> None:
127
132
  nonlocal assistant_text_parts
@@ -164,8 +169,8 @@ def convert_history_to_input(
164
169
  if isinstance(part, message.TextPart):
165
170
  assistant_text_parts.append(
166
171
  cast(
167
- responses.ResponseInputContentParam,
168
- {"type": "input_text", "text": part.text},
172
+ responses.ResponseOutputTextParam,
173
+ {"type": "output_text", "text": part.text},
169
174
  )
170
175
  )
171
176
  elif isinstance(part, message.ToolCallPart):
@@ -188,25 +193,6 @@ def convert_history_to_input(
188
193
  case _:
189
194
  continue
190
195
 
191
- if degraded_thinking_texts:
192
- degraded_item = cast(
193
- responses.ResponseInputItemParam,
194
- {
195
- "type": "message",
196
- "role": "assistant",
197
- "content": [
198
- cast(
199
- responses.ResponseInputContentParam,
200
- {
201
- "type": "input_text",
202
- "text": "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>",
203
- },
204
- )
205
- ],
206
- },
207
- )
208
- items.insert(0, degraded_item)
209
-
210
196
  return items
211
197
 
212
198
 
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableSequence
4
+
5
+ from klaude_code.protocol import message
6
+
7
+
8
+ def append_text_part(parts: MutableSequence[message.Part], text: str) -> int | None:
9
+ if not text:
10
+ return None
11
+
12
+ if parts:
13
+ last = parts[-1]
14
+ if isinstance(last, message.TextPart):
15
+ parts[-1] = message.TextPart(text=last.text + text)
16
+ return len(parts) - 1
17
+
18
+ parts.append(message.TextPart(text=text))
19
+ return len(parts) - 1
20
+
21
+
22
+ def append_thinking_text_part(
23
+ parts: MutableSequence[message.Part],
24
+ text: str,
25
+ *,
26
+ model_id: str,
27
+ force_new: bool = False,
28
+ ) -> int | None:
29
+ if not text:
30
+ return None
31
+
32
+ if not force_new and parts:
33
+ last = parts[-1]
34
+ if isinstance(last, message.ThinkingTextPart):
35
+ parts[-1] = message.ThinkingTextPart(
36
+ text=last.text + text,
37
+ model_id=model_id,
38
+ )
39
+ return len(parts) - 1
40
+
41
+ parts.append(message.ThinkingTextPart(text=text, model_id=model_id))
42
+ return len(parts) - 1
43
+
44
+
45
+ def degrade_thinking_to_text(parts: list[message.Part]) -> list[message.Part]:
46
+ """Degrade thinking parts into a regular TextPart.
47
+
48
+ Some providers require thinking signatures/encrypted content to be echoed back
49
+ for subsequent calls. During interruption we cannot reliably determine whether
50
+ we have a complete signature, so we persist thinking as plain text instead.
51
+ """
52
+
53
+ thinking_texts: list[str] = []
54
+ non_thinking_parts: list[message.Part] = []
55
+
56
+ for part in parts:
57
+ if isinstance(part, message.ThinkingTextPart):
58
+ text = part.text
59
+ if text and text.strip():
60
+ thinking_texts.append(text)
61
+ continue
62
+ if isinstance(part, message.ThinkingSignaturePart):
63
+ continue
64
+ non_thinking_parts.append(part)
65
+
66
+ if not thinking_texts:
67
+ return non_thinking_parts
68
+
69
+ joined = "\n".join(thinking_texts).strip()
70
+ thinking_block = f"<thinking>\n{joined}\n</thinking>"
71
+ if non_thinking_parts:
72
+ thinking_block += "\n\n"
73
+
74
+ return [message.TextPart(text=thinking_block), *non_thinking_parts]
75
+
76
+
77
+ def build_partial_parts(parts: list[message.Part]) -> list[message.Part]:
78
+ filtered_parts: list[message.Part] = [p for p in parts if not isinstance(p, message.ToolCallPart)]
79
+ return degrade_thinking_to_text(filtered_parts)
80
+
81
+
82
+ def build_partial_message(
83
+ parts: list[message.Part],
84
+ *,
85
+ response_id: str | None,
86
+ ) -> message.AssistantMessage | None:
87
+ partial_parts = build_partial_parts(parts)
88
+ if not partial_parts:
89
+ return None
90
+ return message.AssistantMessage(
91
+ parts=partial_parts,
92
+ response_id=response_id,
93
+ stop_reason="aborted",
94
+ )
klaude_code/log.py CHANGED
@@ -1,13 +1,16 @@
1
1
  import gzip
2
+ import json
2
3
  import logging
3
4
  import os
4
5
  import shutil
5
6
  import subprocess
7
+ from base64 import b64encode
6
8
  from collections.abc import Iterable
7
9
  from datetime import datetime, timedelta
8
10
  from enum import Enum
9
11
  from logging.handlers import RotatingFileHandler
10
12
  from pathlib import Path
13
+ from typing import cast
11
14
 
12
15
  from rich.console import Console
13
16
  from rich.logging import RichHandler
@@ -316,3 +319,57 @@ def _trash_path(path: Path) -> None:
316
319
  )
317
320
  except FileNotFoundError:
318
321
  path.unlink(missing_ok=True)
322
+
323
+
324
+ # Debug JSON serialization utilities
325
+ _DEBUG_TRUNCATE_PREFIX_CHARS = 96
326
+
327
+ # Keys whose values should be truncated (e.g., signatures, large payloads)
328
+ _TRUNCATE_KEYS = {"thought_signature", "thoughtSignature"}
329
+
330
+
331
+ def _truncate_debug_str(value: str, *, prefix_chars: int = _DEBUG_TRUNCATE_PREFIX_CHARS) -> str:
332
+ if len(value) <= prefix_chars:
333
+ return value
334
+ return f"{value[:prefix_chars]}...(truncated,len={len(value)})"
335
+
336
+
337
+ def _sanitize_debug_value(value: object) -> object:
338
+ if isinstance(value, (bytes, bytearray)):
339
+ encoded = b64encode(bytes(value)).decode("ascii")
340
+ return _truncate_debug_str(encoded)
341
+ if isinstance(value, str):
342
+ return value
343
+ if isinstance(value, list):
344
+ return [_sanitize_debug_value(v) for v in cast(list[object], value)]
345
+ if isinstance(value, dict):
346
+ return _sanitize_debug_dict(value) # type: ignore[arg-type]
347
+ return value
348
+
349
+
350
+ def _sanitize_debug_dict(obj: dict[object, object]) -> dict[object, object]:
351
+ sanitized: dict[object, object] = {}
352
+ for k, v in obj.items():
353
+ if k in _TRUNCATE_KEYS:
354
+ if isinstance(v, str):
355
+ sanitized[k] = _truncate_debug_str(v)
356
+ else:
357
+ sanitized[k] = _sanitize_debug_value(v)
358
+ continue
359
+ sanitized[k] = _sanitize_debug_value(v)
360
+
361
+ # Truncate inline image payloads (data field with mime_type indicates image blob)
362
+ if "data" in sanitized and ("mime_type" in sanitized or "mimeType" in sanitized):
363
+ data = sanitized.get("data")
364
+ if isinstance(data, str):
365
+ sanitized["data"] = _truncate_debug_str(data)
366
+ elif isinstance(data, (bytes, bytearray)):
367
+ encoded = b64encode(bytes(data)).decode("ascii")
368
+ sanitized["data"] = _truncate_debug_str(encoded)
369
+
370
+ return sanitized
371
+
372
+
373
+ def debug_json(value: object) -> str:
374
+ """Serialize a value to JSON for debug logging, truncating large payloads."""
375
+ return json.dumps(_sanitize_debug_value(value), ensure_ascii=False)
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from pydantic import Field
4
+
3
5
  from klaude_code.protocol import llm_param
4
6
  from klaude_code.protocol.events.chat import DeveloperMessageEvent, UserMessageEvent
5
7
  from klaude_code.protocol.events.lifecycle import TaskFinishEvent, TaskStartEvent, TurnStartEvent
@@ -14,6 +16,7 @@ class WelcomeEvent(Event):
14
16
  work_dir: str
15
17
  llm_config: llm_param.LLMConfigParameter
16
18
  show_klaude_code_info: bool = True
19
+ loaded_skills: dict[str, list[str]] = Field(default_factory=dict)
17
20
 
18
21
 
19
22
  class ErrorEvent(Event):
@@ -120,6 +120,7 @@ class LLMConfigProviderParameter(BaseModel):
120
120
 
121
121
  class LLMConfigModelParameter(BaseModel):
122
122
  model_id: str | None = None
123
+ disabled: bool = False
123
124
  temperature: float | None = None
124
125
  max_tokens: int | None = None
125
126
  context_limit: int | None = None