klaude-code 1.4.3__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/cli/main.py +22 -11
- klaude_code/cli/runtime.py +171 -34
- klaude_code/command/__init__.py +4 -0
- klaude_code/command/fork_session_cmd.py +220 -2
- klaude_code/command/help_cmd.py +2 -1
- klaude_code/command/model_cmd.py +3 -5
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/refresh_cmd.py +4 -4
- klaude_code/command/registry.py +23 -0
- klaude_code/command/resume_cmd.py +62 -2
- klaude_code/command/thinking_cmd.py +30 -199
- klaude_code/config/select_model.py +47 -97
- klaude_code/config/thinking.py +255 -0
- klaude_code/core/executor.py +53 -63
- klaude_code/llm/usage.py +1 -1
- klaude_code/protocol/commands.py +11 -0
- klaude_code/protocol/op.py +15 -0
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/selector.py +65 -65
- klaude_code/session/session.py +18 -12
- klaude_code/ui/modes/repl/completers.py +27 -15
- klaude_code/ui/modes/repl/event_handler.py +24 -33
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +393 -57
- klaude_code/ui/modes/repl/key_bindings.py +30 -10
- klaude_code/ui/modes/repl/renderer.py +1 -1
- klaude_code/ui/renderers/developer.py +2 -2
- klaude_code/ui/renderers/metadata.py +11 -6
- klaude_code/ui/renderers/user_input.py +18 -1
- klaude_code/ui/rich/markdown.py +41 -9
- klaude_code/ui/rich/status.py +83 -22
- klaude_code/ui/rich/theme.py +2 -2
- klaude_code/ui/terminal/notifier.py +42 -0
- klaude_code/ui/terminal/selector.py +488 -136
- {klaude_code-1.4.3.dist-info → klaude_code-1.6.0.dist-info}/METADATA +1 -1
- {klaude_code-1.4.3.dist-info → klaude_code-1.6.0.dist-info}/RECORD +37 -35
- {klaude_code-1.4.3.dist-info → klaude_code-1.6.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.4.3.dist-info → klaude_code-1.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
from dataclasses import dataclass
|
|
2
2
|
|
|
3
3
|
from klaude_code.config.config import ModelEntry, load_config, print_no_available_models_hint
|
|
4
4
|
from klaude_code.trace import log
|
|
@@ -17,15 +17,34 @@ def _normalize_model_key(value: str) -> str:
|
|
|
17
17
|
return "".join(ch for ch in value.casefold() if ch.isalnum())
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
@dataclass
|
|
21
|
+
class ModelMatchResult:
|
|
22
|
+
"""Result of model matching.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
matched_model: The single matched model name, or None if ambiguous/no match.
|
|
26
|
+
filtered_models: List of filtered models for interactive selection.
|
|
27
|
+
filter_hint: The filter hint to show (original preferred value), or None.
|
|
28
|
+
error_message: Error message if no models available, or None.
|
|
21
29
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
|
|
31
|
+
matched_model: str | None
|
|
32
|
+
filtered_models: list[ModelEntry]
|
|
33
|
+
filter_hint: str | None
|
|
34
|
+
error_message: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
|
|
38
|
+
"""Match model from config without interactive selection.
|
|
24
39
|
|
|
25
40
|
If preferred is provided:
|
|
26
|
-
- Exact match:
|
|
27
|
-
- Single partial match (case-insensitive):
|
|
28
|
-
-
|
|
41
|
+
- Exact match: returns matched_model
|
|
42
|
+
- Single partial match (case-insensitive): returns matched_model
|
|
43
|
+
- Multiple matches: returns filtered_models for interactive selection
|
|
44
|
+
- No matches: returns all models with filter_hint=None
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ModelMatchResult with match state.
|
|
29
48
|
"""
|
|
30
49
|
config = load_config()
|
|
31
50
|
|
|
@@ -36,17 +55,22 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
|
36
55
|
|
|
37
56
|
if not models:
|
|
38
57
|
print_no_available_models_hint()
|
|
39
|
-
return
|
|
58
|
+
return ModelMatchResult(
|
|
59
|
+
matched_model=None,
|
|
60
|
+
filtered_models=[],
|
|
61
|
+
filter_hint=None,
|
|
62
|
+
error_message="No models available",
|
|
63
|
+
)
|
|
40
64
|
|
|
41
65
|
names: list[str] = [m.model_name for m in models]
|
|
42
66
|
|
|
43
67
|
# Try to match preferred model name
|
|
44
|
-
|
|
68
|
+
filter_hint = preferred
|
|
45
69
|
if preferred and preferred.strip():
|
|
46
70
|
preferred = preferred.strip()
|
|
47
71
|
# Exact match
|
|
48
72
|
if preferred in names:
|
|
49
|
-
return preferred
|
|
73
|
+
return ModelMatchResult(matched_model=preferred, filtered_models=models, filter_hint=None)
|
|
50
74
|
|
|
51
75
|
preferred_lower = preferred.lower()
|
|
52
76
|
# Case-insensitive exact match (model_name or model_params.model)
|
|
@@ -56,7 +80,9 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
|
56
80
|
if preferred_lower == m.model_name.lower() or preferred_lower == (m.model_params.model or "").lower()
|
|
57
81
|
]
|
|
58
82
|
if len(exact_ci_matches) == 1:
|
|
59
|
-
return
|
|
83
|
+
return ModelMatchResult(
|
|
84
|
+
matched_model=exact_ci_matches[0].model_name, filtered_models=models, filter_hint=None
|
|
85
|
+
)
|
|
60
86
|
|
|
61
87
|
# Normalized matching (e.g. gpt52 == gpt-5.2, gpt52 in gpt-5.2-2025-...)
|
|
62
88
|
preferred_norm = _normalize_model_key(preferred)
|
|
@@ -69,7 +95,9 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
|
69
95
|
or preferred_norm == _normalize_model_key(m.model_params.model or "")
|
|
70
96
|
]
|
|
71
97
|
if len(normalized_matches) == 1:
|
|
72
|
-
return
|
|
98
|
+
return ModelMatchResult(
|
|
99
|
+
matched_model=normalized_matches[0].model_name, filtered_models=models, filter_hint=None
|
|
100
|
+
)
|
|
73
101
|
|
|
74
102
|
if not normalized_matches and len(preferred_norm) >= 4:
|
|
75
103
|
normalized_matches = [
|
|
@@ -79,7 +107,9 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
|
79
107
|
or preferred_norm in _normalize_model_key(m.model_params.model or "")
|
|
80
108
|
]
|
|
81
109
|
if len(normalized_matches) == 1:
|
|
82
|
-
return
|
|
110
|
+
return ModelMatchResult(
|
|
111
|
+
matched_model=normalized_matches[0].model_name, filtered_models=models, filter_hint=None
|
|
112
|
+
)
|
|
83
113
|
|
|
84
114
|
# Partial match (case-insensitive) on model_name or model_params.model.
|
|
85
115
|
# If normalized matching found candidates (even if multiple), prefer those as the filter set.
|
|
@@ -89,93 +119,13 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
|
89
119
|
if preferred_lower in m.model_name.lower() or preferred_lower in (m.model_params.model or "").lower()
|
|
90
120
|
]
|
|
91
121
|
if len(matches) == 1:
|
|
92
|
-
return matches[0].model_name
|
|
122
|
+
return ModelMatchResult(matched_model=matches[0].model_name, filtered_models=models, filter_hint=None)
|
|
93
123
|
if matches:
|
|
94
124
|
# Multiple matches: filter the list for interactive selection
|
|
95
|
-
|
|
125
|
+
return ModelMatchResult(matched_model=None, filtered_models=matches, filter_hint=filter_hint)
|
|
96
126
|
else:
|
|
97
127
|
# No matches: show all models without filter hint
|
|
98
|
-
preferred = None
|
|
99
128
|
log(("No matching models found. Showing all models.", "yellow"))
|
|
129
|
+
return ModelMatchResult(matched_model=None, filtered_models=models, filter_hint=None)
|
|
100
130
|
|
|
101
|
-
|
|
102
|
-
# If we couldn't resolve to a single model deterministically above, fail with a clear hint.
|
|
103
|
-
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
104
|
-
log(("Error: cannot use interactive model selection without a TTY", "red"))
|
|
105
|
-
log(("Hint: pass --model <config-name> or set main_model in ~/.klaude/klaude-config.yaml", "yellow"))
|
|
106
|
-
if preferred:
|
|
107
|
-
log((f"Hint: '{preferred}' did not resolve to a single configured model", "yellow"))
|
|
108
|
-
return None
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
from prompt_toolkit.styles import Style
|
|
112
|
-
|
|
113
|
-
from klaude_code.ui.terminal.selector import SelectItem, select_one
|
|
114
|
-
|
|
115
|
-
max_model_name_length = max(len(m.model_name) for m in filtered_models)
|
|
116
|
-
|
|
117
|
-
def _thinking_info(m: ModelEntry) -> str:
|
|
118
|
-
thinking = m.model_params.thinking
|
|
119
|
-
if not thinking:
|
|
120
|
-
return ""
|
|
121
|
-
if thinking.reasoning_effort:
|
|
122
|
-
return f"reasoning {thinking.reasoning_effort}"
|
|
123
|
-
if thinking.budget_tokens:
|
|
124
|
-
return f"thinking budget {thinking.budget_tokens}"
|
|
125
|
-
return "thinking (configured)"
|
|
126
|
-
|
|
127
|
-
items: list[SelectItem[str]] = []
|
|
128
|
-
for m in filtered_models:
|
|
129
|
-
model_id = m.model_params.model or "N/A"
|
|
130
|
-
first_line_prefix = f"{m.model_name:<{max_model_name_length}} → "
|
|
131
|
-
thinking_info = _thinking_info(m)
|
|
132
|
-
meta_parts: list[str] = [m.provider]
|
|
133
|
-
if thinking_info:
|
|
134
|
-
meta_parts.append(thinking_info)
|
|
135
|
-
if m.model_params.verbosity:
|
|
136
|
-
meta_parts.append(f"verbosity {m.model_params.verbosity}")
|
|
137
|
-
meta_str = " · ".join(meta_parts)
|
|
138
|
-
title = [
|
|
139
|
-
("class:msg", first_line_prefix),
|
|
140
|
-
("class:msg bold", model_id),
|
|
141
|
-
("class:meta", f" {meta_str}\n"),
|
|
142
|
-
]
|
|
143
|
-
search_text = f"{m.model_name} {model_id} {m.provider}"
|
|
144
|
-
items.append(SelectItem(title=title, value=m.model_name, search_text=search_text))
|
|
145
|
-
|
|
146
|
-
try:
|
|
147
|
-
message = f"Select a model (filtered by '{preferred}'):" if preferred else "Select a model:"
|
|
148
|
-
result = select_one(
|
|
149
|
-
message=message,
|
|
150
|
-
items=items,
|
|
151
|
-
pointer="→",
|
|
152
|
-
use_search_filter=True,
|
|
153
|
-
initial_value=config.main_model,
|
|
154
|
-
style=Style(
|
|
155
|
-
[
|
|
156
|
-
("pointer", "ansigreen"),
|
|
157
|
-
("highlighted", "ansigreen"),
|
|
158
|
-
("msg", ""),
|
|
159
|
-
("meta", "fg:ansibrightblack"),
|
|
160
|
-
("text", "ansibrightblack"),
|
|
161
|
-
("question", "bold"),
|
|
162
|
-
("search_prefix", "ansibrightblack"),
|
|
163
|
-
# search filter colors at the bottom
|
|
164
|
-
("search_success", "noinherit fg:ansigreen"),
|
|
165
|
-
("search_none", "noinherit fg:ansired"),
|
|
166
|
-
]
|
|
167
|
-
),
|
|
168
|
-
)
|
|
169
|
-
if isinstance(result, str) and result in names:
|
|
170
|
-
return result
|
|
171
|
-
except KeyboardInterrupt:
|
|
172
|
-
return None
|
|
173
|
-
except Exception as e:
|
|
174
|
-
log((f"Failed to use prompt_toolkit for model selection: {e}", "yellow"))
|
|
175
|
-
# Never return an unvalidated model name here.
|
|
176
|
-
# If we can't interactively select, fall back to a known configured model.
|
|
177
|
-
if isinstance(preferred, str) and preferred in names:
|
|
178
|
-
return preferred
|
|
179
|
-
if config.main_model and config.main_model in names:
|
|
180
|
-
return config.main_model
|
|
181
|
-
return None
|
|
131
|
+
return ModelMatchResult(matched_model=None, filtered_models=models, filter_hint=None)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Thinking level configuration data and helpers.
|
|
2
|
+
|
|
3
|
+
This module contains thinking level definitions and helper functions
|
|
4
|
+
that are shared between command layer and UI layer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from klaude_code.protocol import llm_param
|
|
11
|
+
|
|
12
|
+
ReasoningEffort = Literal["high", "medium", "low", "minimal", "none", "xhigh"]
|
|
13
|
+
|
|
14
|
+
# Thinking level options for different protocols
|
|
15
|
+
RESPONSES_LEVELS = ["low", "medium", "high"]
|
|
16
|
+
RESPONSES_GPT51_LEVELS = ["none", "low", "medium", "high"]
|
|
17
|
+
RESPONSES_GPT52_LEVELS = ["none", "low", "medium", "high", "xhigh"]
|
|
18
|
+
RESPONSES_CODEX_MAX_LEVELS = ["medium", "high", "xhigh"]
|
|
19
|
+
RESPONSES_GEMINI_FLASH_LEVELS = ["minimal", "low", "medium", "high"]
|
|
20
|
+
|
|
21
|
+
ANTHROPIC_LEVELS: list[tuple[str, int | None]] = [
|
|
22
|
+
("off", 0),
|
|
23
|
+
("low (2048 tokens)", 2048),
|
|
24
|
+
("medium (8192 tokens)", 8192),
|
|
25
|
+
("high (31999 tokens)", 31999),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_openrouter_model_with_reasoning_effort(model_name: str | None) -> bool:
|
|
30
|
+
"""Check if the model is GPT series, Grok or Gemini 3."""
|
|
31
|
+
if not model_name:
|
|
32
|
+
return False
|
|
33
|
+
model_lower = model_name.lower()
|
|
34
|
+
return model_lower.startswith(("openai/gpt-", "x-ai/grok-", "google/gemini-3"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_gpt51_model(model_name: str | None) -> bool:
|
|
38
|
+
"""Check if the model is GPT-5.1."""
|
|
39
|
+
if not model_name:
|
|
40
|
+
return False
|
|
41
|
+
return model_name.lower() in ["gpt-5.1", "openai/gpt-5.1", "gpt-5.1-codex-2025-11-13"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_gpt52_model(model_name: str | None) -> bool:
|
|
45
|
+
"""Check if the model is GPT-5.2."""
|
|
46
|
+
if not model_name:
|
|
47
|
+
return False
|
|
48
|
+
return model_name.lower() in ["gpt-5.2", "openai/gpt-5.2"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_codex_max_model(model_name: str | None) -> bool:
|
|
52
|
+
"""Check if the model is GPT-5.1-codex-max."""
|
|
53
|
+
if not model_name:
|
|
54
|
+
return False
|
|
55
|
+
return "codex-max" in model_name.lower()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_gemini_flash_model(model_name: str | None) -> bool:
|
|
59
|
+
"""Check if the model is Gemini 3 Flash."""
|
|
60
|
+
if not model_name:
|
|
61
|
+
return False
|
|
62
|
+
return "gemini-3-flash" in model_name.lower()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def should_auto_trigger_thinking(model_name: str | None) -> bool:
|
|
66
|
+
"""Check if model should auto-trigger thinking selection on switch."""
|
|
67
|
+
if not model_name:
|
|
68
|
+
return False
|
|
69
|
+
model_lower = model_name.lower()
|
|
70
|
+
return "gpt-5" in model_lower or "gemini-3" in model_lower or "opus" in model_lower
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_levels_for_responses(model_name: str | None) -> list[str]:
|
|
74
|
+
"""Get thinking levels for responses protocol."""
|
|
75
|
+
if _is_codex_max_model(model_name):
|
|
76
|
+
return RESPONSES_CODEX_MAX_LEVELS
|
|
77
|
+
if _is_gpt52_model(model_name):
|
|
78
|
+
return RESPONSES_GPT52_LEVELS
|
|
79
|
+
if _is_gpt51_model(model_name):
|
|
80
|
+
return RESPONSES_GPT51_LEVELS
|
|
81
|
+
if _is_gemini_flash_model(model_name):
|
|
82
|
+
return RESPONSES_GEMINI_FLASH_LEVELS
|
|
83
|
+
return RESPONSES_LEVELS
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
|
|
87
|
+
"""Format the current thinking configuration for display."""
|
|
88
|
+
thinking = config.thinking
|
|
89
|
+
if not thinking:
|
|
90
|
+
return "not configured"
|
|
91
|
+
|
|
92
|
+
protocol = config.protocol
|
|
93
|
+
|
|
94
|
+
if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
|
|
95
|
+
if thinking.reasoning_effort:
|
|
96
|
+
return f"reasoning_effort={thinking.reasoning_effort}"
|
|
97
|
+
return "not set"
|
|
98
|
+
|
|
99
|
+
if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
|
|
100
|
+
if thinking.type == "disabled":
|
|
101
|
+
return "off"
|
|
102
|
+
if thinking.type == "enabled":
|
|
103
|
+
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
104
|
+
return "not set"
|
|
105
|
+
|
|
106
|
+
if protocol == llm_param.LLMClientProtocol.OPENROUTER:
|
|
107
|
+
if is_openrouter_model_with_reasoning_effort(config.model):
|
|
108
|
+
if thinking.reasoning_effort:
|
|
109
|
+
return f"reasoning_effort={thinking.reasoning_effort}"
|
|
110
|
+
else:
|
|
111
|
+
if thinking.type == "disabled":
|
|
112
|
+
return "off"
|
|
113
|
+
if thinking.type == "enabled":
|
|
114
|
+
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
115
|
+
return "not set"
|
|
116
|
+
|
|
117
|
+
if protocol == llm_param.LLMClientProtocol.OPENAI:
|
|
118
|
+
if thinking.type == "disabled":
|
|
119
|
+
return "off"
|
|
120
|
+
if thinking.type == "enabled":
|
|
121
|
+
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
122
|
+
return "not set"
|
|
123
|
+
|
|
124
|
+
return "unknown protocol"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Thinking picker data structures
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class ThinkingOption:
|
|
134
|
+
"""A thinking option for selection.
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
label: Display label for this option (e.g., "low", "medium (8192 tokens)").
|
|
138
|
+
value: Encoded value string (e.g., "effort:low", "budget:2048").
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
label: str
|
|
142
|
+
value: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class ThinkingPickerData:
|
|
147
|
+
"""Data for building thinking picker UI.
|
|
148
|
+
|
|
149
|
+
Attributes:
|
|
150
|
+
options: List of thinking options.
|
|
151
|
+
message: Prompt message (e.g., "Select reasoning effort:").
|
|
152
|
+
current_value: Currently selected value, or None.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
options: list[ThinkingOption]
|
|
156
|
+
message: str
|
|
157
|
+
current_value: str | None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _build_effort_options(levels: list[str]) -> list[ThinkingOption]:
|
|
161
|
+
"""Build effort-based thinking options."""
|
|
162
|
+
return [ThinkingOption(label=level, value=f"effort:{level}") for level in levels]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _build_budget_options() -> list[ThinkingOption]:
|
|
166
|
+
"""Build budget-based thinking options."""
|
|
167
|
+
return [ThinkingOption(label=label, value=f"budget:{tokens or 0}") for label, tokens in ANTHROPIC_LEVELS]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _get_current_effort_value(thinking: llm_param.Thinking | None) -> str | None:
|
|
171
|
+
"""Get current value for effort-based thinking."""
|
|
172
|
+
if thinking and thinking.reasoning_effort:
|
|
173
|
+
return f"effort:{thinking.reasoning_effort}"
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _get_current_budget_value(thinking: llm_param.Thinking | None) -> str | None:
|
|
178
|
+
"""Get current value for budget-based thinking."""
|
|
179
|
+
if thinking:
|
|
180
|
+
if thinking.type == "disabled":
|
|
181
|
+
return "budget:0"
|
|
182
|
+
if thinking.budget_tokens:
|
|
183
|
+
return f"budget:{thinking.budget_tokens}"
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_thinking_picker_data(config: llm_param.LLMConfigParameter) -> ThinkingPickerData | None:
|
|
188
|
+
"""Get thinking picker data based on LLM config.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
ThinkingPickerData with options and current value, or None if protocol doesn't support thinking.
|
|
192
|
+
"""
|
|
193
|
+
protocol = config.protocol
|
|
194
|
+
model_name = config.model
|
|
195
|
+
thinking = config.thinking
|
|
196
|
+
|
|
197
|
+
if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
|
|
198
|
+
levels = get_levels_for_responses(model_name)
|
|
199
|
+
return ThinkingPickerData(
|
|
200
|
+
options=_build_effort_options(levels),
|
|
201
|
+
message="Select reasoning effort:",
|
|
202
|
+
current_value=_get_current_effort_value(thinking),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
|
|
206
|
+
return ThinkingPickerData(
|
|
207
|
+
options=_build_budget_options(),
|
|
208
|
+
message="Select thinking level:",
|
|
209
|
+
current_value=_get_current_budget_value(thinking),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if protocol == llm_param.LLMClientProtocol.OPENROUTER:
|
|
213
|
+
if is_openrouter_model_with_reasoning_effort(model_name):
|
|
214
|
+
levels = get_levels_for_responses(model_name)
|
|
215
|
+
return ThinkingPickerData(
|
|
216
|
+
options=_build_effort_options(levels),
|
|
217
|
+
message="Select reasoning effort:",
|
|
218
|
+
current_value=_get_current_effort_value(thinking),
|
|
219
|
+
)
|
|
220
|
+
return ThinkingPickerData(
|
|
221
|
+
options=_build_budget_options(),
|
|
222
|
+
message="Select thinking level:",
|
|
223
|
+
current_value=_get_current_budget_value(thinking),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if protocol == llm_param.LLMClientProtocol.OPENAI:
|
|
227
|
+
return ThinkingPickerData(
|
|
228
|
+
options=_build_budget_options(),
|
|
229
|
+
message="Select thinking level:",
|
|
230
|
+
current_value=_get_current_budget_value(thinking),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def parse_thinking_value(value: str) -> llm_param.Thinking | None:
|
|
237
|
+
"""Parse a thinking value string into a Thinking object.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
value: Encoded value string (e.g., "effort:low", "budget:2048").
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Thinking object, or None if invalid format.
|
|
244
|
+
"""
|
|
245
|
+
if value.startswith("effort:"):
|
|
246
|
+
effort = value[7:]
|
|
247
|
+
return llm_param.Thinking(reasoning_effort=effort) # type: ignore[arg-type]
|
|
248
|
+
|
|
249
|
+
if value.startswith("budget:"):
|
|
250
|
+
budget = int(value[7:])
|
|
251
|
+
if budget == 0:
|
|
252
|
+
return llm_param.Thinking(type="disabled", budget_tokens=0)
|
|
253
|
+
return llm_param.Thinking(type="enabled", budget_tokens=budget)
|
|
254
|
+
|
|
255
|
+
return None
|
klaude_code/core/executor.py
CHANGED
|
@@ -13,18 +13,13 @@ from collections.abc import Callable
|
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
-
from klaude_code.command import dispatch_command
|
|
17
|
-
from klaude_code.command.thinking_cmd import (
|
|
18
|
-
format_current_thinking,
|
|
19
|
-
select_thinking_for_protocol,
|
|
20
|
-
should_auto_trigger_thinking,
|
|
21
|
-
)
|
|
22
16
|
from klaude_code.config import load_config
|
|
23
17
|
from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
|
|
24
18
|
from klaude_code.core.manager import LLMClients, SubAgentManager
|
|
25
19
|
from klaude_code.core.tool import current_run_subtask_callback
|
|
26
20
|
from klaude_code.llm.registry import create_llm_client
|
|
27
21
|
from klaude_code.protocol import commands, events, model, op
|
|
22
|
+
from klaude_code.protocol.llm_param import Thinking
|
|
28
23
|
from klaude_code.protocol.op_handler import OperationHandler
|
|
29
24
|
from klaude_code.protocol.sub_agent import SubAgentResult
|
|
30
25
|
from klaude_code.session.export import build_export_html, get_default_export_path
|
|
@@ -181,7 +176,11 @@ class ExecutorContext:
|
|
|
181
176
|
await self._ensure_agent(operation.session_id)
|
|
182
177
|
|
|
183
178
|
async def handle_user_input(self, operation: op.UserInputOperation) -> None:
|
|
184
|
-
"""Handle a user input operation
|
|
179
|
+
"""Handle a user input operation.
|
|
180
|
+
|
|
181
|
+
Core should not parse slash commands. The UI/CLI layer is responsible for
|
|
182
|
+
turning raw user input into one or more operations.
|
|
183
|
+
"""
|
|
185
184
|
|
|
186
185
|
if operation.session_id is None:
|
|
187
186
|
raise ValueError("session_id cannot be None")
|
|
@@ -190,33 +189,18 @@ class ExecutorContext:
|
|
|
190
189
|
agent = await self._ensure_agent(session_id)
|
|
191
190
|
user_input = operation.input
|
|
192
191
|
|
|
193
|
-
# Emit the original user input to UI (even if the persisted text differs).
|
|
194
192
|
await self.emit_event(
|
|
195
193
|
events.UserMessageEvent(content=user_input.text, session_id=session_id, images=user_input.images)
|
|
196
194
|
)
|
|
195
|
+
agent.session.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
|
|
197
196
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
raise ValueError("Multiple RunAgentOperation results are not supported")
|
|
204
|
-
|
|
205
|
-
persisted_user_input = run_ops[0].input if run_ops else user_input
|
|
206
|
-
|
|
207
|
-
if result.persist_user_input:
|
|
208
|
-
agent.session.append_history(
|
|
209
|
-
[model.UserMessageItem(content=persisted_user_input.text, images=persisted_user_input.images)]
|
|
197
|
+
await self.handle_run_agent(
|
|
198
|
+
op.RunAgentOperation(
|
|
199
|
+
id=operation.id,
|
|
200
|
+
session_id=session_id,
|
|
201
|
+
input=user_input,
|
|
210
202
|
)
|
|
211
|
-
|
|
212
|
-
if result.events:
|
|
213
|
-
for evt in result.events:
|
|
214
|
-
if result.persist_events and isinstance(evt, events.DeveloperMessageEvent):
|
|
215
|
-
agent.session.append_history([evt.item])
|
|
216
|
-
await self.emit_event(evt)
|
|
217
|
-
|
|
218
|
-
for operation_item in ops:
|
|
219
|
-
await operation_item.execute(handler=self)
|
|
203
|
+
)
|
|
220
204
|
|
|
221
205
|
async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
|
|
222
206
|
agent = await self._ensure_agent(operation.session_id)
|
|
@@ -243,56 +227,62 @@ class ExecutorContext:
|
|
|
243
227
|
config.main_model = operation.model_name
|
|
244
228
|
await config.save()
|
|
245
229
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
230
|
+
if operation.emit_switch_message:
|
|
231
|
+
default_note = " (saved as default)" if operation.save_as_default else ""
|
|
232
|
+
developer_item = model.DeveloperMessageItem(
|
|
233
|
+
content=f"Switched to: {llm_config.model}{default_note}",
|
|
234
|
+
command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
|
|
235
|
+
)
|
|
236
|
+
agent.session.append_history([developer_item])
|
|
237
|
+
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
254
238
|
|
|
255
239
|
if self._on_model_change is not None:
|
|
256
240
|
self._on_model_change(llm_client.model_name)
|
|
257
241
|
|
|
258
|
-
if
|
|
259
|
-
thinking_op = op.ChangeThinkingOperation(session_id=operation.session_id)
|
|
260
|
-
await thinking_op.execute(handler=self)
|
|
261
|
-
# WelcomeEvent is already handled by the thinking change
|
|
262
|
-
else:
|
|
242
|
+
if operation.emit_welcome_event:
|
|
263
243
|
await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
|
|
264
244
|
|
|
265
245
|
async def handle_change_thinking(self, operation: op.ChangeThinkingOperation) -> None:
|
|
266
|
-
"""Handle a change thinking operation
|
|
246
|
+
"""Handle a change thinking operation.
|
|
247
|
+
|
|
248
|
+
Interactive thinking selection must happen in the UI/CLI layer. Core only
|
|
249
|
+
applies a concrete thinking configuration.
|
|
250
|
+
"""
|
|
267
251
|
agent = await self._ensure_agent(operation.session_id)
|
|
268
|
-
if not agent.profile:
|
|
269
|
-
return
|
|
270
252
|
|
|
271
253
|
config = agent.profile.llm_client.get_llm_config()
|
|
272
|
-
current = format_current_thinking(config)
|
|
273
|
-
|
|
274
|
-
new_thinking = await select_thinking_for_protocol(config)
|
|
275
254
|
|
|
276
|
-
|
|
255
|
+
def _format_thinking_for_display(thinking: Thinking | None) -> str:
|
|
256
|
+
if thinking is None:
|
|
257
|
+
return "not configured"
|
|
258
|
+
if thinking.reasoning_effort:
|
|
259
|
+
return f"reasoning_effort={thinking.reasoning_effort}"
|
|
260
|
+
if thinking.type == "disabled":
|
|
261
|
+
return "off"
|
|
262
|
+
if thinking.type == "enabled":
|
|
263
|
+
if thinking.budget_tokens is None:
|
|
264
|
+
return "enabled"
|
|
265
|
+
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
266
|
+
return "not set"
|
|
267
|
+
|
|
268
|
+
if operation.thinking is None:
|
|
269
|
+
raise ValueError("thinking must be provided; interactive selection belongs to UI")
|
|
270
|
+
|
|
271
|
+
current = _format_thinking_for_display(config.thinking)
|
|
272
|
+
config.thinking = operation.thinking
|
|
273
|
+
agent.session.model_thinking = operation.thinking
|
|
274
|
+
new_status = _format_thinking_for_display(config.thinking)
|
|
275
|
+
|
|
276
|
+
if operation.emit_switch_message:
|
|
277
277
|
developer_item = model.DeveloperMessageItem(
|
|
278
|
-
content="
|
|
278
|
+
content=f"Thinking changed: {current} -> {new_status}",
|
|
279
279
|
command_output=model.CommandOutput(command_name=commands.CommandName.THINKING),
|
|
280
280
|
)
|
|
281
|
+
agent.session.append_history([developer_item])
|
|
281
282
|
await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
config.thinking = new_thinking
|
|
285
|
-
agent.session.model_thinking = new_thinking
|
|
286
|
-
new_status = format_current_thinking(config)
|
|
287
|
-
|
|
288
|
-
developer_item = model.DeveloperMessageItem(
|
|
289
|
-
content=f"Thinking changed: {current} -> {new_status}",
|
|
290
|
-
command_output=model.CommandOutput(command_name=commands.CommandName.THINKING),
|
|
291
|
-
)
|
|
292
|
-
agent.session.append_history([developer_item])
|
|
293
283
|
|
|
294
|
-
|
|
295
|
-
|
|
284
|
+
if operation.emit_welcome_event:
|
|
285
|
+
await self.emit_event(events.WelcomeEvent(work_dir=str(agent.session.work_dir), llm_config=config))
|
|
296
286
|
|
|
297
287
|
async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
|
|
298
288
|
agent = await self._ensure_agent(operation.session_id)
|
klaude_code/llm/usage.py
CHANGED
|
@@ -81,7 +81,7 @@ class MetadataTracker:
|
|
|
81
81
|
) * 1000
|
|
82
82
|
|
|
83
83
|
if self._last_token_time is not None and self._metadata_item.usage.output_tokens > 0:
|
|
84
|
-
time_duration = self._last_token_time - self.
|
|
84
|
+
time_duration = self._last_token_time - self._request_start_time
|
|
85
85
|
if time_duration >= 0.15:
|
|
86
86
|
self._metadata_item.usage.throughput_tps = self._metadata_item.usage.output_tokens / time_duration
|
|
87
87
|
|
klaude_code/protocol/commands.py
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
from enum import Enum
|
|
2
3
|
|
|
3
4
|
|
|
5
|
+
@dataclass(frozen=True, slots=True)
|
|
6
|
+
class CommandInfo:
|
|
7
|
+
"""Lightweight command metadata for UI purposes (no logic)."""
|
|
8
|
+
|
|
9
|
+
name: str
|
|
10
|
+
summary: str
|
|
11
|
+
support_addition_params: bool = False
|
|
12
|
+
placeholder: str = ""
|
|
13
|
+
|
|
14
|
+
|
|
4
15
|
class CommandName(str, Enum):
|
|
5
16
|
INIT = "init"
|
|
6
17
|
DEBUG = "debug"
|