klaude-code 1.6.0__py3-none-any.whl → 1.7.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.
@@ -6,7 +6,7 @@ from rich.table import Table
6
6
  from rich.text import Text
7
7
 
8
8
  from klaude_code.config import Config
9
- from klaude_code.config.config import ModelConfig, ProviderConfig
9
+ from klaude_code.config.config import ModelConfig, ProviderConfig, parse_env_var_syntax
10
10
  from klaude_code.protocol.llm_param import LLMClientProtocol
11
11
  from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
12
12
  from klaude_code.ui.rich.theme import ThemeKey, get_theme
@@ -94,6 +94,29 @@ def format_api_key_display(provider: ProviderConfig) -> Text:
94
94
  return Text("N/A")
95
95
 
96
96
 
97
+ def format_env_var_display(value: str | None) -> Text:
98
+ """Format environment variable display with warning if not set."""
99
+ env_var, resolved = parse_env_var_syntax(value)
100
+
101
+ if env_var:
102
+ # Using ${ENV_VAR} syntax
103
+ if resolved:
104
+ return Text.assemble(
105
+ (f"${{{env_var}}} = ", "dim"),
106
+ (mask_api_key(resolved), ""),
107
+ )
108
+ else:
109
+ return Text.assemble(
110
+ (f"${{{env_var}}} ", ""),
111
+ ("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
112
+ )
113
+ elif value:
114
+ # Plain value
115
+ return Text(mask_api_key(value))
116
+ else:
117
+ return Text("N/A")
118
+
119
+
97
120
  def _get_model_params_display(model: ModelConfig) -> list[Text]:
98
121
  """Get display elements for model parameters."""
99
122
  params: list[Text] = []
@@ -162,15 +185,43 @@ def display_models_and_providers(config: Config):
162
185
  format_api_key_display(provider),
163
186
  )
164
187
 
188
+ # AWS Bedrock parameters
189
+ if provider.protocol == LLMClientProtocol.BEDROCK:
190
+ if provider.aws_access_key:
191
+ provider_info.add_row(
192
+ Text("AWS Key:", style=ThemeKey.CONFIG_PARAM_LABEL),
193
+ format_env_var_display(provider.aws_access_key),
194
+ )
195
+ if provider.aws_secret_key:
196
+ provider_info.add_row(
197
+ Text("AWS Secret:", style=ThemeKey.CONFIG_PARAM_LABEL),
198
+ format_env_var_display(provider.aws_secret_key),
199
+ )
200
+ if provider.aws_region:
201
+ provider_info.add_row(
202
+ Text("AWS Region:", style=ThemeKey.CONFIG_PARAM_LABEL),
203
+ format_env_var_display(provider.aws_region),
204
+ )
205
+ if provider.aws_session_token:
206
+ provider_info.add_row(
207
+ Text("AWS Token:", style=ThemeKey.CONFIG_PARAM_LABEL),
208
+ format_env_var_display(provider.aws_session_token),
209
+ )
210
+ if provider.aws_profile:
211
+ provider_info.add_row(
212
+ Text("AWS Profile:", style=ThemeKey.CONFIG_PARAM_LABEL),
213
+ format_env_var_display(provider.aws_profile),
214
+ )
215
+
165
216
  # Check if provider has valid API key
166
217
  provider_available = not provider.is_api_key_missing()
167
218
 
168
219
  # Models table for this provider
169
220
  models_table = Table.grid(padding=(0, 1), expand=True)
170
221
  models_table.add_column(width=2, no_wrap=True) # Status
171
- models_table.add_column(overflow="fold", ratio=1) # Name
172
- models_table.add_column(overflow="fold", ratio=2) # Model
173
- models_table.add_column(overflow="fold", ratio=3) # Params
222
+ models_table.add_column(overflow="fold", ratio=2) # Name
223
+ models_table.add_column(overflow="fold", ratio=3) # Model
224
+ models_table.add_column(overflow="fold", ratio=4) # Params
174
225
 
175
226
  # Add header
