klaude-code 1.5.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.
Files changed (33) hide show
  1. klaude_code/cli/list_model.py +55 -4
  2. klaude_code/cli/main.py +3 -56
  3. klaude_code/cli/session_cmd.py +3 -2
  4. klaude_code/command/fork_session_cmd.py +220 -2
  5. klaude_code/command/refresh_cmd.py +4 -4
  6. klaude_code/command/resume_cmd.py +21 -11
  7. klaude_code/config/assets/builtin_config.yaml +37 -2
  8. klaude_code/config/builtin_config.py +1 -0
  9. klaude_code/config/config.py +14 -0
  10. klaude_code/config/thinking.py +14 -0
  11. klaude_code/llm/anthropic/client.py +127 -114
  12. klaude_code/llm/bedrock/__init__.py +3 -0
  13. klaude_code/llm/bedrock/client.py +60 -0
  14. klaude_code/llm/google/__init__.py +3 -0
  15. klaude_code/llm/google/client.py +309 -0
  16. klaude_code/llm/google/input.py +215 -0
  17. klaude_code/llm/registry.py +10 -5
  18. klaude_code/llm/usage.py +1 -1
  19. klaude_code/protocol/llm_param.py +9 -0
  20. klaude_code/session/__init__.py +2 -2
  21. klaude_code/session/selector.py +32 -4
  22. klaude_code/session/session.py +20 -12
  23. klaude_code/ui/modes/repl/event_handler.py +22 -32
  24. klaude_code/ui/modes/repl/renderer.py +1 -1
  25. klaude_code/ui/renderers/developer.py +2 -2
  26. klaude_code/ui/renderers/metadata.py +8 -0
  27. klaude_code/ui/rich/markdown.py +41 -9
  28. klaude_code/ui/rich/status.py +83 -22
  29. klaude_code/ui/terminal/selector.py +72 -3
  30. {klaude_code-1.5.0.dist-info → klaude_code-1.7.0.dist-info}/METADATA +33 -5
  31. {klaude_code-1.5.0.dist-info → klaude_code-1.7.0.dist-info}/RECORD +33 -28
  32. {klaude_code-1.5.0.dist-info → klaude_code-1.7.0.dist-info}/WHEEL +0 -0
  33. {klaude_code-1.5.0.dist-info → klaude_code-1.7.0.dist-info}/entry_points.txt +0 -0
