klaude-code 1.2.6__py3-none-any.whl → 1.8.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/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
klaude_code/config/config.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
2
4
|
from functools import lru_cache
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Any, cast
|
|
@@ -6,172 +8,439 @@ from typing import Any, cast
|
|
|
6
8
|
import yaml
|
|
7
9
|
from pydantic import BaseModel, Field, ValidationError, model_validator
|
|
8
10
|
|
|
11
|
+
from klaude_code.config.builtin_config import SUPPORTED_API_KEY_ENVS, get_builtin_provider_configs
|
|
9
12
|
from klaude_code.protocol import llm_param
|
|
10
13
|
from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
|
|
11
14
|
from klaude_code.trace import log
|
|
12
15
|
|
|
16
|
+
# Pattern to match ${ENV_VAR} syntax
|
|
17
|
+
_ENV_VAR_PATTERN = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_env_var_syntax(value: str | None) -> tuple[str | None, str | None]:
|
|
21
|
+
"""Parse a value that may use ${ENV_VAR} syntax.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A tuple of (env_var_name, resolved_value).
|
|
25
|
+
- If value uses ${ENV_VAR} syntax: (env_var_name, os.environ.get(env_var_name))
|
|
26
|
+
- If value is a plain string: (None, value)
|
|
27
|
+
- If value is None: (None, None)
|
|
28
|
+
"""
|
|
29
|
+
if value is None:
|
|
30
|
+
return None, None
|
|
31
|
+
|
|
32
|
+
match = _ENV_VAR_PATTERN.match(value)
|
|
33
|
+
if match:
|
|
34
|
+
env_var_name = match.group(1)
|
|
35
|
+
return env_var_name, os.environ.get(env_var_name)
|
|
36
|
+
|
|
37
|
+
return None, value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_env_var_syntax(value: str | None) -> bool:
|
|
41
|
+
"""Check if a value uses ${ENV_VAR} syntax."""
|
|
42
|
+
if value is None:
|
|
43
|
+
return False
|
|
44
|
+
return _ENV_VAR_PATTERN.match(value) is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_api_key(value: str | None) -> str | None:
|
|
48
|
+
"""Resolve an API key value, expanding ${ENV_VAR} syntax if present."""
|
|
49
|
+
_, resolved = parse_env_var_syntax(value)
|
|
50
|
+
return resolved
|
|
51
|
+
|
|
52
|
+
|
|
13
53
|
config_path = Path.home() / ".klaude" / "klaude-config.yaml"
|
|
54
|
+
example_config_path = Path.home() / ".klaude" / "klaude-config.example.yaml"
|
|
14
55
|
|
|
15
56
|
|
|
16
57
|
class ModelConfig(BaseModel):
|
|
58
|
+
model_name: str
|
|
59
|
+
model_params: llm_param.LLMConfigModelParameter
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ProviderConfig(llm_param.LLMConfigProviderParameter):
|
|
63
|
+
"""Full provider configuration (used in merged config)."""
|
|
64
|
+
|
|
65
|
+
model_list: list[ModelConfig] = Field(default_factory=lambda: [])
|
|
66
|
+
|
|
67
|
+
def get_resolved_api_key(self) -> str | None:
|
|
68
|
+
"""Get the resolved API key, expanding ${ENV_VAR} syntax if present."""
|
|
69
|
+
return resolve_api_key(self.api_key)
|
|
70
|
+
|
|
71
|
+
def get_api_key_env_var(self) -> str | None:
|
|
72
|
+
"""Get the environment variable name if ${ENV_VAR} syntax is used."""
|
|
73
|
+
env_var, _ = parse_env_var_syntax(self.api_key)
|
|
74
|
+
return env_var
|
|
75
|
+
|
|
76
|
+
def is_api_key_missing(self) -> bool:
|
|
77
|
+
"""Check if the API key is missing (either not set or env var not found).
|
|
78
|
+
|
|
79
|
+
For codex protocol, checks OAuth login status instead of API key.
|
|
80
|
+
For bedrock protocol, checks AWS credentials instead of API key.
|
|
81
|
+
"""
|
|
82
|
+
from klaude_code.protocol.llm_param import LLMClientProtocol
|
|
83
|
+
|
|
84
|
+
if self.protocol == LLMClientProtocol.CODEX:
|
|
85
|
+
# Codex uses OAuth authentication, not API key
|
|
86
|
+
from klaude_code.auth.codex.token_manager import CodexTokenManager
|
|
87
|
+
|
|
88
|
+
token_manager = CodexTokenManager()
|
|
89
|
+
state = token_manager.get_state()
|
|
90
|
+
# Consider available if logged in and token not expired
|
|
91
|
+
return state is None or state.is_expired()
|
|
92
|
+
|
|
93
|
+
if self.protocol == LLMClientProtocol.BEDROCK:
|
|
94
|
+
# Bedrock uses AWS credentials, not API key. Region is always required.
|
|
95
|
+
_, resolved_profile = parse_env_var_syntax(self.aws_profile)
|
|
96
|
+
_, resolved_region = parse_env_var_syntax(self.aws_region)
|
|
97
|
+
|
|
98
|
+
# When using profile, we still need region to initialize the client.
|
|
99
|
+
if resolved_profile:
|
|
100
|
+
return resolved_region is None
|
|
101
|
+
|
|
102
|
+
_, resolved_access_key = parse_env_var_syntax(self.aws_access_key)
|
|
103
|
+
_, resolved_secret_key = parse_env_var_syntax(self.aws_secret_key)
|
|
104
|
+
return resolved_region is None or resolved_access_key is None or resolved_secret_key is None
|
|
105
|
+
|
|
106
|
+
return self.get_resolved_api_key() is None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class UserProviderConfig(BaseModel):
|
|
110
|
+
"""User provider configuration (allows partial overrides).
|
|
111
|
+
|
|
112
|
+
Unlike ProviderConfig, protocol is optional here since user may only want
|
|
113
|
+
to add models to an existing builtin provider.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
provider_name: str
|
|
117
|
+
protocol: llm_param.LLMClientProtocol | None = None
|
|
118
|
+
base_url: str | None = None
|
|
119
|
+
api_key: str | None = None
|
|
120
|
+
is_azure: bool = False
|
|
121
|
+
azure_api_version: str | None = None
|
|
122
|
+
model_list: list[ModelConfig] = Field(default_factory=lambda: [])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ModelEntry(BaseModel):
|
|
17
126
|
model_name: str
|
|
18
127
|
provider: str
|
|
19
128
|
model_params: llm_param.LLMConfigModelParameter
|
|
20
129
|
|
|
21
130
|
|
|
131
|
+
class UserConfig(BaseModel):
|
|
132
|
+
"""User configuration (what gets saved to disk)."""
|
|
133
|
+
|
|
134
|
+
main_model: str | None = None
|
|
135
|
+
sub_agent_models: dict[str, str] = Field(default_factory=dict)
|
|
136
|
+
theme: str | None = None
|
|
137
|
+
provider_list: list[UserProviderConfig] = Field(default_factory=lambda: [])
|
|
138
|
+
|
|
139
|
+
@model_validator(mode="before")
|
|
140
|
+
@classmethod
|
|
141
|
+
def _normalize_sub_agent_models(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
raw_val: Any = data.get("sub_agent_models") or {}
|
|
143
|
+
raw_models: dict[str, Any] = cast(dict[str, Any], raw_val) if isinstance(raw_val, dict) else {}
|
|
144
|
+
normalized: dict[str, str] = {}
|
|
145
|
+
key_map = {p.name.lower(): p.name for p in iter_sub_agent_profiles()}
|
|
146
|
+
for key, value in dict(raw_models).items():
|
|
147
|
+
canonical = key_map.get(str(key).lower(), str(key))
|
|
148
|
+
normalized[canonical] = str(value)
|
|
149
|
+
data["sub_agent_models"] = normalized
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
|
|
22
153
|
class Config(BaseModel):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
main_model: str
|
|
26
|
-
|
|
154
|
+
"""Merged configuration (builtin + user) for runtime use."""
|
|
155
|
+
|
|
156
|
+
main_model: str | None = None
|
|
157
|
+
sub_agent_models: dict[str, str] = Field(default_factory=dict)
|
|
27
158
|
theme: str | None = None
|
|
159
|
+
provider_list: list[ProviderConfig] = Field(default_factory=lambda: [])
|
|
160
|
+
|
|
161
|
+
# Internal: reference to original user config for saving
|
|
162
|
+
_user_config: UserConfig | None = None
|
|
28
163
|
|
|
29
164
|
@model_validator(mode="before")
|
|
30
165
|
@classmethod
|
|
31
|
-
def
|
|
32
|
-
raw_val: Any = data.get("
|
|
166
|
+
def _normalize_sub_agent_models(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
167
|
+
raw_val: Any = data.get("sub_agent_models") or {}
|
|
33
168
|
raw_models: dict[str, Any] = cast(dict[str, Any], raw_val) if isinstance(raw_val, dict) else {}
|
|
34
169
|
normalized: dict[str, str] = {}
|
|
35
170
|
key_map = {p.name.lower(): p.name for p in iter_sub_agent_profiles()}
|
|
36
171
|
for key, value in dict(raw_models).items():
|
|
37
172
|
canonical = key_map.get(str(key).lower(), str(key))
|
|
38
173
|
normalized[canonical] = str(value)
|
|
39
|
-
data["
|
|
174
|
+
data["sub_agent_models"] = normalized
|
|
40
175
|
return data
|
|
41
176
|
|
|
42
|
-
def
|
|
43
|
-
|
|
177
|
+
def set_user_config(self, user_config: UserConfig | None) -> None:
|
|
178
|
+
"""Set the user config reference for saving."""
|
|
179
|
+
object.__setattr__(self, "_user_config", user_config)
|
|
44
180
|
|
|
45
181
|
def get_model_config(self, model_name: str) -> llm_param.LLMConfigParameter:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return llm_param.LLMConfigParameter(
|
|
61
|
-
**provider.model_dump(),
|
|
62
|
-
**model.model_params.model_dump(),
|
|
63
|
-
)
|
|
182
|
+
for provider in self.provider_list:
|
|
183
|
+
# Resolve ${ENV_VAR} syntax for api_key
|
|
184
|
+
api_key = provider.get_resolved_api_key()
|
|
185
|
+
if not api_key:
|
|
186
|
+
continue
|
|
187
|
+
for model in provider.model_list:
|
|
188
|
+
if model.model_name == model_name:
|
|
189
|
+
provider_dump = provider.model_dump(exclude={"model_list"})
|
|
190
|
+
provider_dump["api_key"] = api_key
|
|
191
|
+
return llm_param.LLMConfigParameter(
|
|
192
|
+
**provider_dump,
|
|
193
|
+
**model.model_params.model_dump(),
|
|
194
|
+
)
|
|
64
195
|
|
|
65
|
-
|
|
196
|
+
raise ValueError(f"Unknown model: {model_name}")
|
|
197
|
+
|
|
198
|
+
def iter_model_entries(self, only_available: bool = False) -> list[ModelEntry]:
|
|
199
|
+
"""Return all model entries with their provider names.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
only_available: If True, only return models from providers with valid API keys.
|
|
66
203
|
"""
|
|
67
|
-
|
|
68
|
-
|
|
204
|
+
return [
|
|
205
|
+
ModelEntry(
|
|
206
|
+
model_name=model.model_name,
|
|
207
|
+
provider=provider.provider_name,
|
|
208
|
+
model_params=model.model_params,
|
|
209
|
+
)
|
|
210
|
+
for provider in self.provider_list
|
|
211
|
+
if not only_available or not provider.is_api_key_missing()
|
|
212
|
+
for model in provider.model_list
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
async def save(self) -> None:
|
|
216
|
+
"""Save user config to file (excludes builtin providers).
|
|
217
|
+
|
|
218
|
+
Only saves user-specific settings like main_model and custom providers.
|
|
219
|
+
Builtin providers are never written to the user config file.
|
|
69
220
|
"""
|
|
70
|
-
|
|
221
|
+
# Get user config, creating one if needed
|
|
222
|
+
user_config = self._user_config
|
|
223
|
+
if user_config is None:
|
|
224
|
+
user_config = UserConfig()
|
|
225
|
+
|
|
226
|
+
# Sync user-modifiable fields from merged config to user config
|
|
227
|
+
user_config.main_model = self.main_model
|
|
228
|
+
user_config.sub_agent_models = self.sub_agent_models
|
|
229
|
+
user_config.theme = self.theme
|
|
230
|
+
# Note: provider_list is NOT synced - user providers are already in user_config
|
|
231
|
+
|
|
232
|
+
config_dict = user_config.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
|
|
71
233
|
|
|
72
234
|
def _save_config() -> None:
|
|
73
235
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
-
|
|
236
|
+
yaml_content = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
|
|
237
|
+
_ = config_path.write_text(str(yaml_content or ""))
|
|
75
238
|
|
|
76
239
|
await asyncio.to_thread(_save_config)
|
|
77
240
|
|
|
78
241
|
|
|
79
|
-
def get_example_config() ->
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
242
|
+
def get_example_config() -> UserConfig:
|
|
243
|
+
"""Generate example config for user reference (will be commented out)."""
|
|
244
|
+
return UserConfig(
|
|
245
|
+
main_model="opus",
|
|
246
|
+
sub_agent_models={"explore": "haiku", "oracle": "gpt-5.2", "webagent": "sonnet", "task": "sonnet"},
|
|
83
247
|
provider_list=[
|
|
84
|
-
|
|
85
|
-
provider_name="
|
|
86
|
-
protocol=llm_param.LLMClientProtocol.
|
|
87
|
-
api_key="
|
|
88
|
-
base_url="https://api.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
max_tokens=32000,
|
|
103
|
-
verbosity="medium",
|
|
104
|
-
thinking=llm_param.Thinking(
|
|
105
|
-
reasoning_effort="medium",
|
|
106
|
-
reasoning_summary="auto",
|
|
107
|
-
type="enabled",
|
|
108
|
-
budget_tokens=None,
|
|
248
|
+
UserProviderConfig(
|
|
249
|
+
provider_name="my-provider",
|
|
250
|
+
protocol=llm_param.LLMClientProtocol.OPENAI,
|
|
251
|
+
api_key="${MY_API_KEY}",
|
|
252
|
+
base_url="https://api.example.com/v1",
|
|
253
|
+
model_list=[
|
|
254
|
+
ModelConfig(
|
|
255
|
+
model_name="my-model",
|
|
256
|
+
model_params=llm_param.LLMConfigModelParameter(
|
|
257
|
+
model="model-id-from-provider",
|
|
258
|
+
max_tokens=16000,
|
|
259
|
+
context_limit=200000,
|
|
260
|
+
cost=llm_param.Cost(
|
|
261
|
+
input=1,
|
|
262
|
+
output=10,
|
|
263
|
+
cache_read=0.1,
|
|
264
|
+
),
|
|
265
|
+
),
|
|
109
266
|
),
|
|
110
|
-
|
|
111
|
-
),
|
|
112
|
-
),
|
|
113
|
-
ModelConfig(
|
|
114
|
-
model_name="gpt-5.1-high",
|
|
115
|
-
provider="openai",
|
|
116
|
-
model_params=llm_param.LLMConfigModelParameter(
|
|
117
|
-
model="gpt-5.1-2025-11-13",
|
|
118
|
-
max_tokens=32000,
|
|
119
|
-
verbosity="medium",
|
|
120
|
-
thinking=llm_param.Thinking(
|
|
121
|
-
reasoning_effort="high",
|
|
122
|
-
reasoning_summary="auto",
|
|
123
|
-
type="enabled",
|
|
124
|
-
budget_tokens=None,
|
|
125
|
-
),
|
|
126
|
-
context_limit=368000,
|
|
127
|
-
),
|
|
128
|
-
),
|
|
129
|
-
ModelConfig(
|
|
130
|
-
model_name="haiku",
|
|
131
|
-
provider="openrouter",
|
|
132
|
-
model_params=llm_param.LLMConfigModelParameter(
|
|
133
|
-
model="anthropic/claude-haiku-4.5",
|
|
134
|
-
max_tokens=32000,
|
|
135
|
-
provider_routing=llm_param.OpenRouterProviderRouting(
|
|
136
|
-
sort="throughput",
|
|
137
|
-
),
|
|
138
|
-
context_limit=168000,
|
|
139
|
-
),
|
|
267
|
+
],
|
|
140
268
|
),
|
|
141
269
|
],
|
|
142
270
|
)
|
|
143
271
|
|
|
144
272
|
|
|
145
|
-
|
|
146
|
-
|
|
273
|
+
def _get_builtin_config() -> Config:
|
|
274
|
+
"""Load built-in provider configurations."""
|
|
275
|
+
# Re-validate to ensure compatibility with current ProviderConfig class
|
|
276
|
+
# (needed for tests that may monkeypatch the class)
|
|
277
|
+
providers = [ProviderConfig.model_validate(p.model_dump()) for p in get_builtin_provider_configs()]
|
|
278
|
+
return Config(provider_list=providers)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _merge_provider(builtin: ProviderConfig, user: UserProviderConfig) -> ProviderConfig:
|
|
282
|
+
"""Merge user provider config with builtin provider config.
|
|
283
|
+
|
|
284
|
+
Strategy:
|
|
285
|
+
- model_list: merge by model_name, user models override builtin models with same name
|
|
286
|
+
- Other fields (api_key, base_url, etc.): user config takes precedence if set
|
|
287
|
+
"""
|
|
288
|
+
# Merge model_list: builtin first, then user overrides/appends
|
|
289
|
+
merged_models: dict[str, ModelConfig] = {}
|
|
290
|
+
for m in builtin.model_list:
|
|
291
|
+
merged_models[m.model_name] = m
|
|
292
|
+
for m in user.model_list:
|
|
293
|
+
merged_models[m.model_name] = m
|
|
294
|
+
|
|
295
|
+
# For other fields, use user values if explicitly set, otherwise use builtin
|
|
296
|
+
# We check if user explicitly provided a value by comparing to defaults
|
|
297
|
+
merged_data = builtin.model_dump()
|
|
298
|
+
user_data = user.model_dump(exclude_defaults=True, exclude={"model_list"})
|
|
299
|
+
|
|
300
|
+
# Update with user's explicit settings
|
|
301
|
+
for key, value in user_data.items():
|
|
302
|
+
if value is not None:
|
|
303
|
+
merged_data[key] = value
|
|
304
|
+
|
|
305
|
+
merged_data["model_list"] = list(merged_models.values())
|
|
306
|
+
return ProviderConfig.model_validate(merged_data)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _merge_configs(user_config: UserConfig | None, builtin_config: Config) -> Config:
|
|
310
|
+
"""Merge user config with builtin config.
|
|
311
|
+
|
|
312
|
+
Strategy:
|
|
313
|
+
- provider_list: merge by provider_name
|
|
314
|
+
- Same name: merge model_list (user models override/append), other fields user takes precedence
|
|
315
|
+
- New name: add to list
|
|
316
|
+
- main_model: user config takes precedence
|
|
317
|
+
- sub_agent_models: merge, user takes precedence
|
|
318
|
+
- theme: user config takes precedence
|
|
319
|
+
|
|
320
|
+
The returned Config keeps a reference to user_config for saving.
|
|
321
|
+
"""
|
|
322
|
+
if user_config is None:
|
|
323
|
+
# No user config - return builtin with empty user config reference
|
|
324
|
+
merged = builtin_config.model_copy()
|
|
325
|
+
merged.set_user_config(None)
|
|
326
|
+
return merged
|
|
327
|
+
|
|
328
|
+
# Build lookup for builtin providers
|
|
329
|
+
builtin_providers: dict[str, ProviderConfig] = {p.provider_name: p for p in builtin_config.provider_list}
|
|
330
|
+
|
|
331
|
+
# Merge provider_list
|
|
332
|
+
merged_providers: dict[str, ProviderConfig] = dict(builtin_providers)
|
|
333
|
+
for user_provider in user_config.provider_list:
|
|
334
|
+
if user_provider.provider_name in builtin_providers:
|
|
335
|
+
# Merge with builtin provider
|
|
336
|
+
merged_providers[user_provider.provider_name] = _merge_provider(
|
|
337
|
+
builtin_providers[user_provider.provider_name], user_provider
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
# New provider from user - must have protocol
|
|
341
|
+
if user_provider.protocol is None:
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f"Provider '{user_provider.provider_name}' requires 'protocol' field (not a builtin provider)"
|
|
344
|
+
)
|
|
345
|
+
merged_providers[user_provider.provider_name] = ProviderConfig.model_validate(user_provider.model_dump())
|
|
346
|
+
|
|
347
|
+
# Merge sub_agent_models
|
|
348
|
+
merged_sub_agent_models = {**builtin_config.sub_agent_models, **user_config.sub_agent_models}
|
|
349
|
+
|
|
350
|
+
merged = Config(
|
|
351
|
+
main_model=user_config.main_model or builtin_config.main_model,
|
|
352
|
+
sub_agent_models=merged_sub_agent_models,
|
|
353
|
+
theme=user_config.theme or builtin_config.theme,
|
|
354
|
+
provider_list=list(merged_providers.values()),
|
|
355
|
+
)
|
|
356
|
+
# Keep reference to user config for saving
|
|
357
|
+
merged.set_user_config(user_config)
|
|
358
|
+
return merged
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _load_user_config() -> UserConfig | None:
|
|
362
|
+
"""Load user config from disk. Returns None if file doesn't exist or is empty."""
|
|
147
363
|
if not config_path.exists():
|
|
148
|
-
log(f"Config file not found: {config_path}")
|
|
149
|
-
example_config = get_example_config()
|
|
150
|
-
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
-
config_dict = example_config.model_dump(mode="json", exclude_none=True)
|
|
152
|
-
|
|
153
|
-
# Comment out all example config lines
|
|
154
|
-
yaml_str = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
|
|
155
|
-
commented_yaml = "\n".join(f"# {line}" if line.strip() else "#" for line in yaml_str.splitlines())
|
|
156
|
-
_ = config_path.write_text(commented_yaml)
|
|
157
|
-
|
|
158
|
-
log(f"Example config created at: {config_path}")
|
|
159
|
-
log("Please edit the config file to set up your models", style="yellow bold")
|
|
160
364
|
return None
|
|
161
365
|
|
|
162
366
|
config_yaml = config_path.read_text()
|
|
163
367
|
config_dict = yaml.safe_load(config_yaml)
|
|
164
368
|
|
|
165
369
|
if config_dict is None:
|
|
166
|
-
log(f"Config file is empty or all commented: {config_path}", style="red bold")
|
|
167
|
-
log("Please edit the config file to set up your models", style="yellow bold")
|
|
168
370
|
return None
|
|
169
371
|
|
|
170
372
|
try:
|
|
171
|
-
|
|
373
|
+
return UserConfig.model_validate(config_dict)
|
|
172
374
|
except ValidationError as e:
|
|
173
375
|
log(f"Invalid config file: {config_path}", style="red bold")
|
|
174
376
|
log(str(e), style="red")
|
|
175
377
|
raise ValueError(f"Invalid config file: {config_path}") from e
|
|
176
378
|
|
|
177
|
-
|
|
379
|
+
|
|
380
|
+
def create_example_config() -> bool:
|
|
381
|
+
"""Create example config file if it doesn't exist.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
True if file was created, False if it already exists.
|
|
385
|
+
"""
|
|
386
|
+
if example_config_path.exists():
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
example_config = get_example_config()
|
|
390
|
+
example_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
391
|
+
config_dict = example_config.model_dump(mode="json", exclude_none=True)
|
|
392
|
+
|
|
393
|
+
yaml_str = yaml.dump(config_dict, default_flow_style=False, sort_keys=False) or ""
|
|
394
|
+
header = "# Example configuration for klaude-code\n"
|
|
395
|
+
header += "# Copy this file to klaude-config.yaml and modify as needed.\n"
|
|
396
|
+
header += "# Run `klaude list` to see available models.\n"
|
|
397
|
+
header += "#\n"
|
|
398
|
+
header += "# Built-in providers (anthropic, openai, openrouter, deepseek) are available automatically.\n"
|
|
399
|
+
header += "# Just set the corresponding API key environment variable to use them.\n\n"
|
|
400
|
+
_ = example_config_path.write_text(header + yaml_str)
|
|
401
|
+
return True
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _load_config_uncached() -> Config:
|
|
405
|
+
"""Load and merge builtin + user config. Always returns a valid Config."""
|
|
406
|
+
builtin_config = _get_builtin_config()
|
|
407
|
+
user_config = _load_user_config()
|
|
408
|
+
|
|
409
|
+
return _merge_configs(user_config, builtin_config)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@lru_cache(maxsize=1)
|
|
413
|
+
def _load_config_cached() -> Config:
|
|
414
|
+
return _load_config_uncached()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def load_config() -> Config:
|
|
418
|
+
"""Load config from disk (builtin + user merged).
|
|
419
|
+
|
|
420
|
+
Always returns a valid Config. Use config.iter_model_entries(only_available=True)
|
|
421
|
+
to check if any models are actually usable.
|
|
422
|
+
"""
|
|
423
|
+
try:
|
|
424
|
+
return _load_config_cached()
|
|
425
|
+
except ValueError:
|
|
426
|
+
_load_config_cached.cache_clear()
|
|
427
|
+
raise
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def print_no_available_models_hint() -> None:
|
|
431
|
+
"""Print helpful message when no models are available due to missing API keys."""
|
|
432
|
+
log("No available models. Please set one of the following environment variables:", style="yellow")
|
|
433
|
+
log("")
|
|
434
|
+
for env_var in SUPPORTED_API_KEY_ENVS:
|
|
435
|
+
current_value = os.environ.get(env_var)
|
|
436
|
+
if current_value:
|
|
437
|
+
log(f" {env_var} = (set)", style="green")
|
|
438
|
+
else:
|
|
439
|
+
log(f" export {env_var}=<your-api-key>", style="dim")
|
|
440
|
+
log("")
|
|
441
|
+
log(f"Or add custom providers in: {config_path}", style="dim")
|
|
442
|
+
log(f"See example config: {example_config_path}", style="dim")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# Expose cache control for tests and callers that need to invalidate the cache.
|
|
446
|
+
load_config.cache_clear = _load_config_cached.cache_clear # type: ignore[attr-defined]
|