176
227
  models_table.add_row(
@@ -22,8 +22,9 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
22
22
  log(f"Sessions to delete ({len(sessions)}):")
23
23
  for s in sessions:
24
24
  msg_count_display = "N/A" if s.messages_count == -1 else str(s.messages_count)
25
- first_msg = (s.first_user_message or "").strip().replace("\n", " ")[:50]
26
- if len(s.first_user_message or "") > 50:
25
+ first_msg_text = s.user_messages[0] if s.user_messages else ""
26
+ first_msg = first_msg_text.strip().replace("\n", " ")[:50]
27
+ if len(first_msg_text) > 50:
27
28
  first_msg += "..."
28
29
  log(f" {_fmt(s.updated_at)} {msg_count_display:>3} msgs {first_msg}")
29
30
 
@@ -7,7 +7,7 @@ provider_list:
7
7
  protocol: anthropic
8
8
  api_key: ${ANTHROPIC_API_KEY}
9
9
  model_list:
10
- - model_name: sonnet
10
+ - model_name: sonnet@ant
11
11
  model_params:
12
12
  model: claude-sonnet-4-5-20250929
13
13
  context_limit: 200000
@@ -18,7 +18,7 @@ provider_list:
18
18
  output: 15.0
19
19
  cache_read: 0.3
20
20
  cache_write: 3.75
21
- - model_name: opus
21
+ - model_name: opus@ant
22
22
  model_params:
23
23
  model: claude-opus-4-5-20251101
24
24
  context_limit: 200000
@@ -194,6 +194,41 @@ provider_list:
194
194
  output: 1.74
195
195
  cache_read: 0.04
196
196
 
197
+ - provider_name: google
198
+ protocol: google
199
+ api_key: ${GOOGLE_API_KEY}
200
+ model_list:
201
+ - model_name: gemini-pro@google
202
+ model_params:
203
+ model: gemini-3-pro-preview
204
+ context_limit: 1048576
205
+ cost:
206
+ input: 2.0
207
+ output: 12.0
208
+ cache_read: 0.2
209
+ - model_name: gemini-flash@google
210
+ model_params:
211
+ model: gemini-3-flash-preview
212
+ context_limit: 1048576
213
+ cost:
214
+ input: 0.5
215
+ output: 3.0
216
+ cache_read: 0.05
217
+ - provider_name: bedrock
218
+ protocol: bedrock
219
+ aws_access_key: ${AWS_ACCESS_KEY_ID}
220
+ aws_secret_key: ${AWS_SECRET_ACCESS_KEY}
221
+ aws_region: ${AWS_REGION}
222
+ model_list:
223
+ - model_name: sonnet@bedrock
224
+ model_params:
225
+ model: us.anthropic.claude-sonnet-4-5-20250929-v1:0
226
+ context_limit: 200000
227
+ cost:
228
+ input: 3.0
229
+ output: 15.0
230
+ cache_read: 0.3
231
+ cache_write: 3.75
197
232
  - provider_name: deepseek
198
233
  protocol: anthropic
199
234
  api_key: ${DEEPSEEK_API_KEY}
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
17
17
  # All supported API key environment variables
18
18
  SUPPORTED_API_KEY_ENVS = [
19
19
  "ANTHROPIC_API_KEY",
20
+ "GOOGLE_API_KEY",
20
21
  "OPENAI_API_KEY",
21
22
  "OPENROUTER_API_KEY",
22
23
  "DEEPSEEK_API_KEY",
@@ -77,6 +77,7 @@ class ProviderConfig(llm_param.LLMConfigProviderParameter):
77
77
  """Check if the API key is missing (either not set or env var not found).
78
78
 
79
79
  For codex protocol, checks OAuth login status instead of API key.
80
+ For bedrock protocol, checks AWS credentials instead of API key.
80
81
  """
81
82
  from klaude_code.protocol.llm_param import LLMClientProtocol
82
83
 
@@ -89,6 +90,19 @@ class ProviderConfig(llm_param.LLMConfigProviderParameter):
89
90
  # Consider available if logged in and token not expired
90
91
  return state is None or state.is_expired()
91
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
+
92
106
  return self.get_resolved_api_key() is None
93
107
 
94
108
 
@@ -121,6 +121,13 @@ def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
121
121
  return f"enabled (budget_tokens={thinking.budget_tokens})"
122
122
  return "not set"
123
123
 
124
+ if protocol == llm_param.LLMClientProtocol.GOOGLE:
125
+ if thinking.type == "disabled":
126
+ return "off"
127
+ if thinking.type == "enabled":
128
+ return f"enabled (budget_tokens={thinking.budget_tokens})"
129
+ return "not set"
130
+
124
131
  return "unknown protocol"
125
132
 
126
133
 
@@ -230,6 +237,13 @@ def get_thinking_picker_data(config: llm_param.LLMConfigParameter) -> ThinkingPi
230
237
  current_value=_get_current_budget_value(thinking),
231
238
  )
232
239
 
240
+ if protocol == llm_param.LLMClientProtocol.GOOGLE:
241
+ return ThinkingPickerData(
242
+ options=_build_budget_options(),
243
+ message="Select thinking level:",
244
+ current_value=_get_current_budget_value(thinking),
245
+ )
246
+
233
247
  return None
234
248
 
235
249
 
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import os
3
3
  from collections.abc import AsyncGenerator
4
- from typing import override
4
+ from typing import Any, override
5
5
 
6
6
  import anthropic
7
7
  import httpx
@@ -58,6 +58,130 @@ def build_payload(param: llm_param.LLMCallParameter) -> MessageCreateParamsStrea
58
58
  return payload
59
59
 
60
60
 
61
+ async def parse_anthropic_stream(
62
+ stream: Any,
63
+ param: llm_param.LLMCallParameter,
64
+ metadata_tracker: MetadataTracker,
65
+ ) -> AsyncGenerator[model.ConversationItem]:
66
+ """Parse Anthropic beta messages stream and yield conversation items.
67
+
68
+ This function is shared between AnthropicClient and BedrockClient.
69
+ """
70
+ accumulated_thinking: list[str] = []
71
+ accumulated_content: list[str] = []
72
+ response_id: str | None = None
73
+
74
+ current_tool_name: str | None = None
75
+ current_tool_call_id: str | None = None
76
+ current_tool_inputs: list[str] | None = None
77
+
78
+ input_token = 0
79
+ cached_token = 0
80
+
81
+ async for event in await stream:
82
+ log_debug(
83
+ f"[{event.type}]",
84
+ event.model_dump_json(exclude_none=True),
85
+ style="blue",
86
+ debug_type=DebugType.LLM_STREAM,
87
+ )
88
+ match event:
89
+ case BetaRawMessageStartEvent() as event:
90
+ response_id = event.message.id
91
+ cached_token = event.message.usage.cache_read_input_tokens or 0
92
+ input_token = event.message.usage.input_tokens
93
+ yield model.StartItem(response_id=response_id)
94
+ case BetaRawContentBlockDeltaEvent() as event:
95
+ match event.delta:
96
+ case BetaThinkingDelta() as delta:
97
+ if delta.thinking:
98
+ metadata_tracker.record_token()
99
+ accumulated_thinking.append(delta.thinking)
100
+ yield model.ReasoningTextDelta(
101
+ content=delta.thinking,
102
+ response_id=response_id,
103
+ )
104
+ case BetaSignatureDelta() as delta:
105
+ yield model.ReasoningEncryptedItem(
106
+ encrypted_content=delta.signature,
107
+ response_id=response_id,
108
+ model=str(param.model),
109
+ )
110
+ case BetaTextDelta() as delta:
111
+ if delta.text:
112
+ metadata_tracker.record_token()
113
+ accumulated_content.append(delta.text)
114
+ yield model.AssistantMessageDelta(
115
+ content=delta.text,
116
+ response_id=response_id,
117
+ )
118
+ case BetaInputJSONDelta() as delta:
119
+ if current_tool_inputs is not None:
120
+ if delta.partial_json:
121
+ metadata_tracker.record_token()
122
+ current_tool_inputs.append(delta.partial_json)
123
+ case _:
124
+ pass
125
+ case BetaRawContentBlockStartEvent() as event:
126
+ match event.content_block:
127
+ case BetaToolUseBlock() as block:
128
+ metadata_tracker.record_token()
129
+ yield model.ToolCallStartItem(
130
+ response_id=response_id,
131
+ call_id=block.id,
132
+ name=block.name,
133
+ )
134
+ current_tool_name = block.name
135
+ current_tool_call_id = block.id
136
+ current_tool_inputs = []
137
+ case _:
138
+ pass
139
+ case BetaRawContentBlockStopEvent():
140
+ if len(accumulated_thinking) > 0:
141
+ metadata_tracker.record_token()
142
+ full_thinking = "".join(accumulated_thinking)
143
+ yield model.ReasoningTextItem(
144
+ content=full_thinking,
145
+ response_id=response_id,
146
+ model=str(param.model),
147
+ )
148
+ accumulated_thinking.clear()
149
+ if len(accumulated_content) > 0:
150
+ metadata_tracker.record_token()
151
+ yield model.AssistantMessageItem(
152
+ content="".join(accumulated_content),
153
+ response_id=response_id,
154
+ )
155
+ accumulated_content.clear()
156
+ if current_tool_name and current_tool_call_id:
157
+ metadata_tracker.record_token()
158
+ yield model.ToolCallItem(
159
+ name=current_tool_name,
160
+ call_id=current_tool_call_id,
161
+ arguments="".join(current_tool_inputs) if current_tool_inputs else "",
162
+ response_id=response_id,
163
+ )
164
+ current_tool_name = None
165
+ current_tool_call_id = None
166
+ current_tool_inputs = None
167
+ case BetaRawMessageDeltaEvent() as event:
168
+ metadata_tracker.set_usage(
169
+ model.Usage(
170
+ input_tokens=input_token + cached_token,
171
+ output_tokens=event.usage.output_tokens,
172
+ cached_tokens=cached_token,
173
+ context_size=input_token + cached_token + event.usage.output_tokens,
174
+ context_limit=param.context_limit,
175
+ max_tokens=param.max_tokens,
176
+ )
177
+ )
178
+ metadata_tracker.set_model_name(str(param.model))
179
+ metadata_tracker.set_response_id(response_id)
180
+ yield metadata_tracker.finalize()
181
+ case _:
182
+ pass
183
+
184
+
61
185
  @register(llm_param.LLMClientProtocol.ANTHROPIC)
62
186
  class AnthropicClient(LLMClientABC):
63
187
  def __init__(self, config: llm_param.LLMConfigParameter):
@@ -102,119 +226,8 @@ class AnthropicClient(LLMClientABC):
102
226
  extra_headers={"extra": json.dumps({"session_id": param.session_id}, sort_keys=True)},
103
227
  )
104
228
 
105
- accumulated_thinking: list[str] = []
106
- accumulated_content: list[str] = []
107
- response_id: str | None = None
108
-
109
- current_tool_name: str | None = None
110
- current_tool_call_id: str | None = None
111
- current_tool_inputs: list[str] | None = None
112
-
113
- input_token = 0
114
- cached_token = 0
115
-
116
229
  try:
