klaude-code 2.5.1__py3-none-any.whl → 2.5.2__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.
klaude_code/.DS_Store ADDED
Binary file
@@ -288,6 +288,14 @@ def _build_models_table(
288
288
  name = Text.assemble((prefix, ThemeKey.LINES), (model.model_name, "dim"))
289
289
  model_id = Text(model.model_id or "", style="dim")
290
290
  params = Text("(unavailable)", style="dim")
291
+ elif model.disabled:
292
+ name = Text.assemble(
293
+ (prefix, ThemeKey.LINES),
294
+ (model.model_name, "dim strike"),
295
+ (" (disabled)", "dim"),
296
+ )
297
+ model_id = Text(model.model_id or "", style="dim")
298
+ params = Text(" · ").join(_get_model_params_display(model))
291
299
  else:
292
300
  # Build role tags for this model
293
301
  roles: list[str] = []
klaude_code/cli/main.py CHANGED
@@ -192,7 +192,7 @@ def main_callback(
192
192
  if raw_model:
193
193
  matches = [
194
194
  m.selector
195
- for m in cfg.iter_model_entries()
195
+ for m in cfg.iter_model_entries(only_available=True, include_disabled=False)
196
196
  if (m.model_id or "").strip().lower() == raw_model.lower()
197
197
  ]
198
198
  if len(matches) == 1:
@@ -96,7 +96,6 @@ provider_list:
96
96
  - model_name: opus
97
97
  model_id: anthropic/claude-4.5-opus
98
98
  context_limit: 200000
99
- verbosity: high
100
99
  thinking:
101
100
  type: enabled
102
101
  budget_tokens: 2048
@@ -222,7 +221,6 @@ provider_list:
222
221
  - model_name: opus
223
222
  model_id: claude-opus-4-5-20251101
224
223
  context_limit: 200000
225
- verbosity: high
226
224
  thinking:
227
225
  type: enabled
228
226
  budget_tokens: 2048
@@ -332,11 +332,12 @@ class Config(BaseModel):
332
332
 
333
333
  raise ValueError(f"Unknown model: {model_name}")
334
334
 
335
- def iter_model_entries(self, only_available: bool = False) -> list[ModelEntry]:
335
+ def iter_model_entries(self, only_available: bool = False, include_disabled: bool = True) -> list[ModelEntry]:
336
336
  """Return all model entries with their provider names.
337
337
 
338
338
  Args:
339
339
  only_available: If True, only return models from providers with valid API keys.
340
+ include_disabled: If False, exclude models with disabled=True.
340
341
  """
341
342
  return [
342
343
  ModelEntry(
@@ -347,25 +348,26 @@ class Config(BaseModel):
347
348
  for provider in self.provider_list
348
349
  if not only_available or not provider.is_api_key_missing()
349
350
  for model in provider.model_list
351
+ if include_disabled or not model.disabled
350
352
  ]
351
353
 
352
354
  def has_available_image_model(self) -> bool:
353
355
  """Check if any image generation model is available."""
354
- for entry in self.iter_model_entries(only_available=True):
356
+ for entry in self.iter_model_entries(only_available=True, include_disabled=False):
355
357
  if entry.modalities and "image" in entry.modalities:
356
358
  return True
357
359
  return False
358
360
 
359
361
  def get_first_available_nano_banana_model(self) -> str | None:
360
362
  """Get the first available nano-banana model, or None."""
361
- for entry in self.iter_model_entries(only_available=True):
363
+ for entry in self.iter_model_entries(only_available=True, include_disabled=False):
362
364
  if "nano-banana" in entry.model_name:
363
365
  return entry.model_name
364
366
  return None
365
367
 
366
368
  def get_first_available_image_model(self) -> str | None:
367
369
  """Get the first available image generation model, or None."""
368
- for entry in self.iter_model_entries(only_available=True):
370
+ for entry in self.iter_model_entries(only_available=True, include_disabled=False):
369
371
  if entry.modalities and "image" in entry.modalities:
370
372
  return entry.model_name
371
373
  return None
@@ -435,11 +437,26 @@ def _get_builtin_config() -> Config:
435
437
  return Config(provider_list=providers, sub_agent_models=sub_agent_models)
436
438
 
437
439
 
440
+ def _merge_model(builtin: ModelConfig, user: ModelConfig) -> ModelConfig:
441
+ """Merge user model config with builtin model config.
442
+
443
+ Strategy: user values take precedence if explicitly set (not default).
444
+ This allows users to override specific fields (e.g., disabled=true)
445
+ without losing other builtin settings (e.g., model_id, max_tokens).
446
+ """
447
+ merged_data = builtin.model_dump()
448
+ user_data = user.model_dump(exclude_defaults=True, exclude={"model_name"})
449
+ for key, value in user_data.items():
450
+ if value is not None:
451
+ merged_data[key] = value
452
+ return ModelConfig.model_validate(merged_data)
453
+
454
+
438
455
  def _merge_provider(builtin: ProviderConfig, user: UserProviderConfig) -> ProviderConfig:
439
456
  """Merge user provider config with builtin provider config.
440
457
 
441
458
  Strategy:
442
- - model_list: merge by model_name, user models override builtin models with same name
459
+ - model_list: merge by model_name, user model fields override builtin fields
443
460
  - Other fields (api_key, base_url, etc.): user config takes precedence if set
444
461
  """
445
462
  # Merge model_list: builtin first, then user overrides/appends
@@ -447,7 +464,12 @@ def _merge_provider(builtin: ProviderConfig, user: UserProviderConfig) -> Provid
447
464
  for m in builtin.model_list:
448
465
  merged_models[m.model_name] = m
449
466
  for m in user.model_list:
450
- merged_models[m.model_name] = m
467
+ if m.model_name in merged_models:
468
+ # Merge with builtin model
469
+ merged_models[m.model_name] = _merge_model(merged_models[m.model_name], m)
470
+ else:
471
+ # New model from user
472
+ merged_models[m.model_name] = m
451
473
 
452
474
  # For other fields, use user values if explicitly set, otherwise use builtin
453
475
  # We check if user explicitly provided a value by comparing to defaults
@@ -578,7 +600,8 @@ def _load_config_cached() -> Config:
578
600
  def load_config() -> Config:
579
601
  """Load config from disk (builtin + user merged).
580
602
 
581
- Always returns a valid Config. Use config.iter_model_entries(only_available=True)
603
+ Always returns a valid Config. Use
604
+ ``config.iter_model_entries(only_available=True, include_disabled=False)``
582
605
  to check if any models are actually usable.
583
606
  """
584
607
  try:
@@ -48,9 +48,9 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
48
48
  """
49
49
  config = load_config()
50
50
 
51
- # Only show models from providers with valid API keys
51
+ # Only show models from providers with valid API keys, exclude disabled models
52
52
  models: list[ModelEntry] = sorted(
53
- config.iter_model_entries(only_available=True),
53
+ config.iter_model_entries(only_available=True, include_disabled=False),
54
54
  key=lambda m: (m.provider.lower(), m.model_name.lower()),
55
55
  )
56
56
 
@@ -180,7 +180,7 @@ class SubAgentModelHelper:
180
180
  - Returns all available models
181
181
  """
182
182
  profile = get_sub_agent_profile(sub_agent_type)
183
- all_models = self._config.iter_model_entries(only_available=True)
183
+ all_models = self._config.iter_model_entries(only_available=True, include_disabled=False)
184
184
 
185
185
  if profile.availability_requirement == AVAILABILITY_IMAGE_MODEL:
186
186
  return [m for m in all_models if m.modalities and "image" in m.modalities]
@@ -48,6 +48,7 @@ COMMAND_DESCRIPTIONS: dict[str, str] = {
48
48
  "fd": "simple and fast alternative to find",
49
49
  "tree": "directory listing as a tree",
50
50
  "sg": "ast-grep - AST-aware code search",
51
+ "jq": "command-line JSON processor",
51
52
  "jj": "jujutsu - Git-compatible version control system",
52
53
  }
53
54
 
@@ -18,6 +18,7 @@ from klaude_code.config import load_config
18
18
  from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
19
19
  from klaude_code.core.agent import Agent
20
20
  from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
21
+ from klaude_code.core.loaded_skills import get_loaded_skill_names_by_location
21
22
  from klaude_code.core.manager import LLMClients, SubAgentManager
22
23
  from klaude_code.llm.registry import create_llm_client
23
24
  from klaude_code.log import DebugType, log_debug
@@ -136,6 +137,7 @@ class AgentRuntime:
136
137
  session_id=session.id,
137
138
  work_dir=str(session.work_dir),
138
139
  llm_config=self._llm_clients.main.get_llm_config(),
140
+ loaded_skills=get_loaded_skill_names_by_location(),
139
141
  )
140
142
  )
141
143
 
@@ -192,6 +194,7 @@ class AgentRuntime:
192
194
  session_id=agent.session.id,
193
195
  work_dir=str(agent.session.work_dir),
194
196
  llm_config=self._llm_clients.main.get_llm_config(),
197
+ loaded_skills=get_loaded_skill_names_by_location(),
195
198
  )