@@ -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(
klaude_code/cli/main.py CHANGED
@@ -11,64 +11,11 @@ from klaude_code.cli.config_cmd import register_config_commands
11
11
  from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, resolve_debug_settings
12
12
  from klaude_code.cli.self_update import register_self_update_commands, version_option_callback
13
13
  from klaude_code.cli.session_cmd import register_session_commands
14
- from klaude_code.session import Session, build_session_select_options
14
+ from klaude_code.command.resume_cmd import select_session_sync
15
+ from klaude_code.session import Session
15
16
  from klaude_code.trace import DebugType, prepare_debug_log_file
16
17
 
17
18
 
18
- def select_session_interactive() -> str | None:
19
- """Interactive session selection for CLI.
20
-
21
- Returns:
22
- Selected session ID, or None if no session selected or no sessions exist.
23
- """
24
- from klaude_code.trace import log
25
-
26
- options = build_session_select_options()
27
- if not options:
28
- log("No sessions found for this project.")
29
- return None
30
-
31
- from prompt_toolkit.styles import Style
32
-
33
- from klaude_code.ui.terminal.selector import SelectItem, select_one
34
-
35
- items: list[SelectItem[str]] = []
36
- for opt in options:
37
- title = [
38
- ("class:msg", f"{opt.first_user_message}\n"),
39
- ("class:meta", f" {opt.messages_count} · {opt.relative_time} · {opt.model_name} · {opt.session_id}\n\n"),
40
- ]
41
- items.append(
42
- SelectItem(
43
- title=title,
44
- value=opt.session_id,
45
- search_text=f"{opt.first_user_message} {opt.model_name} {opt.session_id}",
46
- )
47
- )
48
-
49
- try:
50
- return select_one(
51
- message="Select a session to resume:",
52
- items=items,
53
- pointer="→",
54
- style=Style(
55
- [
56
- ("msg", ""),
57
- ("meta", "fg:ansibrightblack"),
58
- ("pointer", "bold fg:ansigreen"),
59
- ("highlighted", "fg:ansigreen"),
60
- ("search_prefix", "fg:ansibrightblack"),
61
- ("search_success", "noinherit fg:ansigreen"),
62
- ("search_none", "noinherit fg:ansired"),
63
- ("question", "bold"),
64
- ("text", ""),
65
- ]
66
- ),
67
- )
68
- except KeyboardInterrupt:
69
- return None
70
-
71
-
72
19
  def set_terminal_title(title: str) -> None:
73
20
  """Set terminal window title using ANSI escape sequence."""
74
21
  # Never write terminal control sequences when stdout is not a TTY (pipes/CI/redirects).
@@ -361,7 +308,7 @@ def main_callback(
361
308
  session_id: str | None = None
362
309
 
363
310
  if resume:
364
- session_id = select_session_interactive()
311
+ session_id = select_session_sync()
365
312
  if session_id is None:
366
313
  return
367
314
  # If user didn't pick, allow fallback to --continue
@@ -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
 
@@ -1,5 +1,182 @@
1
+ import asyncio
2
+ import sys
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ from prompt_toolkit.styles import Style
7
+
1
8
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
2
9
  from klaude_code.protocol import commands, events, model
10
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
11
+
12
+ FORK_SELECT_STYLE = Style(
13
+ [
14
+ ("msg", ""),
15
+ ("meta", "fg:ansibrightblack"),
16
+ ("separator", "fg:ansibrightblack"),
17
+ ("assistant", "fg:ansiblue"),
18
+ ("pointer", "bold fg:ansigreen"),
19
+ ("search_prefix", "fg:ansibrightblack"),
20
+ ("search_success", "noinherit fg:ansigreen"),
21
+ ("search_none", "noinherit fg:ansired"),
22
+ ("question", "bold"),
23
+ ("text", ""),
24
+ ]
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class ForkPoint:
30
+ """A fork point in conversation history."""
31
+
32
+ history_index: int | None # None means fork entire conversation
33
+ user_message: str
34
+ tool_call_stats: dict[str, int] # tool_name -> count
35
+ last_assistant_summary: str
36
+
37
+
38
+ def _truncate(text: str, max_len: int = 60) -> str:
39
+ """Truncate text to max_len, adding ellipsis if needed."""
40
+ text = text.replace("\n", " ").strip()
41
+ if len(text) <= max_len:
42
+ return text
43
+ return text[: max_len - 3] + "..."
44
+
45
+
46
+ def _build_fork_points(conversation_history: list[model.ConversationItem]) -> list[ForkPoint]:
47
+ """Build list of fork points from conversation history.
48
+
49
+ Fork points are:
50
+ - Each UserMessageItem position (for UI display, including first which would be empty session)
51
+ - The end of the conversation (fork entire conversation)
52
+ """
53
+ fork_points: list[ForkPoint] = []
54
+ user_indices: list[int] = []
55
+
56
+ for i, item in enumerate(conversation_history):
57
+ if isinstance(item, model.UserMessageItem):
58
+ user_indices.append(i)
59
+
60
+ # For each UserMessageItem, create a fork point at that position
61
+ for i, user_idx in enumerate(user_indices):
62
+ user_item = conversation_history[user_idx]
63
+ assert isinstance(user_item, model.UserMessageItem)
64
+
65
+ # Find the end of this "task" (next UserMessageItem or end of history)
66
+ next_user_idx = user_indices[i + 1] if i + 1 < len(user_indices) else len(conversation_history)
67
+
68
+ # Count tool calls by name and find last assistant message in this segment
69
+ tool_stats: dict[str, int] = {}
70
+ last_assistant_content = ""
71
+ for j in range(user_idx, next_user_idx):
72
+ item = conversation_history[j]
73
+ if isinstance(item, model.ToolCallItem):
74
+ tool_stats[item.name] = tool_stats.get(item.name, 0) + 1
75
+ elif isinstance(item, model.AssistantMessageItem) and item.content:
76
+ last_assistant_content = item.content
77
+
78
+ fork_points.append(
79
+ ForkPoint(
80
+ history_index=user_idx,
81
+ user_message=user_item.content or "(empty)",
82
+ tool_call_stats=tool_stats,
83
+ last_assistant_summary=_truncate(last_assistant_content) if last_assistant_content else "",
84
+ )
85
+ )
86
+
87
+ # Add the "fork entire conversation" option at the end
88
+ if user_indices:
89
+ fork_points.append(
90
+ ForkPoint(
91
+ history_index=None, # None means fork entire conversation
92
+ user_message="", # No specific message, this represents the end
93
+ tool_call_stats={},
94
+ last_assistant_summary="",
95
+ )
96
+ )
97
+
98
+ return fork_points
99
+
100
+
101
+ def _build_select_items(fork_points: list[ForkPoint]) -> list[SelectItem[int | None]]:
102
+ """Build SelectItem list from fork points."""
103
+ items: list[SelectItem[int | None]] = []
104
+
105
+ for i, fp in enumerate(fork_points):
106
+ is_first = i == 0
107
+ is_last = i == len(fork_points) - 1
108
+
109
+ # Build the title
110
+ title_parts: list[tuple[str, str]] = []
111
+
112
+ # First line: separator (with special markers for first/last fork points)
113
+ if is_first and not is_last:
114
+ title_parts.append(("class:separator", "----- fork from here (empty session) -----\n\n"))
115
+ elif is_last:
116
+ title_parts.append(("class:separator", "----- fork from here (entire session) -----\n\n"))
117
+ else:
118
+ title_parts.append(("class:separator", "----- fork from here -----\n\n"))
119
+
120
+ if not is_last:
121
+ # Second line: user message
122
+ title_parts.append(("class:msg", f"user: {_truncate(fp.user_message, 70)}\n"))
123
+
124
+ # Third line: tool call stats (if any)
125
+ if fp.tool_call_stats:
126
+ tool_parts = [f"{name} × {count}" for name, count in fp.tool_call_stats.items()]
127
+ title_parts.append(("class:meta", f"tools: {', '.join(tool_parts)}\n"))
128
+
129
+ # Fourth line: last assistant message summary (if any)
130
+ if fp.last_assistant_summary:
131
+ title_parts.append(("class:assistant", f"ai: {fp.last_assistant_summary}\n"))
132
+
133
+ # Empty line at the end
134
+ title_parts.append(("class:text", "\n"))
135
+
136
+ items.append(
137
+ SelectItem(
138
+ title=title_parts,
139
+ value=fp.history_index,
140
+ search_text=fp.user_message if not is_last else "fork entire conversation",
141
+ )
142
+ )
143
+
144
+ return items
145
+
146
+
147
+ def _select_fork_point_sync(fork_points: list[ForkPoint]) -> int | None | Literal["cancelled"]:
148
+ """Interactive fork point selection (sync version for asyncio.to_thread).
149
+
150
+ Returns:
151
+ - int: history index to fork at (exclusive)
152
+ - None: fork entire conversation
153
+ - "cancelled": user cancelled selection
154
+ """
155
+ items = _build_select_items(fork_points)
156
+ if not items:
157
+ return None
158
+
159
+ # Default to the last option (fork entire conversation)
160
+ last_value = items[-1].value
161
+
162
+ # Non-interactive environments default to forking entire conversation
163
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
164
+ return last_value
165
+
166
+ try:
167
+ result = select_one(
168
+ message="Select fork point (messages before this point will be included):",
169
+ items=items,
170
+ pointer="→",
171
+ style=FORK_SELECT_STYLE,
172
+ initial_value=last_value,
173
+ highlight_pointed_item=False,
174
+ )
175
+ if result is None:
176
+ return "cancelled"
177
+ return result
178
+ except KeyboardInterrupt:
179
+ return "cancelled"
3
180
 
4
181
 
5
182
  class ForkSessionCommand(CommandABC):
@@ -13,6 +190,10 @@ class ForkSessionCommand(CommandABC):
13
190
  def summary(self) -> str:
14
191
  return "Fork the current session and show a resume-by-id command"
15
192
 
193
+ @property
194
+ def is_interactive(self) -> bool:
195
+ return True
196
+
16
197
  async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
17
198
  del user_input # unused
18
199
 
@@ -26,13 +207,50 @@ class ForkSessionCommand(CommandABC):
26
207
  )
27
208
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
28
209
 
29
- new_session = agent.session.fork()
210
+ # Build fork points from conversation history
211
+ fork_points = _build_fork_points(agent.session.conversation_history)
212
+
213
+ if not fork_points:
214
+ # Only one user message, just fork entirely
215
+ new_session = agent.session.fork()
216
+ await new_session.wait_for_flush()
217
+
218
+ event = events.DeveloperMessageEvent(
219
+ session_id=agent.session.id,
220
+ item=model.DeveloperMessageItem(
221
+ content=f"Session forked successfully. New session id: {new_session.id}",
222
+ command_output=model.CommandOutput(
223
+ command_name=self.name,
224
+ ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
225
+ ),
226
+ ),
227
+ )
228
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
229
+
230
+ # Interactive selection
231
+ selected = await asyncio.to_thread(_select_fork_point_sync, fork_points)
232
+
233
+ if selected == "cancelled":
234
+ event = events.DeveloperMessageEvent(
235
+ session_id=agent.session.id,
236
+ item=model.DeveloperMessageItem(
237
+ content="(fork cancelled)",
238
+ command_output=model.CommandOutput(command_name=self.name),
239
+ ),
240
+ )
241
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
242
+
243
+ # Perform the fork
244
+ new_session = agent.session.fork(until_index=selected)
30
245
  await new_session.wait_for_flush()
31
246
 
247
+ # Build result message
248
+ fork_description = "entire conversation" if selected is None else f"up to message index {selected}"
249
+
32
250
  event = events.DeveloperMessageEvent(
33
251
  session_id=agent.session.id,
34
252
  item=model.DeveloperMessageItem(
35
- content=f"Session forked successfully. New session id: {new_session.id}",
253
+ content=f"Session forked ({fork_description}). New session id: {new_session.id}",
36
254
  command_output=model.CommandOutput(
37
255
  command_name=self.name,
38
256
  ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
@@ -23,7 +23,7 @@ class RefreshTerminalCommand(CommandABC):
23
23
 
24
24
  os.system("cls" if os.name == "nt" else "clear")
25
25
 
26
- result = CommandResult(
26
+ return CommandResult(
27
27
  events=[
28
28
  events.WelcomeEvent(
29
29
  work_dir=str(agent.session.work_dir),
@@ -35,7 +35,7 @@ class RefreshTerminalCommand(CommandABC):
35
35
  updated_at=agent.session.updated_at,
36
36
  is_load=False,
37
37
  ),
38
- ]
38
+ ],
39
+ persist_user_input=False,
40
+ persist_events=False,
39
41
  )
40
-
41
- return result
@@ -4,14 +4,14 @@ from prompt_toolkit.styles import Style
4
4
 
5
5
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
6
  from klaude_code.protocol import commands, events, model, op
7
- from klaude_code.session.selector import build_session_select_options
7
+ from klaude_code.session.selector import build_session_select_options, format_user_messages_display
8
8
  from klaude_code.trace import log
9
9
  from klaude_code.ui.terminal.selector import SelectItem, select_one
10
10
 
11
11
  SESSION_SELECT_STYLE = Style(
12
12
  [
13
- ("msg", ""),
14
- ("meta", "fg:ansibrightblack"),
13
+ ("msg", "fg:ansibrightblack"),
14
+ ("meta", ""),
15
15
  ("pointer", "bold fg:ansigreen"),
16
16
  ("highlighted", "fg:ansigreen"),
17
17
  ("search_prefix", "fg:ansibrightblack"),
@@ -23,7 +23,7 @@ SESSION_SELECT_STYLE = Style(
23
23
  )
24
24
 
25
25
 
26
- def _select_session_sync() -> str | None:
26
+ def select_session_sync() -> str | None:
27
27
  """Interactive session selection (sync version for asyncio.to_thread)."""
28
28
  options = build_session_select_options()
29
29
  if not options:
@@ -31,16 +31,26 @@ def _select_session_sync() -> str | None:
31
31
  return None
32
32
 
33
33
  items: list[SelectItem[str]] = []
34
- for opt in options:
35
- title = [
36
- ("class:msg", f"{opt.first_user_message}\n"),
37
- ("class:meta", f" {opt.messages_count} · {opt.relative_time} · {opt.model_name} · {opt.session_id}\n\n"),
38
- ]
34
+ for idx, opt in enumerate(options, 1):
35
+ display_msgs = format_user_messages_display(opt.user_messages)
36
+ title: list[tuple[str, str]] = []
37
+ title.append(("fg:ansibrightblack", f"{idx:2}. "))
38
+ title.append(
39
+ ("class:meta", f"{opt.relative_time} · {opt.messages_count} · {opt.model_name} · {opt.session_id}\n")
40
+ )
41
+ for msg in display_msgs:
42
+ if msg == "⋮":
43
+ title.append(("class:msg", f" {msg}\n"))
44
+ else:
45
+ title.append(("class:msg", f" > {msg}\n"))
46
+ title.append(("", "\n"))
47
+
48
+ search_text = " ".join(opt.user_messages) + f" {opt.model_name} {opt.session_id}"
39
49
  items.append(
40
50
  SelectItem(
41
51
  title=title,
42
52
  value=opt.session_id,
43
- search_text=f"{opt.first_user_message} {opt.model_name} {opt.session_id}",
53
+ search_text=search_text,
44
54
  )
45
55
  )
46
56
 
@@ -83,7 +93,7 @@ class ResumeCommand(CommandABC):
83
93
  )
84
94
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
85
95
 
86
- selected_session_id = await asyncio.to_thread(_select_session_sync)
96
+ selected_session_id = await asyncio.to_thread(select_session_sync)
87
97
  if selected_session_id is None:
88
98
  event = events.DeveloperMessageEvent(
89
99
  session_id=agent.session.id,
@@ -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