117
- async for event in await stream:
118
- log_debug(
119
- f"[{event.type}]",
120
- event.model_dump_json(exclude_none=True),
121
- style="blue",
122
- debug_type=DebugType.LLM_STREAM,
123
- )
124
- match event:
125
- case BetaRawMessageStartEvent() as event:
126
- response_id = event.message.id
127
- cached_token = event.message.usage.cache_read_input_tokens or 0
128
- input_token = event.message.usage.input_tokens
129
- yield model.StartItem(response_id=response_id)
130
- case BetaRawContentBlockDeltaEvent() as event:
131
- match event.delta:
132
- case BetaThinkingDelta() as delta:
133
- if delta.thinking:
134
- metadata_tracker.record_token()
135
- accumulated_thinking.append(delta.thinking)
136
- yield model.ReasoningTextDelta(
137
- content=delta.thinking,
138
- response_id=response_id,
139
- )
140
- case BetaSignatureDelta() as delta:
141
- yield model.ReasoningEncryptedItem(
142
- encrypted_content=delta.signature,
143
- response_id=response_id,
144
- model=str(param.model),
145
- )
146
- case BetaTextDelta() as delta:
147
- if delta.text:
148
- metadata_tracker.record_token()
149
- accumulated_content.append(delta.text)
150
- yield model.AssistantMessageDelta(
151
- content=delta.text,
152
- response_id=response_id,
153
- )
154
- case BetaInputJSONDelta() as delta:
155
- if current_tool_inputs is not None:
156
- if delta.partial_json:
157
- metadata_tracker.record_token()
158
- current_tool_inputs.append(delta.partial_json)
159
- case _:
160
- pass
161
- case BetaRawContentBlockStartEvent() as event:
162
- match event.content_block:
163
- case BetaToolUseBlock() as block:
164
- metadata_tracker.record_token()
165
- yield model.ToolCallStartItem(
166
- response_id=response_id,
167
- call_id=block.id,
168
- name=block.name,
169
- )
170
- current_tool_name = block.name
171
- current_tool_call_id = block.id
172
- current_tool_inputs = []
173
- case _:
174
- pass
175
- case BetaRawContentBlockStopEvent() as event:
176
- if len(accumulated_thinking) > 0:
177
- metadata_tracker.record_token()
178
- full_thinking = "".join(accumulated_thinking)
179
- yield model.ReasoningTextItem(
180
- content=full_thinking,
181
- response_id=response_id,
182
- model=str(param.model),
183
- )
184
- accumulated_thinking.clear()
185
- if len(accumulated_content) > 0:
186
- metadata_tracker.record_token()
187
- yield model.AssistantMessageItem(
188
- content="".join(accumulated_content),
189
- response_id=response_id,
190
- )
191
- accumulated_content.clear()
192
- if current_tool_name and current_tool_call_id:
193
- metadata_tracker.record_token()
194
- yield model.ToolCallItem(
195
- name=current_tool_name,
196
- call_id=current_tool_call_id,
197
- arguments="".join(current_tool_inputs) if current_tool_inputs else "",
198
- response_id=response_id,
199
- )
200
- current_tool_name = None
201
- current_tool_call_id = None
202
- current_tool_inputs = None
203
- case BetaRawMessageDeltaEvent() as event:
204
- metadata_tracker.set_usage(
205
- model.Usage(
206
- input_tokens=input_token + cached_token,
207
- output_tokens=event.usage.output_tokens,
208
- cached_tokens=cached_token,
209
- context_size=input_token + cached_token + event.usage.output_tokens,
210
- context_limit=param.context_limit,
211
- max_tokens=param.max_tokens,
212
- )
213
- )
214
- metadata_tracker.set_model_name(str(param.model))
215
- metadata_tracker.set_response_id(response_id)
216
- yield metadata_tracker.finalize()
217
- case _:
218
- pass
230
+ async for item in parse_anthropic_stream(stream, param, metadata_tracker):
231
+ yield item
219
232
  except (APIError, httpx.HTTPError) as e:
220
233
  yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