196
199
  )
197
200
 
@@ -215,6 +218,7 @@ class AgentRuntime:
215
218
  session_id=target_session.id,
216
219
  work_dir=str(target_session.work_dir),
217
220
  llm_config=self._llm_clients.main.get_llm_config(),
221
+ loaded_skills=get_loaded_skill_names_by_location(),
218
222
  )
219
223
  )
220
224
 
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def get_loaded_skill_names_by_location() -> dict[str, list[str]]:
5
+ """Return loaded skill names grouped by location.
6
+
7
+ The UI should not import the skill system directly. Core can expose a
8
+ lightweight summary suitable for WelcomeEvent rendering.
9
+ """
10
+
11
+ try:
12
+ # Import lazily to keep startup overhead minimal and avoid unnecessary
13
+ # coupling at module import time.
14
+ from klaude_code.skill.manager import get_available_skills
15
+ except Exception:
16
+ return {}
17
+
18
+ result: dict[str, list[str]] = {"user": [], "project": [], "system": []}
19
+ try:
20
+ for name, _desc, location in get_available_skills():
21
+ if location == "user":
22
+ result["user"].append(name)
23
+ elif location == "project":
24
+ result["project"].append(name)
25
+ elif location == "system":
26
+ result["system"].append(name)
27
+ except Exception:
28
+ return {}
29
+
30
+ if not result["user"] and not result["project"] and not result["system"]:
31
+ return {}
32
+
33
+ result["user"].sort()
34
+ result["project"].sort()
35
+ result["system"].sort()
36
+ return result
@@ -89,7 +89,5 @@ class ToolContext:
89
89
  def with_record_sub_agent_session_id(self, callback: Callable[[str], None] | None) -> ToolContext:
90
90
  return replace(self, record_sub_agent_session_id=callback)
91
91
 
92
- def with_register_sub_agent_metadata_getter(
93
- self, callback: Callable[[GetMetadataFn], None] | None
94
- ) -> ToolContext:
92
+ def with_register_sub_agent_metadata_getter(self, callback: Callable[[GetMetadataFn], None] | None) -> ToolContext:
95
93
  return replace(self, register_sub_agent_metadata_getter=callback)
klaude_code/core/turn.py CHANGED
@@ -339,12 +339,6 @@ class TurnExecutor:
339
339
  )
340
340
  case message.StreamErrorItem() as msg:
341
341
  turn_result.stream_error = msg
342
- log_debug(
343
- "[StreamError]",
344
- msg.error,
345
- style="red",
346
- debug_type=DebugType.RESPONSE,
347
- )
348
342
  case message.ToolCallStartDelta() as msg:
349
343
  if thinking_active:
350
344
  thinking_active = False
@@ -282,8 +282,6 @@ async def parse_anthropic_stream(
282
282
  max_tokens=param.max_tokens,
283
283
  )
284
284
  )
285
- metadata_tracker.set_model_name(str(param.model_id))
286
- metadata_tracker.set_response_id(state.response_id)
287
285
  raw_stop_reason = getattr(event, "stop_reason", None)
288
286
  if isinstance(raw_stop_reason, str):
289
287
  state.stop_reason = _map_anthropic_stop_reason(raw_stop_reason)
@@ -293,6 +291,8 @@ async def parse_anthropic_stream(
293
291
  parts = state.flush_all()
294
292
  if parts:
295
293
  metadata_tracker.record_token()
294
+ metadata_tracker.set_model_name(str(param.model_id))
295
+ metadata_tracker.set_response_id(state.response_id)
296
296
  metadata = metadata_tracker.finalize()
297
297
  yield message.AssistantMessage(
298
298
  parts=parts,
@@ -322,15 +322,27 @@ class AnthropicLLMStream(LLMStreamABC):
322
322
  return self._iterate()
323
323
 
324
324
  async def _iterate(self) -> AsyncGenerator[message.LLMStreamItem]:
325
- async for item in parse_anthropic_stream(
326
- self._stream,
327
- self._param,
328
- self._metadata_tracker,
329
- self._state,
330
- ):
331
- if isinstance(item, message.AssistantMessage):
332
- self._completed = True
333
- yield item
325
+ try:
326
+ async for item in parse_anthropic_stream(
327
+ self._stream,
328
+ self._param,
329
+ self._metadata_tracker,
330
+ self._state,
331
+ ):
332
+ if isinstance(item, message.AssistantMessage):
333
+ self._completed = True
334
+ yield item
335
+ except (anthropic.AnthropicError, httpx.HTTPError) as e:
336
+ yield message.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
337
+ self._metadata_tracker.set_model_name(str(self._param.model_id))
338
+ self._metadata_tracker.set_response_id(self._state.response_id)
339
+ metadata = self._metadata_tracker.finalize()
340
+ yield message.AssistantMessage(
341
+ parts=[],
342
+ response_id=self._state.response_id,
343
+ usage=metadata,
344
+ stop_reason="error",
345
+ )
334
346
 
335
347
  def get_partial_message(self) -> message.AssistantMessage | None:
336
348
  if self._completed:
@@ -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