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.
- klaude_code/.DS_Store +0 -0
- klaude_code/cli/auth_cmd.py +2 -13
- klaude_code/cli/cost_cmd.py +10 -10
- klaude_code/cli/list_model.py +8 -0
- klaude_code/cli/main.py +41 -8
- klaude_code/cli/session_cmd.py +2 -11
- klaude_code/config/assets/builtin_config.yaml +45 -26
- klaude_code/config/config.py +30 -7
- klaude_code/config/model_matcher.py +3 -3
- klaude_code/config/sub_agent_model_helper.py +1 -1
- klaude_code/const.py +2 -1
- klaude_code/core/agent_profile.py +1 -0
- klaude_code/core/executor.py +4 -0
- klaude_code/core/loaded_skills.py +36 -0
- klaude_code/core/tool/context.py +1 -3
- klaude_code/core/tool/file/edit_tool.py +1 -1
- klaude_code/core/tool/file/read_tool.py +2 -2
- klaude_code/core/tool/file/write_tool.py +1 -1
- klaude_code/core/turn.py +19 -7
- klaude_code/llm/anthropic/client.py +97 -60
- klaude_code/llm/anthropic/input.py +20 -9
- klaude_code/llm/google/client.py +223 -148
- klaude_code/llm/google/input.py +44 -36
- klaude_code/llm/openai_compatible/stream.py +109 -99
- klaude_code/llm/openrouter/reasoning.py +4 -29
- klaude_code/llm/partial_message.py +2 -32
- klaude_code/llm/responses/client.py +99 -81
- klaude_code/llm/responses/input.py +11 -25
- klaude_code/llm/stream_parts.py +94 -0
- klaude_code/log.py +57 -0
- klaude_code/protocol/events/system.py +3 -0
- klaude_code/protocol/llm_param.py +1 -0
- klaude_code/session/export.py +259 -91
- klaude_code/session/templates/export_session.html +141 -59
- klaude_code/skill/.DS_Store +0 -0
- klaude_code/skill/assets/.DS_Store +0 -0
- klaude_code/skill/loader.py +1 -0
- klaude_code/tui/command/fork_session_cmd.py +14 -23
- klaude_code/tui/command/model_picker.py +2 -17
- klaude_code/tui/command/refresh_cmd.py +2 -0
- klaude_code/tui/command/resume_cmd.py +2 -18
- klaude_code/tui/command/sub_agent_model_cmd.py +5 -19
- klaude_code/tui/command/thinking_cmd.py +2 -14
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/metadata.py +22 -21
- klaude_code/tui/components/rich/markdown.py +8 -0
- klaude_code/tui/components/rich/quote.py +36 -8
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/welcome.py +32 -0
- klaude_code/tui/input/prompt_toolkit.py +3 -1
- klaude_code/tui/machine.py +19 -1
- klaude_code/tui/renderer.py +3 -4
- klaude_code/tui/terminal/selector.py +174 -31
- {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/METADATA +1 -1
- {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/RECORD +57 -53
- klaude_code/skill/assets/jj-workspace/SKILL.md +0 -20
- {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/WHEEL +0 -0
- {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/entry_points.txt +0 -0
klaude_code/.DS_Store
ADDED
|
Binary file
|
klaude_code/cli/auth_cmd.py
CHANGED
|
@@ -4,20 +4,9 @@ import datetime
|
|
|
4
4
|
import webbrowser
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
|
-
from prompt_toolkit.styles import Style
|
|
8
7
|
|
|
9
8
|
from klaude_code.log import log
|
|
10
|
-
from klaude_code.tui.terminal.selector import SelectItem, select_one
|
|
11
|
-
|
|
12
|
-
_SELECT_STYLE = Style(
|
|
13
|
-
[
|
|
14
|
-
("instruction", "ansibrightblack"),
|
|
15
|
-
("pointer", "ansigreen"),
|
|
16
|
-
("highlighted", "ansigreen"),
|
|
17
|
-
("text", "ansibrightblack"),
|
|
18
|
-
("question", "bold"),
|
|
19
|
-
]
|
|
20
|
-
)
|
|
9
|
+
from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
|
|
21
10
|
|
|
22
11
|
|
|
23
12
|
def _select_provider() -> str | None:
|
|
@@ -30,7 +19,7 @@ def _select_provider() -> str | None:
|
|
|
30
19
|
message="Select provider to login:",
|
|
31
20
|
items=items,
|
|
32
21
|
pointer="→",
|
|
33
|
-
style=
|
|
22
|
+
style=DEFAULT_PICKER_STYLE,
|
|
34
23
|
use_search_filter=False,
|
|
35
24
|
)
|
|
36
25
|
|
klaude_code/cli/cost_cmd.py
CHANGED
|
@@ -183,14 +183,14 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
|
|
|
183
183
|
box=ASCII_HORIZONAL,
|
|
184
184
|
)
|
|
185
185
|
|
|
186
|
-
table.add_column("Date", style="cyan"
|
|
187
|
-
table.add_column("Model",
|
|
188
|
-
table.add_column("Input", justify="right"
|
|
189
|
-
table.add_column("Output", justify="right"
|
|
190
|
-
table.add_column("Cache", justify="right"
|
|
191
|
-
table.add_column("Total", justify="right"
|
|
192
|
-
table.add_column("USD", justify="right"
|
|
193
|
-
table.add_column("CNY", justify="right"
|
|
186
|
+
table.add_column("Date", style="cyan")
|
|
187
|
+
table.add_column("Model", overflow="ellipsis")
|
|
188
|
+
table.add_column("Input", justify="right")
|
|
189
|
+
table.add_column("Output", justify="right")
|
|
190
|
+
table.add_column("Cache", justify="right")
|
|
191
|
+
table.add_column("Total", justify="right")
|
|
192
|
+
table.add_column("USD", justify="right")
|
|
193
|
+
table.add_column("CNY", justify="right")
|
|
194
194
|
|
|
195
195
|
# Sort dates
|
|
196
196
|
sorted_dates = sorted(daily_stats.keys())
|
|
@@ -222,7 +222,7 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
|
|
|
222
222
|
|
|
223
223
|
table.add_row(
|
|
224
224
|
format_date_display(date_str) if first_row else "",
|
|
225
|
-
f"
|
|
225
|
+
f"{model_name}",
|
|
226
226
|
format_tokens(stats.input_tokens),
|
|
227
227
|
format_tokens(stats.output_tokens),
|
|
228
228
|
format_tokens(stats.cached_tokens),
|
|
@@ -276,7 +276,7 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
|
|
|
276
276
|
usd_str, cny_str = format_cost_dual(stats.cost_usd, stats.cost_cny)
|
|
277
277
|
table.add_row(
|
|
278
278
|
"",
|
|
279
|
-
f"
|
|
279
|
+
f"{model_name}",
|
|
280
280
|
format_tokens(stats.input_tokens),
|
|
281
281
|
format_tokens(stats.output_tokens),
|
|
282
282
|
format_tokens(stats.cached_tokens),
|
klaude_code/cli/list_model.py
CHANGED
|
@@ -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
|
@@ -13,13 +13,46 @@ from klaude_code.session import Session
|
|
|
13
13
|
from klaude_code.tui.command.resume_cmd import select_session_sync
|
|
14
14
|
from klaude_code.ui.terminal.title import update_terminal_title
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
Environment Variables:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
""
|
|
16
|
+
ENV_HELP_LINES = [
|
|
17
|
+
"Environment Variables:",
|
|
18
|
+
"",
|
|
19
|
+
"Provider API keys (built-in config):",
|
|
20
|
+
" ANTHROPIC_API_KEY Anthropic API key",
|
|
21
|
+
" OPENAI_API_KEY OpenAI API key",
|
|
22
|
+
" OPENROUTER_API_KEY OpenRouter API key",
|
|
23
|
+
" GOOGLE_API_KEY Google API key (Gemini)",
|
|
24
|
+
" DEEPSEEK_API_KEY DeepSeek API key",
|
|
25
|
+
" MOONSHOT_API_KEY Moonshot API key (Kimi)",
|
|
26
|
+
"",
|
|
27
|
+
"AWS credentials (Bedrock):",
|
|
28
|
+
" AWS_ACCESS_KEY_ID AWS access key id",
|
|
29
|
+
" AWS_SECRET_ACCESS_KEY AWS secret access key",
|
|
30
|
+
" AWS_REGION AWS region",
|
|
31
|
+
"",
|
|
32
|
+
"Tool limits (Read):",
|
|
33
|
+
" KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)",
|
|
34
|
+
" KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)",
|
|
35
|
+
" KLAUDE_READ_MAX_IMAGE_BYTES Max image bytes to read (default: 4MB)",
|
|
36
|
+
" KLAUDE_IMAGE_OUTPUT_MAX_BYTES Max decoded image bytes (default: 64MB)",
|
|
37
|
+
"",
|
|
38
|
+
"Notifications / testing:",
|
|
39
|
+
" KLAUDE_NOTIFY Set to 0/off/false/disable(d) to disable task notifications",
|
|
40
|
+
" KLAUDE_TEST_SIGNAL In tmux, emit `tmux wait-for -S <channel>` on task completion",
|
|
41
|
+
" TMUX Auto-detected; required for KLAUDE_TEST_SIGNAL",
|
|
42
|
+
"",
|
|
43
|
+
"Editor / terminal integration:",
|
|
44
|
+
" EDITOR Preferred editor for `klaude config`",
|
|
45
|
+
" TERM Terminal identification (auto-detected)",
|
|
46
|
+
" TERM_PROGRAM Terminal identification (auto-detected)",
|
|
47
|
+
" WT_SESSION Terminal hint (auto-detected)",
|
|
48
|
+
" VTE_VERSION Terminal hint (auto-detected)",
|
|
49
|
+
" GHOSTTY_RESOURCES_DIR Ghostty detection (auto-detected)",
|
|
50
|
+
"",
|
|
51
|
+
"Compatibility:",
|
|
52
|
+
" ANTHROPIC_AUTH_TOKEN Reserved by anthropic SDK; temporarily unset during client init",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
ENV_HELP = "\n\n".join(ENV_HELP_LINES)
|
|
23
56
|
|
|
24
57
|
app = typer.Typer(
|
|
25
58
|
add_completion=False,
|
|
@@ -192,7 +225,7 @@ def main_callback(
|
|
|
192
225
|
if raw_model:
|
|
193
226
|
matches = [
|
|
194
227
|
m.selector
|
|
195
|
-
for m in cfg.iter_model_entries()
|
|
228
|
+
for m in cfg.iter_model_entries(only_available=True, include_disabled=False)
|
|
196
229
|
if (m.model_id or "").strip().lower() == raw_model.lower()
|
|
197
230
|
]
|
|
198
231
|
if len(matches) == 1:
|
klaude_code/cli/session_cmd.py
CHANGED
|
@@ -9,9 +9,7 @@ from klaude_code.session import Session
|
|
|
9
9
|
def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) -> bool:
|
|
10
10
|
"""Show session list and confirm deletion using prompt_toolkit."""
|
|
11
11
|
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
from klaude_code.tui.terminal.selector import SelectItem, select_one
|
|
12
|
+
from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
|
|
15
13
|
|
|
16
14
|
def _fmt(ts: float) -> str:
|
|
17
15
|
try:
|
|
@@ -37,14 +35,7 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
|
|
|
37
35
|
message=message,
|
|
38
36
|
items=items,
|
|
39
37
|
pointer="→",
|
|
40
|
-
style=
|
|
41
|
-
[
|
|
42
|
-
("question", "bold"),
|
|
43
|
-
("pointer", "ansigreen"),
|
|
44
|
-
("highlighted", "ansigreen"),
|
|
45
|
-
("text", ""),
|
|
46
|
-
]
|
|
47
|
-
),
|
|
38
|
+
style=DEFAULT_PICKER_STYLE,
|
|
48
39
|
use_search_filter=False,
|
|
49
40
|
)
|
|
50
41
|
return bool(result)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
---
|
|
2
1
|
# Built-in provider and model configurations
|
|
3
2
|
# Users can start using klaude by simply setting environment variables
|
|
4
3
|
# (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) without manual configuration.
|
|
@@ -25,32 +24,58 @@ provider_list:
|
|
|
25
24
|
protocol: responses
|
|
26
25
|
api_key: ${OPENAI_API_KEY}
|
|
27
26
|
model_list:
|
|
28
|
-
- model_name: gpt-5.2
|
|
27
|
+
- model_name: gpt-5.2-high
|
|
29
28
|
model_id: gpt-5.2
|
|
30
29
|
max_tokens: 128000
|
|
31
30
|
context_limit: 400000
|
|
32
31
|
verbosity: high
|
|
33
32
|
thinking:
|
|
34
33
|
reasoning_effort: high
|
|
34
|
+
reasoning_summary: detailed
|
|
35
|
+
cost: {input: 1.75, output: 14, cache_read: 0.17}
|
|
36
|
+
- model_name: gpt-5.2-medium
|
|
37
|
+
model_id: gpt-5.2
|
|
38
|
+
context_limit: 400000
|
|
39
|
+
verbosity: high
|
|
40
|
+
thinking:
|
|
41
|
+
reasoning_effort: medium
|
|
42
|
+
reasoning_summary: concise
|
|
43
|
+
cost: {input: 1.75, output: 14, cache_read: 0.17}
|
|
44
|
+
- model_name: gpt-5.2-low
|
|
45
|
+
model_id: gpt-5.2
|
|
46
|
+
context_limit: 400000
|
|
47
|
+
verbosity: low
|
|
48
|
+
thinking:
|
|
49
|
+
reasoning_effort: low
|
|
50
|
+
reasoning_summary: concise
|
|
51
|
+
cost: {input: 1.75, output: 14, cache_read: 0.17}
|
|
52
|
+
- model_name: gpt-5.2-fast
|
|
53
|
+
model_id: gpt-5.2
|
|
54
|
+
context_limit: 400000
|
|
55
|
+
verbosity: low
|
|
56
|
+
thinking:
|
|
57
|
+
reasoning_effort: none
|
|
35
58
|
cost: {input: 1.75, output: 14, cache_read: 0.17}
|
|
36
|
-
- provider_name: openrouter
|
|
37
|
-
protocol: openrouter
|
|
38
|
-
api_key: ${OPENROUTER_API_KEY}
|
|
39
|
-
model_list:
|
|
40
59
|
- model_name: gpt-5.1-codex-max
|
|
41
|
-
model_id:
|
|
60
|
+
model_id: gpt-5.1-codex-max
|
|
42
61
|
max_tokens: 128000
|
|
43
62
|
context_limit: 400000
|
|
44
63
|
thinking:
|
|
45
64
|
reasoning_effort: medium
|
|
65
|
+
reasoning_summary: detailed
|
|
46
66
|
cost: {input: 1.25, output: 10, cache_read: 0.13}
|
|
47
|
-
|
|
67
|
+
- provider_name: openrouter
|
|
68
|
+
protocol: openrouter
|
|
69
|
+
api_key: ${OPENROUTER_API_KEY}
|
|
70
|
+
model_list:
|
|
71
|
+
- model_name: gpt-5.2-high
|
|
48
72
|
model_id: openai/gpt-5.2
|
|
49
73
|
max_tokens: 128000
|
|
50
74
|
context_limit: 400000
|
|
51
75
|
verbosity: high
|
|
52
76
|
thinking:
|
|
53
77
|
reasoning_effort: high
|
|
78
|
+
reasoning_summary: detailed
|
|
54
79
|
cost: {input: 1.75, output: 14, cache_read: 0.17}
|
|
55
80
|
- model_name: gpt-5.2-medium
|
|
56
81
|
model_id: openai/gpt-5.2
|
|
@@ -59,22 +84,7 @@ provider_list:
|
|
|
59
84
|
verbosity: high
|
|
60
85
|
thinking:
|
|
61
86
|
reasoning_effort: medium
|
|
62
|
-
|
|
63
|
-
- model_name: gpt-5.2-low
|
|
64
|
-
model_id: openai/gpt-5.2
|
|
65
|
-
max_tokens: 128000
|
|
66
|
-
context_limit: 400000
|
|
67
|
-
verbosity: low
|
|
68
|
-
thinking:
|
|
69
|
-
reasoning_effort: low
|
|
70
|
-
cost: {input: 1.75, output: 14, cache_read: 0.17}
|
|
71
|
-
- model_name: gpt-5.2-fast
|
|
72
|
-
model_id: openai/gpt-5.2
|
|
73
|
-
max_tokens: 128000
|
|
74
|
-
context_limit: 400000
|
|
75
|
-
verbosity: low
|
|
76
|
-
thinking:
|
|
77
|
-
reasoning_effort: none
|
|
87
|
+
reasoning_summary: concise
|
|
78
88
|
cost: {input: 1.75, output: 14, cache_read: 0.17}
|
|
79
89
|
- model_name: kimi
|
|
80
90
|
model_id: moonshotai/kimi-k2-thinking
|
|
@@ -96,7 +106,6 @@ provider_list:
|
|
|
96
106
|
- model_name: opus
|
|
97
107
|
model_id: anthropic/claude-4.5-opus
|
|
98
108
|
context_limit: 200000
|
|
99
|
-
verbosity: high
|
|
100
109
|
thinking:
|
|
101
110
|
type: enabled
|
|
102
111
|
budget_tokens: 2048
|
|
@@ -166,10 +175,14 @@ provider_list:
|
|
|
166
175
|
- model_name: gemini-pro
|
|
167
176
|
model_id: gemini-3-pro-preview
|
|
168
177
|
context_limit: 1048576
|
|
178
|
+
thinking:
|
|
179
|
+
reasoning_effort: high
|
|
169
180
|
cost: {input: 2, output: 12, cache_read: 0.2}
|
|
170
181
|
- model_name: gemini-flash
|
|
171
182
|
model_id: gemini-3-flash-preview
|
|
172
183
|
context_limit: 1048576
|
|
184
|
+
thinking:
|
|
185
|
+
reasoning_effort: medium
|
|
173
186
|
cost: {input: 0.5, output: 3, cache_read: 0.05}
|
|
174
187
|
- model_name: nano-banana-pro
|
|
175
188
|
model_id: gemini-3-pro-image-preview
|
|
@@ -178,6 +191,13 @@ provider_list:
|
|
|
178
191
|
- image
|
|
179
192
|
- text
|
|
180
193
|
cost: {input: 2, output: 12, cache_read: 0.2, image: 120}
|
|
194
|
+
- model_name: nano-banana
|
|
195
|
+
model_id: gemini-2.5-flash-image
|
|
196
|
+
context_limit: 33000
|
|
197
|
+
modalities:
|
|
198
|
+
- image
|
|
199
|
+
- text
|
|
200
|
+
cost: {input: 0.3, output: 2.5, cache_read: 0.03, image: 30}
|
|
181
201
|
- provider_name: bedrock
|
|
182
202
|
protocol: bedrock
|
|
183
203
|
aws_access_key: ${AWS_ACCESS_KEY_ID}
|
|
@@ -222,7 +242,6 @@ provider_list:
|
|
|
222
242
|
- model_name: opus
|
|
223
243
|
model_id: claude-opus-4-5-20251101
|
|
224
244
|
context_limit: 200000
|
|
225
|
-
verbosity: high
|
|
226
245
|
thinking:
|
|
227
246
|
type: enabled
|
|
228
247
|
budget_tokens: 2048
|
klaude_code/config/config.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -102,6 +102,7 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
|
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
# Normalized matching (e.g. gpt52 == gpt-5.2, gpt52 in gpt-5.2-2025-...)
|
|
105
|
+
# Only match selector/model_name exactly; model_id is checked via substring match below
|
|
105
106
|
preferred_norm = _normalize_model_key(preferred)
|
|
106
107
|
normalized_matches: list[ModelEntry] = []
|
|
107
108
|
if preferred_norm:
|
|
@@ -110,7 +111,6 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
|
|
|
110
111
|
for m in models
|
|
111
112
|
if preferred_norm == _normalize_model_key(m.selector)
|
|
112
113
|
or preferred_norm == _normalize_model_key(m.model_name)
|
|
113
|
-
or preferred_norm == _normalize_model_key(m.model_id or "")
|
|
114
114
|
]
|
|
115
115
|
if len(normalized_matches) == 1:
|
|
116
116
|
return ModelMatchResult(
|
|
@@ -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]
|
klaude_code/const.py
CHANGED
|
@@ -27,6 +27,7 @@ def _get_int_env(name: str, default: int) -> int:
|
|
|
27
27
|
# =============================================================================
|
|
28
28
|
|
|
29
29
|
MAX_FAILED_TURN_RETRIES = 10 # Maximum retry attempts for failed turns
|
|
30
|
+
RETRY_PRESERVE_PARTIAL_MESSAGE = True # Preserve partial message on stream error for retry prefill
|
|
30
31
|
LLM_HTTP_TIMEOUT_TOTAL = 300.0 # HTTP timeout for LLM API requests (seconds)
|
|
31
32
|
LLM_HTTP_TIMEOUT_CONNECT = 15.0 # HTTP connect timeout (seconds)
|
|
32
33
|
LLM_HTTP_TIMEOUT_READ = 285.0 # HTTP read timeout (seconds)
|
|
@@ -157,7 +158,7 @@ MARKDOWN_RIGHT_MARGIN = 2 # Right margin (columns) for markdown rendering
|
|
|
157
158
|
STATUS_HINT_TEXT = " (esc to interrupt)" # Status hint text shown after spinner
|
|
158
159
|
|
|
159
160
|
# Spinner status texts
|
|
160
|
-
STATUS_WAITING_TEXT = "
|
|
161
|
+
STATUS_WAITING_TEXT = "Loading …"
|
|
161
162
|
STATUS_THINKING_TEXT = "Thinking …"
|
|
162
163
|
STATUS_COMPOSING_TEXT = "Composing"
|
|
163
164
|
|
|
@@ -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
|
|
klaude_code/core/executor.py
CHANGED
|
@@ -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
|
klaude_code/core/tool/context.py
CHANGED
|
@@ -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)
|
|
@@ -98,7 +98,7 @@ class EditTool(ToolABC):
|
|
|
98
98
|
if is_directory(file_path):
|
|
99
99
|
return message.ToolResultMessage(
|
|
100
100
|
status="error",
|
|
101
|
-
output_text="<tool_use_error>Illegal operation on a directory
|
|
101
|
+
output_text="<tool_use_error>Illegal operation on a directory: edit</tool_use_error>",
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
if args.old_string == "":
|
|
@@ -210,7 +210,7 @@ class ReadTool(ToolABC):
|
|
|
210
210
|
if is_directory(file_path):
|
|
211
211
|
return message.ToolResultMessage(
|
|
212
212
|
status="error",
|
|
213
|
-
output_text="<tool_use_error>Illegal operation on a directory
|
|
213
|
+
output_text="<tool_use_error>Illegal operation on a directory: read</tool_use_error>",
|
|
214
214
|
)
|
|
215
215
|
if not file_exists(file_path):
|
|
216
216
|
return message.ToolResultMessage(
|
|
@@ -308,7 +308,7 @@ class ReadTool(ToolABC):
|
|
|
308
308
|
except IsADirectoryError:
|
|
309
309
|
return message.ToolResultMessage(
|
|
310
310
|
status="error",
|
|
311
|
-
output_text="<tool_use_error>Illegal operation on a directory
|
|
311
|
+
output_text="<tool_use_error>Illegal operation on a directory: read</tool_use_error>",
|
|
312
312
|
)
|
|
313
313
|
|
|
314
314
|
if offset > max(read_result.total_lines, 0):
|
|
@@ -57,7 +57,7 @@ class WriteTool(ToolABC):
|
|
|
57
57
|
if is_directory(file_path):
|
|
58
58
|
return message.ToolResultMessage(
|
|
59
59
|
status="error",
|
|
60
|
-
output_text="<tool_use_error>Illegal operation on a directory
|
|
60
|
+
output_text="<tool_use_error>Illegal operation on a directory: write</tool_use_error>",
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
file_tracker = context.file_tracker
|
klaude_code/core/turn.py
CHANGED
|
@@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from klaude_code.const import SUPPORTED_IMAGE_SIZES
|
|
7
|
+
from klaude_code.const import RETRY_PRESERVE_PARTIAL_MESSAGE, SUPPORTED_IMAGE_SIZES
|
|
8
8
|
from klaude_code.core.tool import ToolABC
|
|
9
9
|
from klaude_code.core.tool.context import SubAgentResumeClaims, ToolContext
|
|
10
10
|
|
|
@@ -24,6 +24,12 @@ from klaude_code.llm.client import LLMStreamABC
|
|
|
24
24
|
from klaude_code.log import DebugType, log_debug
|
|
25
25
|
from klaude_code.protocol import events, llm_param, message, model, tools
|
|
26
26
|
|
|
27
|
+
# Protocols that support prefill (continuing from partial assistant message)
|
|
28
|
+
_PREFILL_SUPPORTED_PROTOCOLS = frozenset({
|
|
29
|
+
"anthropic",
|
|
30
|
+
"claude_oauth",
|
|
31
|
+
})
|
|
32
|
+
|
|
27
33
|
|
|
28
34
|
class TurnError(Exception):
|
|
29
35
|
"""Raised when a turn fails and should be retried."""
|
|
@@ -176,6 +182,18 @@ class TurnExecutor:
|
|
|
176
182
|
yield event
|
|
177
183
|
|
|
178
184
|
if self._turn_result.stream_error is not None:
|
|
185
|
+
# Save accumulated content for potential prefill on retry (only for supported protocols)
|
|
186
|
+
protocol = ctx.llm_client.get_llm_config().protocol
|
|
187
|
+
supports_prefill = protocol.value in _PREFILL_SUPPORTED_PROTOCOLS
|
|
188
|
+
if (
|
|
189
|
+
RETRY_PRESERVE_PARTIAL_MESSAGE
|
|
190
|
+
and supports_prefill
|
|
191
|
+
and self._turn_result.assistant_message is not None
|
|
192
|
+
and self._turn_result.assistant_message.parts
|
|
193
|
+
):
|
|
194
|
+
session_ctx.append_history([self._turn_result.assistant_message])
|
|
195
|
+
# Add continuation prompt to avoid Anthropic thinking block requirement
|
|
196
|
+
session_ctx.append_history([message.UserMessage(parts=[message.TextPart(text="continue")])])
|
|
179
197
|
session_ctx.append_history([self._turn_result.stream_error])
|
|
180
198
|
yield events.TurnEndEvent(session_id=session_ctx.session_id)
|
|
181
199
|
raise TurnError(self._turn_result.stream_error.error)
|
|
@@ -339,12 +357,6 @@ class TurnExecutor:
|
|
|
339
357
|
)
|
|
340
358
|
case message.StreamErrorItem() as msg:
|
|
341
359
|
turn_result.stream_error = msg
|
|
342
|
-
log_debug(
|
|
343
|
-
"[StreamError]",
|
|
344
|
-
msg.error,
|
|
345
|
-
style="red",
|
|
346
|
-
debug_type=DebugType.RESPONSE,
|
|
347
|
-
)
|
|
348
360
|
case message.ToolCallStartDelta() as msg:
|
|
349
361
|
if thinking_active:
|
|
350
362
|
thinking_active = False
|