@@ -0,0 +1,3 @@
1
+ from klaude_code.llm.bedrock.client import BedrockClient
2
+
3
+ __all__ = ["BedrockClient"]
@@ -0,0 +1,60 @@
1
+ """AWS Bedrock LLM client using Anthropic SDK."""
2
+
3
+ import json
4
+ from collections.abc import AsyncGenerator
5
+ from typing import override
6
+
7
+ import anthropic
8
+ import httpx
9
+ from anthropic import APIError
10
+
11
+ from klaude_code.llm.anthropic.client import build_payload, parse_anthropic_stream
12
+ from klaude_code.llm.client import LLMClientABC
13
+ from klaude_code.llm.input_common import apply_config_defaults
14
+ from klaude_code.llm.registry import register
15
+ from klaude_code.llm.usage import MetadataTracker
16
+ from klaude_code.protocol import llm_param, model
17
+ from klaude_code.trace import DebugType, log_debug
18
+
19
+
20
+ @register(llm_param.LLMClientProtocol.BEDROCK)
21
+ class BedrockClient(LLMClientABC):
22
+ """LLM client for AWS Bedrock using Anthropic SDK."""
23
+
24
+ def __init__(self, config: llm_param.LLMConfigParameter):
25
+ super().__init__(config)
26
+ self.client = anthropic.AsyncAnthropicBedrock(
27
+ aws_access_key=config.aws_access_key,
28
+ aws_secret_key=config.aws_secret_key,
29
+ aws_region=config.aws_region,
30
+ aws_session_token=config.aws_session_token,
31
+ aws_profile=config.aws_profile,
32
+ timeout=httpx.Timeout(300.0, connect=15.0, read=285.0),
33
+ )
34
+
35
+ @classmethod
36
+ @override
37
+ def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
38
+ return cls(config)
39
+
40
+ @override
41
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
42
+ param = apply_config_defaults(param, self.get_llm_config())
43
+
44
+ metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
45
+
46
+ payload = build_payload(param)
47
+
48
+ log_debug(
49
+ json.dumps(payload, ensure_ascii=False, default=str),
50
+ style="yellow",
51
+ debug_type=DebugType.LLM_PAYLOAD,
52
+ )
53
+
54
+ stream = self.client.beta.messages.create(**payload)
55
+
56
+ try:
57
+ async for item in parse_anthropic_stream(stream, param, metadata_tracker):
58
+ yield item
59
+ except (APIError, httpx.HTTPError) as e:
60
+ yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
@@ -0,0 +1,3 @@
1
+ from .client import GoogleClient
2
+
3
+ __all__ = ["GoogleClient"]
@@ -0,0 +1,309 @@
1
+ # pyright: reportUnknownMemberType=false
2
+ # pyright: reportUnknownVariableType=false
3
+ # pyright: reportUnknownArgumentType=false
4
+ # pyright: reportAttributeAccessIssue=false
5
+
6
+ import json
7
+ from collections.abc import AsyncGenerator, AsyncIterator
8
+ from typing import Any, cast, override
9
+ from uuid import uuid4
10
+
11
+ import httpx
12
+ from google.genai import Client
13
+ from google.genai.errors import APIError, ClientError, ServerError
14
+ from google.genai.types import (
15
+ FunctionCallingConfig,
16
+ FunctionCallingConfigMode,
17
+ GenerateContentConfig,
18
+ HttpOptions,
19
+ ThinkingConfig,
20
+ ToolConfig,
21
+ UsageMetadata,
22
+ )
23
+
24
+ from klaude_code.llm.client import LLMClientABC
25
+ from klaude_code.llm.google.input import convert_history_to_contents, convert_tool_schema
26
+ from klaude_code.llm.input_common import apply_config_defaults
27
+ from klaude_code.llm.registry import register
28
+ from klaude_code.llm.usage import MetadataTracker
29
+ from klaude_code.protocol import llm_param, model
30
+ from klaude_code.trace import DebugType, log_debug
31
+
32
+
33
+ def _build_config(param: llm_param.LLMCallParameter) -> GenerateContentConfig:
34
+ tool_list = convert_tool_schema(param.tools)
35
+ tool_config: ToolConfig | None = None
36
+
37
+ if tool_list:
38
+ tool_config = ToolConfig(
39
+ function_calling_config=FunctionCallingConfig(
40
+ mode=FunctionCallingConfigMode.AUTO,
41
+ # Gemini streams tool args; keep this enabled to maximize fidelity.
42
+ stream_function_call_arguments=True,
43
+ )
44
+ )
45
+
46
+ thinking_config: ThinkingConfig | None = None
47
+ if param.thinking and param.thinking.type == "enabled":
48
+ thinking_config = ThinkingConfig(
49
+ include_thoughts=True,
50
+ thinking_budget=param.thinking.budget_tokens,
51
+ )
52
+
53
+ return GenerateContentConfig(
54
+ system_instruction=param.system,
55
+ temperature=param.temperature,
56
+ max_output_tokens=param.max_tokens,
57
+ tools=tool_list or None,
58
+ tool_config=tool_config,
59
+ thinking_config=thinking_config,
60
+ )
61
+
62
+
63
+ def _usage_from_metadata(
64
+ usage: UsageMetadata | None,
65
+ *,
66
+ context_limit: int | None,
67
+ max_tokens: int | None,
68
+ ) -> model.Usage | None:
69
+ if usage is None:
70
+ return None
71
+
72
+ cached = usage.cached_content_token_count or 0
73
+ prompt = usage.prompt_token_count or 0
74
+ response = usage.response_token_count or 0
75
+ thoughts = usage.thoughts_token_count or 0
76
+
77
+ total = usage.total_token_count
78
+ if total is None:
79
+ total = prompt + cached + response + thoughts
80
+
81
+ return model.Usage(
82
+ input_tokens=prompt + cached,
83
+ cached_tokens=cached,
84
+ output_tokens=response + thoughts,
85
+ reasoning_tokens=thoughts,
86
+ context_size=total,
87
+ context_limit=context_limit,
88
+ max_tokens=max_tokens,
89
+ )
90
+
91
+
92
+ def _partial_arg_value(partial: Any) -> Any:
93
+ if getattr(partial, "string_value", None) is not None:
94
+ return partial.string_value
95
+ if getattr(partial, "number_value", None) is not None:
96
+ return partial.number_value
97
+ if getattr(partial, "bool_value", None) is not None:
98
+ return partial.bool_value
99
+ if getattr(partial, "null_value", None) is not None:
100
+ return None
101
+ return None
102
+
103
+
104
+ def _merge_partial_args(dst: dict[str, Any], partial_args: list[Any] | None) -> None:
105
+ if not partial_args:
106
+ return
107
+ for partial in partial_args:
108
+ json_path = getattr(partial, "json_path", None)
109
+ if not isinstance(json_path, str) or not json_path.startswith("$."):
110
+ continue
111
+ key = json_path[2:]
112
+ if not key or any(ch in key for ch in "[]"):
113
+ continue
114
+ dst[key] = _partial_arg_value(partial)
115
+
116
+
117
+ async def parse_google_stream(
118
+ stream: AsyncIterator[Any],
119
+ param: llm_param.LLMCallParameter,
120
+ metadata_tracker: MetadataTracker,
121
+ ) -> AsyncGenerator[model.ConversationItem]:
122
+ response_id: str | None = None
123
+ started = False
124
+
125
+ accumulated_text: list[str] = []
126
+ accumulated_thoughts: list[str] = []
127
+ thought_signature: str | None = None
128
+
129
+ # Track tool calls where args arrive as partial updates.
130
+ partial_args_by_call: dict[str, dict[str, Any]] = {}
131
+ started_tool_calls: dict[str, str] = {} # call_id -> name
132
+ started_tool_items: set[str] = set()
133
+ emitted_tool_items: set[str] = set()
134
+
135
+ last_usage_metadata: UsageMetadata | None = None
136
+
137
+ async for chunk in stream:
138
+ log_debug(
139
+ chunk.model_dump_json(exclude_none=True),
140
+ style="blue",
141
+ debug_type=DebugType.LLM_STREAM,
142
+ )
143
+
144
+ if response_id is None:
145
+ response_id = getattr(chunk, "response_id", None) or uuid4().hex
146
+ assert response_id is not None
147
+ if not started:
148
+ started = True
149
+ yield model.StartItem(response_id=response_id)
150
+
151
+ if getattr(chunk, "usage_metadata", None) is not None:
152
+ last_usage_metadata = chunk.usage_metadata
153
+
154
+ candidates = getattr(chunk, "candidates", None) or []
155
+ candidate0 = candidates[0] if candidates else None
156
+ content = getattr(candidate0, "content", None) if candidate0 else None
157
+ parts = getattr(content, "parts", None) if content else None
158
+ if not parts:
159
+ continue
160
+
161
+ for part in parts:
162
+ if getattr(part, "text", None) is not None:
163
+ metadata_tracker.record_token()
164
+ text = part.text
165
+ if getattr(part, "thought", False) is True:
166
+ accumulated_thoughts.append(text)
167
+ if getattr(part, "thought_signature", None):
168
+ thought_signature = part.thought_signature
169
+ yield model.ReasoningTextDelta(content=text, response_id=response_id)
170
+ else:
171
+ accumulated_text.append(text)
172
+ yield model.AssistantMessageDelta(content=text, response_id=response_id)
173
+
174
+ function_call = getattr(part, "function_call", None)
175
+ if function_call is None:
176
+ continue
177
+
178
+ metadata_tracker.record_token()
179
+ call_id = getattr(function_call, "id", None) or uuid4().hex
180
+ name = getattr(function_call, "name", None) or ""
181
+ started_tool_calls.setdefault(call_id, name)
182
+
183
+ if call_id not in started_tool_items:
184
+ started_tool_items.add(call_id)
185
+ yield model.ToolCallStartItem(response_id=response_id, call_id=call_id, name=name)
186
+
187
+ args_obj = getattr(function_call, "args", None)
188
+ if args_obj is not None:
189
+ emitted_tool_items.add(call_id)
190
+ yield model.ToolCallItem(
191
+ response_id=response_id,
192
+ call_id=call_id,
193
+ name=name,
194
+ arguments=json.dumps(args_obj, ensure_ascii=False),
195
+ )
196
+ continue
197
+
198
+ partial_args = getattr(function_call, "partial_args", None)
199
+ if partial_args is not None:
200
+ acc = partial_args_by_call.setdefault(call_id, {})
201
+ _merge_partial_args(acc, partial_args)
202
+
203
+ will_continue = getattr(function_call, "will_continue", None)
204
+ if will_continue is False and call_id in partial_args_by_call and call_id not in emitted_tool_items:
205
+ emitted_tool_items.add(call_id)
206
+ yield model.ToolCallItem(
207
+ response_id=response_id,
208
+ call_id=call_id,
209
+ name=name,
210
+ arguments=json.dumps(partial_args_by_call[call_id], ensure_ascii=False),
211
+ )
212
+
213
+ # Flush any pending tool calls that never produced args.
214
+ for call_id, name in started_tool_calls.items():
215
+ if call_id in emitted_tool_items:
216
+ continue
217
+ args = partial_args_by_call.get(call_id, {})
218
+ emitted_tool_items.add(call_id)
219
+ yield model.ToolCallItem(
220
+ response_id=response_id,
221
+ call_id=call_id,
222
+ name=name,
223
+ arguments=json.dumps(args, ensure_ascii=False),
224
+ )
225
+
226
+ if accumulated_thoughts:
227
+ metadata_tracker.record_token()
228
+ yield model.ReasoningTextItem(
229
+ content="".join(accumulated_thoughts),
230
+ response_id=response_id,
231
+ model=str(param.model),
232
+ )
233
+ if thought_signature:
234
+ yield model.ReasoningEncryptedItem(
235
+ encrypted_content=thought_signature,
236
+ response_id=response_id,
237
+ model=str(param.model),
238
+ format="google_thought_signature",
239
+ )
240
+
241
+ if accumulated_text:
242
+ metadata_tracker.record_token()
243
+ yield model.AssistantMessageItem(content="".join(accumulated_text), response_id=response_id)
244
+
245
+ usage = _usage_from_metadata(last_usage_metadata, context_limit=param.context_limit, max_tokens=param.max_tokens)
246
+ if usage is not None:
247
+ metadata_tracker.set_usage(usage)
248
+ metadata_tracker.set_model_name(str(param.model))
249
+ metadata_tracker.set_response_id(response_id)
250
+ yield metadata_tracker.finalize()
251
+
252
+
253
+ @register(llm_param.LLMClientProtocol.GOOGLE)
254
+ class GoogleClient(LLMClientABC):
255
+ def __init__(self, config: llm_param.LLMConfigParameter):
256
+ super().__init__(config)
257
+ http_options: HttpOptions | None = None
258
+ if config.base_url:
259
+ # If base_url already contains version path, don't append api_version.
260
+ http_options = HttpOptions(base_url=str(config.base_url), api_version="")
261
+
262
+ self.client = Client(
263
+ api_key=config.api_key,
264
+ http_options=http_options,
265
+ )
266
+
267
+ @classmethod
268
+ @override
269
+ def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
270
+ return cls(config)
271
+
272
+ @override
273
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
274
+ param = apply_config_defaults(param, self.get_llm_config())
275
+ metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
276
+
277
+ contents = convert_history_to_contents(param.input, model_name=str(param.model))
278
+ config = _build_config(param)
279
+
280
+ log_debug(
281
+ json.dumps(
282
+ {
283
+ "model": str(param.model),
284
+ "contents": [c.model_dump(exclude_none=True) for c in contents],
285
+ "config": config.model_dump(exclude_none=True),
286
+ },
287
+ ensure_ascii=False,
288
+ ),
289
+ style="yellow",
290
+ debug_type=DebugType.LLM_PAYLOAD,
291
+ )
292
+
293
+ try:
294
+ stream = await self.client.aio.models.generate_content_stream(
295
+ model=str(param.model),
296
+ contents=cast(Any, contents),
297
+ config=config,
298
+ )
299
+ except (APIError, ClientError, ServerError, httpx.HTTPError) as e:
300
+ yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
301
+ yield metadata_tracker.finalize()
302
+ return
303
+
304
+ try:
305
+ async for item in parse_google_stream(stream, param=param, metadata_tracker=metadata_tracker):
306
+ yield item
307
+ except (APIError, ClientError, ServerError, httpx.HTTPError) as e:
308
+ yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
309
+ yield metadata_tracker.finalize()
@@ -0,0 +1,215 @@
1
+ # pyright: reportReturnType=false
2
+ # pyright: reportArgumentType=false
3
+ # pyright: reportUnknownMemberType=false
4
+ # pyright: reportAttributeAccessIssue=false
5
+
6
+ import json
7
+ from base64 import b64decode
8
+ from binascii import Error as BinasciiError
9
+ from typing import Any
10
+
11
+ from google.genai import types
12
+
13
+ from klaude_code.llm.input_common import AssistantGroup, ToolGroup, UserGroup, merge_reminder_text, parse_message_groups
14
+ from klaude_code.protocol import llm_param, model
15
+
16
+
17
+ def _data_url_to_blob(url: str) -> types.Blob:
18
+ header_and_media = url.split(",", 1)
19
+ if len(header_and_media) != 2:
20
+ raise ValueError("Invalid data URL for image: missing comma separator")
21
+ header, base64_data = header_and_media
22
+ if not header.startswith("data:"):
23
+ raise ValueError("Invalid data URL for image: missing data: prefix")
24
+ if ";base64" not in header:
25
+ raise ValueError("Invalid data URL for image: missing base64 marker")
26
+
27
+ media_type = header[5:].split(";", 1)[0]
28
+ base64_payload = base64_data.strip()
29
+ if base64_payload == "":
30
+ raise ValueError("Inline image data is empty")
31
+
32
+ try:
33
+ decoded = b64decode(base64_payload, validate=True)
34
+ except (BinasciiError, ValueError) as exc:
35
+ raise ValueError("Inline image data is not valid base64") from exc
36
+
37
+ return types.Blob(data=decoded, mime_type=media_type)
38
+
39
+
40
+ def _image_part_to_part(image: model.ImageURLPart) -> types.Part:
41
+ url = image.image_url.url
42
+ if url.startswith("data:"):
43
+ return types.Part(inline_data=_data_url_to_blob(url))
44
+ # Best-effort: Gemini supports file URIs, and may accept public HTTPS URLs.
45
+ return types.Part(file_data=types.FileData(file_uri=url))
46
+
47
+
48
+ def _user_group_to_content(group: UserGroup) -> types.Content:
49
+ parts: list[types.Part] = []
50
+ for text in group.text_parts:
51
+ parts.append(types.Part(text=text + "\n"))
52
+ for image in group.images:
53
+ parts.append(_image_part_to_part(image))
54
+ if not parts:
55
+ parts.append(types.Part(text=""))
56
+ return types.Content(role="user", parts=parts)
57
+
58
+
59
+ def _tool_groups_to_content(groups: list[ToolGroup], model_name: str | None) -> list[types.Content]:
60
+ supports_multimodal_function_response = bool(model_name and "gemini-3" in model_name.lower())
61
+
62
+ response_parts: list[types.Part] = []
63
+ extra_image_contents: list[types.Content] = []
64
+
65
+ for group in groups:
66
+ merged_text = merge_reminder_text(
67
+ group.tool_result.output or "<system-reminder>Tool ran without output or errors</system-reminder>",
68
+ group.reminder_texts,
69
+ )
70
+ has_text = merged_text.strip() != ""
71
+
72
+ images = list(group.tool_result.images or []) + list(group.reminder_images)
73
+ image_parts: list[types.Part] = []
74
+ for image in images:
75
+ try:
76
+ image_parts.append(_image_part_to_part(image))
77
+ except ValueError:
78
+ # Skip invalid data URLs
79
+ continue
80
+
81
+ has_images = len(image_parts) > 0
82
+ response_value = merged_text if has_text else "(see attached image)" if has_images else ""
83
+ response_payload = (
84
+ {"error": response_value} if group.tool_result.status == "error" else {"output": response_value}
85
+ )
86
+
87
+ function_response = types.FunctionResponse(
88
+ id=group.tool_result.call_id,
89
+ name=group.tool_result.tool_name or "",
90
+ response=response_payload,
91
+ parts=image_parts if (has_images and supports_multimodal_function_response) else None,
92
+ )
93
+ response_parts.append(types.Part(function_response=function_response))
94
+
95
+ if has_images and not supports_multimodal_function_response:
96
+ extra_image_contents.append(
97
+ types.Content(role="user", parts=[types.Part(text="Tool result image:"), *image_parts])
98
+ )
99
+
100
+ contents: list[types.Content] = []
101
+ if response_parts:
102
+ contents.append(types.Content(role="user", parts=response_parts))
103
+ contents.extend(extra_image_contents)
104
+ return contents
105
+
106
+
107
+ def _assistant_group_to_content(group: AssistantGroup, model_name: str | None) -> types.Content | None:
108
+ parts: list[types.Part] = []
109
+
110
+ degraded_thinking_texts: list[str] = []
111
+ pending_thought_text: str | None = None
112
+ pending_thought_signature: str | None = None
113
+
114
+ for item in group.reasoning_items:
115
+ match item:
116
+ case model.ReasoningTextItem():
117
+ if not item.content:
118
+ continue
119
+ if model_name is not None and item.model is not None and item.model != model_name:
120
+ degraded_thinking_texts.append(item.content)
121
+ else:
122
+ pending_thought_text = item.content
123
+ case model.ReasoningEncryptedItem():
124
+ if not (
125
+ model_name is not None
126
+ and item.model == model_name
127
+ and item.encrypted_content
128
+ and (item.format or "").startswith("google")
129
+ and pending_thought_text
130
+ ):
131
+ continue
132
+ pending_thought_signature = item.encrypted_content
133
+ parts.append(
134
+ types.Part(
135
+ text=pending_thought_text,
136
+ thought=True,
137
+ thought_signature=pending_thought_signature,
138
+ )
139
+ )
140
+ pending_thought_text = None
141
+ pending_thought_signature = None
142
+
143
+ if pending_thought_text:
144
+ parts.append(
145
+ types.Part(
146
+ text=pending_thought_text,
147
+ thought=True,
148
+ thought_signature=pending_thought_signature,
149
+ )
150
+ )
151
+
152
+ if degraded_thinking_texts:
153
+ parts.insert(0, types.Part(text="<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"))
154
+
155
+ if group.text_content:
156
+ parts.append(types.Part(text=group.text_content))
157
+
158
+ for tc in group.tool_calls:
159
+ args: dict[str, Any]
160
+ if tc.arguments:
161
+ try:
162
+ args = json.loads(tc.arguments)
163
+ except json.JSONDecodeError:
164
+ args = {"_raw": tc.arguments}
165
+ else:
166
+ args = {}
167
+ parts.append(types.Part(function_call=types.FunctionCall(id=tc.call_id, name=tc.name, args=args)))
168
+
169
+ if not parts:
170
+ return None
171
+ return types.Content(role="model", parts=parts)
172
+
173
+
174
+ def convert_history_to_contents(
175
+ history: list[model.ConversationItem],
176
+ model_name: str | None,
177
+ ) -> list[types.Content]:
178
+ contents: list[types.Content] = []
179
+ pending_tool_groups: list[ToolGroup] = []
180
+
181
+ def flush_tool_groups() -> None:
182
+ nonlocal pending_tool_groups
183
+ if pending_tool_groups:
184
+ contents.extend(_tool_groups_to_content(pending_tool_groups, model_name=model_name))
185
+ pending_tool_groups = []
186
+
187
+ for group in parse_message_groups(history):
188
+ match group:
189
+ case UserGroup():
190
+ flush_tool_groups()
191
+ contents.append(_user_group_to_content(group))
192
+ case ToolGroup():
193
+ pending_tool_groups.append(group)
194
+ case AssistantGroup():
195
+ flush_tool_groups()
196
+ content = _assistant_group_to_content(group, model_name=model_name)
197
+ if content is not None:
198
+ contents.append(content)
199
+
200
+ flush_tool_groups()
201
+ return contents
202
+
203
+
204
+ def convert_tool_schema(tools: list[llm_param.ToolSchema] | None) -> list[types.Tool]:
205
+ if tools is None or len(tools) == 0:
206
+ return []
207
+ declarations = [
208
+ types.FunctionDeclaration(
209
+ name=tool.name,
210
+ description=tool.description,
211
+ parameters_json_schema=tool.parameters,
212
+ )
213
+ for tool in tools
214
+ ]
215
+ return [types.Tool(function_declarations=declarations)]
@@ -1,3 +1,4 @@
1
+ import importlib
1
2
  from collections.abc import Callable
2
3
  from typing import TYPE_CHECKING, TypeVar
3
4
 
@@ -21,15 +22,19 @@ def _load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
21
22
 
22
23
  # Import only the needed module to trigger @register decorator
23
24
  if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
24
- from . import anthropic as _
25
+ importlib.import_module("klaude_code.llm.anthropic")
26
+ elif protocol == llm_param.LLMClientProtocol.BEDROCK:
27
+ importlib.import_module("klaude_code.llm.bedrock")
25
28
  elif protocol == llm_param.LLMClientProtocol.CODEX:
26
- from . import codex as _
29
+ importlib.import_module("klaude_code.llm.codex")
27
30
  elif protocol == llm_param.LLMClientProtocol.OPENAI:
28
- from . import openai_compatible as _
31
+ importlib.import_module("klaude_code.llm.openai_compatible")
29
32
  elif protocol == llm_param.LLMClientProtocol.OPENROUTER:
30
- from . import openrouter as _
33
+ importlib.import_module("klaude_code.llm.openrouter")
31
34
  elif protocol == llm_param.LLMClientProtocol.RESPONSES:
32
- from . import responses as _ # noqa: F401
35
+ importlib.import_module("klaude_code.llm.responses")
36
+ elif protocol == llm_param.LLMClientProtocol.GOOGLE:
37
+ importlib.import_module("klaude_code.llm.google")
33
38
 
34
39
 
35
40
  def register(name: llm_param.LLMClientProtocol) -> Callable[[_T], _T]:
@@ -12,7 +12,9 @@ class LLMClientProtocol(Enum):
12
12
  RESPONSES = "responses"
13
13
  OPENROUTER = "openrouter"
14
14
  ANTHROPIC = "anthropic"
15
+ BEDROCK = "bedrock"
15
16
  CODEX = "codex"
17
+ GOOGLE = "google"
16
18
 
17
19
 
18
20
  class ToolSchema(BaseModel):
@@ -91,8 +93,15 @@ class LLMConfigProviderParameter(BaseModel):
91
93
  protocol: LLMClientProtocol
92
94
  base_url: str | None = None
93
95
  api_key: str | None = None
96
+ # Azure OpenAI
94
97
  is_azure: bool = False
95
98
  azure_api_version: str | None = None
99
+ # AWS Bedrock configuration
100
+ aws_access_key: str | None = None
101
+ aws_secret_key: str | None = None
102
+ aws_region: str | None = None
103
+ aws_session_token: str | None = None
104
+ aws_profile: str | None = None
96
105
 
97
106
 
98
107
  class LLMConfigModelParameter(BaseModel):
@@ -218,7 +218,9 @@ class Session(BaseModel):
218
218
  forked.file_tracker = {k: v.model_copy(deep=True) for k, v in self.file_tracker.items()}
219
219
  forked.todos = [todo.model_copy(deep=True) for todo in self.todos]
220
220
 
221
- history_to_copy = self.conversation_history[:until_index] if until_index is not None else self.conversation_history
221
+ history_to_copy = (
222
+ self.conversation_history[:until_index] if until_index is not None else self.conversation_history
223
+ )
222
224
  items = [it.model_copy(deep=True) for it in history_to_copy]
223
225
  if items:
224
226
  forked.append_history(items)
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
7
7
  Requires-Dist: ddgs>=9.9.3
8
8
  Requires-Dist: diff-match-patch>=20241021
9
+ Requires-Dist: google-genai>=1.56.0
9
10
  Requires-Dist: markdown-it-py>=4.0.0
10
11
  Requires-Dist: openai>=1.102.0
11
12
  Requires-Dist: pillow>=12.0.0
@@ -34,6 +35,10 @@ Minimal code agent CLI.
34
35
  - **Output truncation**: Large outputs saved to file system with snapshot links
35
36
  - **Skills**: Built-in + user + project Agent Skills (with implicit invocation by Skill tool or explicit invocation by typing `$`)
36
37
  - **Sessions**: Resumable with `--continue`
38
+ - **Cost tracking**: Automatic API cost calculation and display (USD/CNY)
39
+ - **Version update check**: Background PyPI version check with upgrade prompts
40
+ - **Terminal title**: Shows current directory and model name
41
+ - **Mermaid diagrams**: Interactive local HTML viewer with zoom, pan, and SVG export
37
42
  - **Extras**: Slash commands, sub-agents, image paste, terminal notifications, auto-theming
38
43
 
39
44
  ## Installation
@@ -77,6 +82,7 @@ klaude [--model <name>] [--select-model]
77
82
  - `--select-model`/`-s`: Open the interactive model selector at startup (shows all models unless `--model` is also provided).
78
83
  - `--continue`/`-c`: Resume the most recent session.
79
84
  - `--resume`/`-r`: Select a session to resume for this project.
85
+ - `--resume-by-id <id>`: Resume a session by its ID directly.
80
86
  - `--vanilla`: Minimal mode with only basic tools (Bash, Read, Edit) and no system prompts.
81
87
 
82
88
  **Model selection behavior:**
@@ -251,12 +257,18 @@ klaude session clean-all
251
257
 
252
258
  Inside the interactive session (`klaude`), use these commands to streamline your workflow:
253
259
 
254
- - `/dev-doc [feature]` - Generate a comprehensive execution plan for a feature.
255
- - `/export` - Export last assistant message to a temp Markdown file.
256
- - `/init` - Bootstrap a new project structure or module.
257
260
  - `/model` - Switch the active LLM during the session.
261
+ - `/thinking` - Configure model thinking/reasoning level.
258
262
  - `/clear` - Clear the current conversation context.
259
- - `/diff` - Show local git diff changes.
263
+ - `/status` - Show session usage statistics (cost, tokens, model breakdown).
264
+ - `/resume` - Select and resume a previous session.
265
+ - `/fork-session` - Fork current session to a new session ID (supports interactive fork point selection).
266
+ - `/export` - Export last assistant message to a temp Markdown file.
267
+ - `/export-online` - Export and deploy session to surge.sh as a static webpage.
268
+ - `/debug [filters]` - Toggle debug mode and configure debug filters.
269
+ - `/init` - Bootstrap a new project structure or module.
270
+ - `/dev-doc [feature]` - Generate a comprehensive execution plan for a feature.
271
+ - `/terminal-setup` - Configure terminal for Shift+Enter support.
260
272
  - `/help` - List all available commands.
261
273
 
262
274
 
@@ -267,6 +279,8 @@ Inside the interactive session (`klaude`), use these commands to streamline your
267
279
  | `Enter` | Submit input |
268
280
  | `Shift+Enter` | Insert newline (requires `/terminal-setup`) |
269
281
  | `Ctrl+J` | Insert newline |
282
+ | `Ctrl+L` | Open model picker overlay |
283
+ | `Ctrl+T` | Open thinking level picker overlay |
270
284
  | `Ctrl+V` | Paste image from clipboard |
271
285
  | `Left/Right` | Move cursor (wraps across lines) |
272
286
  | `Backspace` | Delete character or selected text |
@@ -290,4 +304,18 @@ echo "generate quicksort in python" | klaude exec --model gpt-5.1
290
304
 
291
305
  # Partial/ambiguous name opens the interactive selector (filtered)
292
306
  echo "generate quicksort in python" | klaude exec --model gpt
307
+
308
+ # Stream all events as JSON lines (for programmatic processing)
309
+ klaude exec "what is 2+2?" --stream-json
293
310
  ```
311
+
312
+ ### Sub-Agents
313
+
314
+ The main agent can spawn specialized sub-agents for specific tasks:
315
+
316
+ | Sub-Agent | Purpose |
317
+ |-----------|---------|
318
+ | **Explore** | Fast codebase exploration - find files, search code, answer questions about the codebase |
319
+ | **Task** | Handle complex multi-step tasks autonomously |
320
+ | **WebAgent** | Search the web, fetch pages, and analyze content |
321
+ | **Oracle** | Advanced reasoning advisor for code reviews, architecture planning, and bug analysis |
@@ -9,11 +9,11 @@ klaude_code/cli/__init__.py,sha256=YzlAoWAr5rx5oe6B_4zPxRFS4QaZauuy1AFwampP5fg,4
9
9
  klaude_code/cli/auth_cmd.py,sha256=UWMHjn9xZp2o8OZc-x8y9MnkZgRWOkFXk05iKJYcySE,2561
10
10
  klaude_code/cli/config_cmd.py,sha256=hlvslLNgdRHkokq1Pnam0XOdR3jqO3K0vNLqtWnPa6Q,3261
11
11
  klaude_code/cli/debug.py,sha256=cPQ7cgATcJTyBIboleW_Q4Pa_t-tGG6x-Hj3woeeuHE,2669
12
- klaude_code/cli/list_model.py,sha256=uA0PNR1RjUK7BCKu2Q0Sh2xB9j9Gpwp_bsWhroTW6JY,9260
12
+ klaude_code/cli/list_model.py,sha256=3SLURZXH_WgX-vGWIt52NuRm2D14-jcONtiS5GDM2xA,11248
13
13
  klaude_code/cli/main.py,sha256=uNZl0RjeLRITbfHerma4_kq2f0hF166dFZqAHLBu580,13236
14
14
  klaude_code/cli/runtime.py,sha256=6CtsQa8UcC9ppnNm2AvsF3yxgncyEYwpIIX0bb-3NN0,19826
15
15
  klaude_code/cli/self_update.py,sha256=iGuj0i869Zi0M70W52-VVLxZp90ISr30fQpZkHGMK2o,8059
16
- klaude_code/cli/session_cmd.py,sha256=q2YarlV6KARkFnbm_36ZUvBh8Noj8B7TlMg1RIlt1GE,3154
16
+ klaude_code/cli/session_cmd.py,sha256=9C30dzXCbPobminqenCjYvEuzZBS5zWXg3JpuhxT_OQ,3199
17
17
  klaude_code/command/__init__.py,sha256=IK2jz2SFMLVIcVzD5evKk3zWv6u1CjgCgfJXzWdvDlk,3470
18
18
  klaude_code/command/clear_cmd.py,sha256=3Ru6pFmOwru06XTLTuEGNUhKgy3COOaNe22Dk0TpGrQ,719
19
19
  klaude_code/command/command_abc.py,sha256=wZl_azY6Dpd4OvjtkSEPI3ilXaygLIVkO7NCgNlrofQ,2536
@@ -36,11 +36,11 @@ klaude_code/command/terminal_setup_cmd.py,sha256=SivM1gX_anGY_8DCQNFZ5VblFqt4sVg
36
36
  klaude_code/command/thinking_cmd.py,sha256=NPejWmx6HDxoWzAJVLEENCr3Wi6sQSbT8A8LRh1-2Nk,3059
37
37
  klaude_code/config/__init__.py,sha256=Qe1BeMekBfO2-Zd30x33lB70hdM1QQZGrp4DbWSQ-II,353
38
38
  klaude_code/config/assets/__init__.py,sha256=uMUfmXT3I-gYiI-HVr1DrE60mx5cY1o8V7SYuGqOmvY,32
39
- klaude_code/config/assets/builtin_config.yaml,sha256=9sfpcVqY3uWVGSdyteH3P_B8ZDwPhfJAoT2Q5o7I1bk,5605
40
- klaude_code/config/builtin_config.py,sha256=RgbawLV4yKk1IhJsQn04RkitDyLLhCwTqQ3nJkdvHGI,1113
41
- klaude_code/config/config.py,sha256=rTMU-7IYl_fmthB4LsrCaSgUVttNSw7Agif8XRCmU1g,16331
39
+ klaude_code/config/assets/builtin_config.yaml,sha256=9kQZOEd5PmNZhhQWUnrCENTfHNTBhRIJUfZ444X3rmY,6491
40
+ klaude_code/config/builtin_config.py,sha256=LkHr7Ml-6ir6rObn9hUj5-wa-fgfJsc4T2_NdRa1ax0,1135
41
+ klaude_code/config/config.py,sha256=Nxnwcu8SvOweX6YC6ueVgEEdClLAij1G1v6yyFeiXEc,17114
42
42
  klaude_code/config/select_model.py,sha256=PPbQ-BAJkwXPINBcCSPAlZjiXm4rEtg2y0hPnZE8Bnc,5183
43
- klaude_code/config/thinking.py,sha256=lYkwa4OvJuFk8viY0pgb6I4wCMhn2duVwC3ADf9ZFF0,8712
43
+ klaude_code/config/thinking.py,sha256=X-vywa36ggO_2z4iVhss1mAVEPAwAbcj1s68F0-B0G4,9223
44
44
  klaude_code/const.py,sha256=Xc6UKku2sGQE05mvPNCpBbKK205vJrS9CaNAeKvu1AA,4612
45
45
  klaude_code/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  klaude_code/core/agent.py,sha256=bWm-UFX_0-KAy5j_YHH8X8o3MJT4-40Ni2EaDP2SL5k,5819
@@ -107,11 +107,16 @@ klaude_code/core/tool/web/web_search_tool.py,sha256=9-dzzMXOdTA_QsdnwHw251R0VelO
107
107
  klaude_code/core/turn.py,sha256=PvtVV5GLAvYYAsl3RJNDnIvX1Yp4Va8whr0TR8x-9PI,12706
108
108
  klaude_code/llm/__init__.py,sha256=b4AsqnrMIs0a5qR_ti6rZcHwFzAReTwOW96EqozEoSo,287
109
109
  klaude_code/llm/anthropic/__init__.py,sha256=PWETvaeNAAX3ue0ww1uRUIxTJG0RpWiutkn7MlwKxBs,67
110
- klaude_code/llm/anthropic/client.py,sha256=EYBi2Ak1_DCJmZ-HG8uCz1W-6YRg9fLsCc3fjDIaUJM,10656
110
+ klaude_code/llm/anthropic/client.py,sha256=32MdRXm605OUXWrOy3MHAaFrUBih0gchETwNbjuoLmg,10248
111
111
  klaude_code/llm/anthropic/input.py,sha256=nyDX3uFK0GVduSiLlBEgBjAl70e0pgIZSF3PbbbuW78,8585
112
+ klaude_code/llm/bedrock/__init__.py,sha256=UmXPBXMmigAJ7euIh59iivSeUdrYJwum0RYU7okkkPM,86
113
+ klaude_code/llm/bedrock/client.py,sha256=U-vWTEwrwpTKMGNPEINEOHtkCwpLv6n3fBDSF4By0mU,2135
112
114
  klaude_code/llm/client.py,sha256=FbFnzLUAKM61goiYNdKi8-D4rBfu_ksaxjJtmJn0w_4,960
113
115
  klaude_code/llm/codex/__init__.py,sha256=8vN2j2ezWB_UVpfqQ8ooStsBeLL5SY4SUMXOXdWiMaI,132
114
116
  klaude_code/llm/codex/client.py,sha256=0BAOiLAdk2PxBEYuC_TGOs4_h6yfNZr1YWuf1lzkBxM,5329
117
+ klaude_code/llm/google/__init__.py,sha256=tQtf_mh_mC3E4S9XAsnhS2JZXGRnYUsBKF0jpXZTvM0,61
118
+ klaude_code/llm/google/client.py,sha256=VqEkPqypzrTyBU_u3yfnMcBlfhZISQi8tejertqICKs,11383
119
+ klaude_code/llm/google/input.py,sha256=AJkrqtTyP80tE80HK5hMaPsHMJK2oF5F3h25cKeELeQ,7930
115
120
  klaude_code/llm/input_common.py,sha256=NxiYlhGRFntiLiKm5sKLCF0xGYW6DwcyvIhj6KAZoeU,8533
116
121
  klaude_code/llm/openai_compatible/__init__.py,sha256=ACGpnki7k53mMcCl591aw99pm9jZOZk0ghr7atOfNps,81
117
122
  klaude_code/llm/openai_compatible/client.py,sha256=sMzHxaDZ4CRDgwSr1pZPqpG6YbHJA-Zk0cFyC_r-ihA,4396
@@ -122,7 +127,7 @@ klaude_code/llm/openrouter/__init__.py,sha256=_As8lHjwj6vapQhLorZttTpukk5ZiCdhFd
122
127
  klaude_code/llm/openrouter/client.py,sha256=zUkH7wkiYUJMGS_8iaVwdhzUnry7WLw4Q1IDQtncmK0,4864
123
128
  klaude_code/llm/openrouter/input.py,sha256=aHVJCejkwzWaTM_EBbgmzKWyZfttAws2Y7QDW_NCnZk,5671
124
129
  klaude_code/llm/openrouter/reasoning.py,sha256=d6RU6twuGfdf0mXGQoxSNRzFaSa3GJFV8Eve5GzDXfU,4472
125
- klaude_code/llm/registry.py,sha256=grgHetTd-lSxTXiY689QW_Zd6voaid7qBqSnulpg_fE,1734
130
+ klaude_code/llm/registry.py,sha256=ezfUcPld-j_KbC-DBuFeJpqLYTOL54DvGlfJpAslEL8,2089
126
131
  klaude_code/llm/responses/__init__.py,sha256=WsiyvnNiIytaYcaAqNiB8GI-5zcpjjeODPbMlteeFjA,67
127
132
  klaude_code/llm/responses/client.py,sha256=XEsVehevQJ0WFbEVxIkI-su7VwIcaeq0P9eSrIRcGug,10184
128
133
  klaude_code/llm/responses/input.py,sha256=qr61LmQJdcb_f-ofrAz06WpK_k4PEcI36XsyuZAXbKk,6805
@@ -130,7 +135,7 @@ klaude_code/llm/usage.py,sha256=ohQ6EBsWXZj6B4aJ4lDPqfhXRyd0LUAM1nXEJ_elD7A,4207
130
135
  klaude_code/protocol/__init__.py,sha256=aGUgzhYqvhuT3Mk2vj7lrHGriH4h9TSbqV1RsRFAZjQ,194
131
136
  klaude_code/protocol/commands.py,sha256=4tFt98CD_KvS9C-XEaHLN-S-QFsbDxQb_kGKnPkQlrk,958
132
137
  klaude_code/protocol/events.py,sha256=KUMf1rLNdHQO9cZiQ9Pa1VsKkP1PTMbUkp18bu_jGy8,3935
133
- klaude_code/protocol/llm_param.py,sha256=cb4ubLq21PIsMOC8WJb0aid12z_sT1b7FsbNJMr-jLg,4255
138
+ klaude_code/protocol/llm_param.py,sha256=O5sn3-KJnhS_0FOxDDvAFJ2Sx7ZYdJ74ugnwu-gHZG8,4538
134
139
  klaude_code/protocol/model.py,sha256=zz1DeSkpUWDT-OZBlypaGWA4z78TSeefA-Tj8mJMHp4,14257
135
140
  klaude_code/protocol/op.py,sha256=--RllgP6Upacb6cqHd26RSwrvqZg4w6GhcebmV8gKJo,5763
136
141
  klaude_code/protocol/op_handler.py,sha256=hSxEVPEkk0vRPRsOyJdEn3sa87c_m17wzFGKjaMqa4o,1929
@@ -144,7 +149,7 @@ klaude_code/session/__init__.py,sha256=4sw81uQvEd3YUOOjamKk1KqGmxeb4Ic9T1Tee5zzt
144
149
  klaude_code/session/codec.py,sha256=ummbqT7t6uHHXtaS9lOkyhi1h0YpMk7SNSms8DyGAHU,2015
145
150
  klaude_code/session/export.py,sha256=dj-IRUNtXL8uONDj9bsEXcEHKyeHY7lIcXv80yP88h4,31022
146
151
  klaude_code/session/selector.py,sha256=FpKpGs06fM-LdV-yVUqEY-FJsFn2OtGK-0paXjsZVTg,2770
147
- klaude_code/session/session.py,sha256=CUbIBpPgl_m2lr3h5jZNktJG0S5eYGZkL37seWdZ7uw,17318
152
+ klaude_code/session/session.py,sha256=VvGMxu5gwFasLlaT9h5x1gBFpuIfXDZJKC1qNwKU8tQ,17342
148
153
  klaude_code/session/store.py,sha256=-e-lInCB3N1nFLlet7bipkmPk1PXmGthuMxv5z3hg5o,6953
149
154
  klaude_code/session/templates/export_session.html,sha256=bA27AkcC7DQRoWmcMBeaR8WOx1z76hezEDf0aYH-0HQ,119780
150
155
  klaude_code/session/templates/mermaid_viewer.html,sha256=lOkETxlctX1C1WJtS1wFw6KhNQmemxwJZFpXDSjlMOk,27842
@@ -207,7 +212,7 @@ klaude_code/ui/terminal/progress_bar.py,sha256=MDnhPbqCnN4GDgLOlxxOEVZPDwVC_XL2N
207
212
  klaude_code/ui/terminal/selector.py,sha256=NblhWxUp0AW2OyepG4DHNy4yKE947Oi0OiqlafvBCEE,22144
208
213
  klaude_code/ui/utils/__init__.py,sha256=YEsCLjbCPaPza-UXTPUMTJTrc9BmNBUP5CbFWlshyOQ,15
209
214
  klaude_code/ui/utils/common.py,sha256=tqHqwgLtAyP805kwRFyoAL4EgMutcNb3Y-GAXJ4IeuM,2263
210
- klaude_code-1.6.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
211
- klaude_code-1.6.0.dist-info/entry_points.txt,sha256=kkXIXedaTOtjXPr2rVjRVVXZYlFUcBHELaqmyVlWUFA,92
212
- klaude_code-1.6.0.dist-info/METADATA,sha256=h5IGANYxmMyc3H-4lRuaTKpumksBz0NEnJBAwbSZMuU,9091
213
- klaude_code-1.6.0.dist-info/RECORD,,
215
+ klaude_code-1.7.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
216
+ klaude_code-1.7.0.dist-info/entry_points.txt,sha256=kkXIXedaTOtjXPr2rVjRVVXZYlFUcBHELaqmyVlWUFA,92
217
+ klaude_code-1.7.0.dist-info/METADATA,sha256=AscMVASGUlpBW2pN_633mhuS6Tz9ExAdGZpC97m_6WM,10675
218
+ klaude_code-1.7.0.dist-info/RECORD,,