klaude-code 1.2.26__py3-none-any.whl → 1.2.27__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 (36) hide show
  1. klaude_code/cli/config_cmd.py +1 -5
  2. klaude_code/cli/list_model.py +170 -129
  3. klaude_code/cli/main.py +37 -5
  4. klaude_code/cli/runtime.py +4 -6
  5. klaude_code/cli/self_update.py +2 -1
  6. klaude_code/cli/session_cmd.py +1 -1
  7. klaude_code/config/__init__.py +3 -1
  8. klaude_code/config/assets/__init__.py +1 -0
  9. klaude_code/config/assets/builtin_config.yaml +233 -0
  10. klaude_code/config/builtin_config.py +37 -0
  11. klaude_code/config/config.py +332 -112
  12. klaude_code/config/select_model.py +45 -8
  13. klaude_code/core/executor.py +4 -2
  14. klaude_code/core/manager/llm_clients_builder.py +4 -1
  15. klaude_code/core/tool/file/edit_tool.py +4 -4
  16. klaude_code/core/tool/file/write_tool.py +4 -4
  17. klaude_code/core/tool/shell/bash_tool.py +2 -2
  18. klaude_code/llm/openai_compatible/stream.py +2 -1
  19. klaude_code/session/export.py +1 -1
  20. klaude_code/session/selector.py +2 -2
  21. klaude_code/session/session.py +4 -4
  22. klaude_code/ui/modes/repl/completers.py +4 -4
  23. klaude_code/ui/modes/repl/event_handler.py +1 -1
  24. klaude_code/ui/modes/repl/input_prompt_toolkit.py +4 -4
  25. klaude_code/ui/modes/repl/key_bindings.py +4 -4
  26. klaude_code/ui/renderers/diffs.py +1 -1
  27. klaude_code/ui/renderers/metadata.py +2 -2
  28. klaude_code/ui/renderers/tools.py +1 -1
  29. klaude_code/ui/rich/markdown.py +1 -1
  30. klaude_code/ui/rich/theme.py +1 -1
  31. klaude_code/ui/terminal/color.py +1 -1
  32. klaude_code/ui/terminal/control.py +4 -4
  33. {klaude_code-1.2.26.dist-info → klaude_code-1.2.27.dist-info}/METADATA +121 -127
  34. {klaude_code-1.2.26.dist-info → klaude_code-1.2.27.dist-info}/RECORD +36 -33
  35. {klaude_code-1.2.26.dist-info → klaude_code-1.2.27.dist-info}/WHEEL +0 -0
  36. {klaude_code-1.2.26.dist-info → klaude_code-1.2.27.dist-info}/entry_points.txt +0 -0
@@ -88,7 +88,7 @@ class EditTool(ToolABC):
88
88
  async def call(cls, arguments: str) -> model.ToolResultItem:
89
89
  try:
90
90
  args = EditTool.EditArguments.model_validate_json(arguments)
91
- except Exception as e: # pragma: no cover - defensive
91
+ except ValueError as e: # pragma: no cover - defensive
92
92
  return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
93
93
 
94
94
  file_path = os.path.abspath(args.file_path)
@@ -150,7 +150,7 @@ class EditTool(ToolABC):
150
150
  # Backward-compat: old sessions only stored mtime.
151
151
  try:
152
152
  current_mtime = Path(file_path).stat().st_mtime
153
- except Exception:
153
+ except OSError:
154
154
  current_mtime = tracked_status.mtime
155
155
  if current_mtime != tracked_status.mtime:
156
156
  return model.ToolResultItem(
@@ -188,7 +188,7 @@ class EditTool(ToolABC):
188
188
  # Write back
189
189
  try:
190
190
  await asyncio.to_thread(write_text, file_path, after)
191
- except Exception as e: # pragma: no cover
191
+ except (OSError, UnicodeError) as e: # pragma: no cover
192
192
  return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
193
193
 
194
194
  # Prepare UI extra: unified diff with 3 context lines
@@ -233,7 +233,7 @@ class EditTool(ToolABC):
233
233
  plus_range = plus.split(" ")[0]
234
234
  start = int(plus_range.split(",")[0]) if "," in plus_range else int(plus_range)
235
235
  after_line_no = start - 1
236
- except Exception:
236
+ except (ValueError, IndexError):
237
237
  after_line_no = 0
238
238
  continue
239
239
  if line.startswith(" ") or (line.startswith("+") and not line.startswith("+++ ")):
@@ -49,7 +49,7 @@ class WriteTool(ToolABC):
49
49
  async def call(cls, arguments: str) -> model.ToolResultItem:
50
50
  try:
51
51
  args = WriteArguments.model_validate_json(arguments)
52
- except Exception as e: # pragma: no cover - defensive
52
+ except ValueError as e: # pragma: no cover - defensive
53
53
  return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
54
54
 
55
55
  file_path = os.path.abspath(args.file_path)
@@ -79,7 +79,7 @@ class WriteTool(ToolABC):
79
79
  try:
80
80
  before = await asyncio.to_thread(read_text, file_path)
81
81
  before_read_ok = True
82
- except Exception:
82
+ except OSError:
83
83
  before = ""
84
84
  before_read_ok = False
85
85
 
@@ -98,7 +98,7 @@ class WriteTool(ToolABC):
98
98
  # Backward-compat: old sessions only stored mtime, or we couldn't hash.
99
99
  try:
100
100
  current_mtime = Path(file_path).stat().st_mtime
101
- except Exception:
101
+ except OSError:
102
102
  current_mtime = tracked_status.mtime
103
103
  if current_mtime != tracked_status.mtime:
104
104
  return model.ToolResultItem(
@@ -111,7 +111,7 @@ class WriteTool(ToolABC):
111
111
 
112
112
  try:
113
113
  await asyncio.to_thread(write_text, file_path, args.content)
114
- except Exception as e: # pragma: no cover
114
+ except (OSError, UnicodeError) as e: # pragma: no cover
115
115
  return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
116
116
 
117
117
  if file_tracker is not None:
@@ -274,7 +274,7 @@ class BashTool(ToolABC):
274
274
  proc.terminate()
275
275
  except ProcessLookupError:
276
276
  return
277
- except Exception:
277
+ except OSError:
278
278
  # Fall back to kill below.
279
279
  pass
280
280
 
@@ -356,7 +356,7 @@ class BashTool(ToolABC):
356
356
  except asyncio.CancelledError:
357
357
  # Propagate cooperative cancellation so outer layers can handle interrupts correctly.
358
358
  raise
359
- except Exception as e: # safeguard against unexpected failures
359
+ except OSError as e: # safeguard: catch remaining OS-level errors (permissions, resources, etc.)
360
360
  return model.ToolResultItem(
361
361
  status="error",
362
362
  output=f"Execution error: {e}",
@@ -20,6 +20,7 @@ from typing import Any, Literal, cast
20
20
  import httpx
21
21
  import openai
22
22
  import openai.types
23
+ import pydantic
23
24
  from openai import AsyncStream
24
25
  from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
25
26
 
@@ -204,7 +205,7 @@ async def parse_chat_completions_stream(
204
205
  try:
205
206
  usage = openai.types.CompletionUsage.model_validate(choice_usage)
206
207
  metadata_tracker.set_usage(convert_usage(usage, param.context_limit, param.max_tokens))
207
- except Exception:
208
+ except pydantic.ValidationError:
208
209
  pass
209
210
 
210
211
  delta = cast(Any, getattr(choice0, "delta", None))
@@ -702,7 +702,7 @@ def _render_sub_agent_session(
702
702
 
703
703
  try:
704
704
  sub_session = Session.load(session_id)
705
- except Exception:
705
+ except (OSError, json.JSONDecodeError, ValueError):
706
706
  return None
707
707
 
708
708
  sub_history = sub_session.conversation_history
@@ -23,7 +23,7 @@ def resume_select_session() -> str | None:
23
23
  def _fmt(ts: float) -> str:
24
24
  try:
25
25
  return time.strftime("%m-%d %H:%M:%S", time.localtime(ts))
26
- except Exception:
26
+ except (ValueError, OSError):
27
27
  return str(ts)
28
28
 
29
29
  try:
@@ -76,6 +76,6 @@ def resume_select_session() -> str | None:
76
76
  idx = int(raw)
77
77
  if 1 <= idx <= len(sessions):
78
78
  return str(sessions[idx - 1].id)
79
- except Exception:
79
+ except (ValueError, EOFError):
80
80
  return None
81
81
  return None
@@ -7,7 +7,7 @@ from collections.abc import Iterable, Sequence
7
7
  from pathlib import Path
8
8
  from typing import Any, cast
9
9
 
10
- from pydantic import BaseModel, Field, PrivateAttr
10
+ from pydantic import BaseModel, Field, PrivateAttr, ValidationError
11
11
 
12
12
  from klaude_code.protocol import events, llm_param, model, tools
13
13
  from klaude_code.session.store import JsonlSessionStore, ProjectPaths, build_meta_snapshot
@@ -124,7 +124,7 @@ class Session(BaseModel):
124
124
  if isinstance(k, str) and isinstance(v, dict):
125
125
  try:
126
126
  file_tracker[k] = model.FileStatus.model_validate(v)
127
- except Exception:
127
+ except ValidationError:
128
128
  continue
129
129
 
130
130
  todos_raw = raw.get("todos")
@@ -135,7 +135,7 @@ class Session(BaseModel):
135
135
  continue
136
136
  try:
137
137
  todos.append(model.TodoItem.model_validate(todo_raw))
138
- except Exception:
138
+ except ValidationError:
139
139
  continue
140
140
 
141
141
  created_at = float(raw.get("created_at", time.time()))
@@ -306,7 +306,7 @@ class Session(BaseModel):
306
306
  seen_sub_agent_sessions.add(session_id)
307
307
  try:
308
308
  sub_session = Session.load(session_id)
309
- except Exception:
309
+ except (OSError, json.JSONDecodeError, ValueError):
310
310
  return
311
311
  yield from sub_session.get_history_item()
312
312
 
@@ -204,7 +204,7 @@ class _SkillCompleter(Completer):
204
204
  from klaude_code.skill import get_available_skills
205
205
 
206
206
  return get_available_skills()
207
- except Exception:
207
+ except (ImportError, RuntimeError):
208
208
  return []
209
209
 
210
210
  def is_skill_context(self, document: Document) -> bool:
@@ -497,7 +497,7 @@ class _AtFilesCompleter(Completer):
497
497
  try:
498
498
  if (cwd / s).is_dir():
499
499
  uniq[idx] = f"{s}/"
500
- except Exception:
500
+ except OSError:
501
501
  continue
502
502
  return uniq
503
503
 
@@ -530,7 +530,7 @@ class _AtFilesCompleter(Completer):
530
530
  if tag != "search":
531
531
  return root, None
532
532
  return root, kw
533
- except Exception:
533
+ except ValueError:
534
534
  return None, None
535
535
 
536
536
  # ---- Utilities ----
@@ -680,7 +680,7 @@ class _AtFilesCompleter(Completer):
680
680
  if p.is_dir() and not rel.endswith("/"):
681
681
  rel += "/"
682
682
  items.append(rel)
683
- except Exception:
683
+ except OSError:
684
684
  return []
685
685
  return items[: min(self._max_results, 100)]
686
686
 
@@ -246,7 +246,7 @@ class SpinnerStatusState:
246
246
  """Get current spinner status as rich Text (without context)."""
247
247
  activity_text = self._activity.get_activity_text()
248
248
 
249
- base_status = self._todo_status or self._reasoning_status
249
+ base_status = self._reasoning_status or self._todo_status
250
250
 
251
251
  if base_status:
252
252
  result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD)
@@ -118,7 +118,7 @@ class PromptToolkitInput(InputProviderABC):
118
118
  try:
119
119
  status = self._status_provider()
120
120
  update_message = status.update_message
121
- except Exception:
121
+ except (AttributeError, RuntimeError):
122
122
  pass
123
123
 
124
124
  # If update available, show only the update message
@@ -127,7 +127,7 @@ class PromptToolkitInput(InputProviderABC):
127
127
  try:
128
128
  terminal_width = shutil.get_terminal_size().columns
129
129
  padding = " " * max(0, terminal_width - len(left_text))
130
- except Exception:
130
+ except (OSError, ValueError):
131
131
  padding = ""
132
132
  toolbar_text = left_text + padding
133
133
  return FormattedText([("#ansiyellow", toolbar_text)])
@@ -151,7 +151,7 @@ class PromptToolkitInput(InputProviderABC):
151
151
  # Add context if available
152
152
  if status.context_usage_percent is not None:
153
153
  right_parts.append(f"context {status.context_usage_percent:.1f}%")
154
- except Exception:
154
+ except (AttributeError, RuntimeError):
155
155
  pass
156
156
 
157
157
  # Build left and right text with borders
@@ -163,7 +163,7 @@ class PromptToolkitInput(InputProviderABC):
163
163
  terminal_width = shutil.get_terminal_size().columns
164
164
  used_width = len(left_text) + len(right_text)
165
165
  padding = " " * max(0, terminal_width - used_width)
166
- except Exception:
166
+ except (OSError, ValueError):
167
167
  padding = ""
168
168
 
169
169
  # Build result with style
@@ -52,7 +52,7 @@ def create_key_bindings(
52
52
  buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
53
53
  buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
54
54
  return
55
- except Exception:
55
+ except (AttributeError, TypeError):
56
56
  # Fall through to default behavior if anything goes wrong
57
57
  pass
58
58
 
@@ -111,7 +111,7 @@ def create_key_bindings(
111
111
 
112
112
  if should_refresh:
113
113
  buf.start_completion(select_first=False) # type: ignore[reportUnknownMemberType]
114
- except Exception:
114
+ except (AttributeError, TypeError):
115
115
  pass
116
116
 
117
117
  @kb.add("left")
@@ -136,7 +136,7 @@ def create_key_bindings(
136
136
  # Default behavior: move one character left when possible.
137
137
  if doc.cursor_position > 0: # type: ignore[reportUnknownMemberType]
138
138
  buf.cursor_left() # type: ignore[reportUnknownMemberType]
139
- except Exception:
139
+ except (AttributeError, IndexError, TypeError):
140
140
  pass
141
141
 
142
142
  @kb.add("right")
@@ -163,7 +163,7 @@ def create_key_bindings(
163
163
  # Default behavior: move one character right when possible.
164
164
  if doc.cursor_position < len(doc.text): # type: ignore[reportUnknownMemberType]
165
165
  buf.cursor_right() # type: ignore[reportUnknownMemberType]
166
- except Exception:
166
+ except (AttributeError, IndexError, TypeError):
167
167
  pass
168
168
 
169
169
  return kb
@@ -148,7 +148,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
148
148
  plus = parts[2] # like '+12,4'
149
149
  new_start = int(plus[1:].split(",")[0])
150
150
  new_ln = new_start
151
- except Exception:
151
+ except (IndexError, ValueError):
152
152
  new_ln = None
153
153
  if has_rendered_diff_content:
154
154
  grid.add_row(Text(f"{'⋮':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
@@ -1,4 +1,4 @@
1
- from importlib.metadata import version
1
+ from importlib.metadata import PackageNotFoundError, version
2
2
 
3
3
  from rich import box
4
4
  from rich.console import Group, RenderableType
@@ -17,7 +17,7 @@ def _get_version() -> str:
17
17
  """Get the current version of klaude-code."""
18
18
  try:
19
19
  return version("klaude-code")
20
- except Exception:
20
+ except PackageNotFoundError:
21
21
  return "unknown"
22
22
 
23
23
 
@@ -18,7 +18,7 @@ from klaude_code.ui.rich.theme import ThemeKey
18
18
 
19
19
  # Tool markers (Unicode symbols for UI display)
20
20
  MARK_GENERIC = "⚒"
21
- MARK_BASH = ""
21
+ MARK_BASH = "$"
22
22
  MARK_PLAN = "Ξ"
23
23
  MARK_READ = "←"
24
24
  MARK_EDIT = "±"
@@ -192,7 +192,7 @@ class MarkdownStream:
192
192
 
193
193
  try:
194
194
  tokens = self._parser.parse(text)
195
- except Exception:
195
+ except Exception: # markdown-it-py may raise various internal errors during parsing
196
196
  return 0
197
197
 
198
198
  top_level: list[Token] = [token for token in tokens if token.level == 0 and token.map is not None]
@@ -276,7 +276,7 @@ def get_theme(theme: str | None = None) -> Themes:
276
276
  ThemeKey.RESUME_FLAG.value: "bold reverse " + palette.green,
277
277
  ThemeKey.RESUME_INFO.value: palette.green,
278
278
  # CONFIGURATION DISPLAY
279
- ThemeKey.CONFIG_TABLE_HEADER.value: palette.green,
279
+ ThemeKey.CONFIG_TABLE_HEADER.value: palette.grey1,
280
280
  ThemeKey.CONFIG_STATUS_OK.value: palette.green,
281
281
  ThemeKey.CONFIG_STATUS_PRIMARY.value: palette.yellow,
282
282
  ThemeKey.CONFIG_STATUS_ERROR.value: palette.red,
@@ -182,7 +182,7 @@ def _parse_osc_color_response(data: bytes) -> tuple[int, int, int] | None:
182
182
 
183
183
  try:
184
184
  text = data.decode("ascii", errors="ignore")
185
- except Exception:
185
+ except LookupError: # encoding lookup failure (should not happen with "ascii")
186
186
  return None
187
187
 
188
188
  match = _OSC_BG_REGEX.search(text)
@@ -48,7 +48,7 @@ def start_esc_interrupt_monitor(
48
48
  try:
49
49
  fd = sys.stdin.fileno()
50
50
  old = termios.tcgetattr(fd)
51
- except Exception as exc: # pragma: no cover - environment dependent
51
+ except OSError as exc: # pragma: no cover - environment dependent
52
52
  log((f"esc monitor init error: {exc}", "r red"))
53
53
  return
54
54
 
@@ -60,7 +60,7 @@ def start_esc_interrupt_monitor(
60
60
  continue
61
61
  try:
62
62
  ch = os.read(fd, 1).decode(errors="ignore")
63
- except Exception:
63
+ except OSError:
64
64
  continue
65
65
  if ch != "\x1b":
66
66
  continue
@@ -71,7 +71,7 @@ def start_esc_interrupt_monitor(
71
71
  while r2:
72
72
  try:
73
73
  seq += os.read(fd, 1).decode(errors="ignore")
74
- except Exception:
74
+ except OSError:
75
75
  break
76
76
  r2, _, _ = select.select([sys.stdin], [], [], 0.0)
77
77
 
@@ -127,7 +127,7 @@ def install_sigint_double_press_exit(
127
127
 
128
128
  try:
129
129
  signal.signal(signal.SIGINT, _handler)
130
- except Exception: # pragma: no cover - platform dependent
130
+ except (OSError, ValueError): # pragma: no cover - platform dependent
131
131
  # If installing the handler fails, restore() will be a no-op.
132
132
  return lambda: None
133
133
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.26
4
- Summary: Add your description here
3
+ Version: 1.2.27
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
@@ -21,7 +21,7 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  # Klaude Code
23
23
 
24
- Multi-model code agent CLI.
24
+ Minimal code agent CLI.
25
25
 
26
26
  ## Features
27
27
  - **Multi-provider**: Anthropic, OpenAI Responses API, OpenRouter
@@ -92,147 +92,141 @@ klaude [--model <name>] [--select-model]
92
92
 
93
93
  ### Configuration
94
94
 
95
- An example config will be created in `~/.klaude/klaude-config.yaml` when first run.
95
+ #### Quick Start (Zero Config)
96
96
 
97
- Open the configuration file in editor:
97
+ Klaude comes with built-in provider configurations. Just set an API key environment variable and start using it:
98
+
99
+ ```bash
100
+ # Pick one (or more) of these:
101
+ export ANTHROPIC_API_KEY=sk-ant-xxx # Claude models
102
+ export OPENAI_API_KEY=sk-xxx # GPT models
103
+ export OPENROUTER_API_KEY=sk-or-xxx # OpenRouter (multi-provider)
104
+ export DEEPSEEK_API_KEY=sk-xxx # DeepSeek models
105
+ export MOONSHOT_API_KEY=sk-xxx # Moonshot/Kimi models
106
+
107
+ # Then just run:
108
+ klaude
109
+ ```
110
+
111
+ On first run, you'll be prompted to select a model. Your choice is saved as `main_model`.
112
+
113
+ #### Built-in Providers
114
+
115
+ | Provider | Env Variable | Models |
116
+ |-------------|-----------------------|-------------------------------------------------------------------------------|
117
+ | anthropic | `ANTHROPIC_API_KEY` | sonnet, opus |
118
+ | openai | `OPENAI_API_KEY` | gpt-5.2 |
119
+ | openrouter | `OPENROUTER_API_KEY` | gpt-5.2, gpt-5.2-fast, gpt-5.1-codex-max, sonnet, opus, haiku, kimi, gemini-* |
120
+ | deepseek | `DEEPSEEK_API_KEY` | deepseek |
121
+ | moonshot | `MOONSHOT_API_KEY` | kimi@moonshot |
122
+ | codex | N/A (OAuth) | gpt-5.2-codex |
123
+
124
+ List all configured providers and models:
125
+
126
+ ```bash
127
+ klaude list
128
+ ```
129
+
130
+ Models from providers without a valid API key are shown as dimmed/unavailable.
131
+
132
+ #### Custom Configuration
133
+
134
+ User config file: `~/.klaude/klaude-config.yaml`
135
+
136
+ Open in editor:
98
137
 
99
138
  ```bash
100
139
  klaude config
101
140
  ```
102
141
 
103
- An example config yaml:
142
+ ##### Adding Models to Built-in Providers
143
+
144
+ You can add custom models to existing providers without redefining the entire provider:
104
145
 
105
146
  ```yaml
147
+ # Just specify provider_name and your new models - no need for protocol/api_key
106
148
  provider_list:
107
- - provider_name: openrouter
108
- protocol: openrouter # support <responses|openrouter|anthropic|openai>
109
- api_key: <your-openrouter-api-key>
110
-
111
- - provider_name: openai-responses
112
- protocol: responses
113
- api_key: <your-openai-api-key>
114
-
115
- - provider_name: anthropic
116
- protocol: anthropic
117
- api_key: <your-anthropic-api-key>
118
-
119
- - provider_name: moonshot
120
- protocol: anthropic
121
- base_url: https://api.moonshot.cn/anthropic
122
- api_key: <your-api-key>
123
-
124
- - provider_name: deepseek
125
- protocol: anthropic
126
- base_url: https://api.deepseek.com/anthropic
127
- api_key: <your-api-key>
128
-
129
- model_list:
130
-
131
- - model_name: deepseek
132
- provider: deepseek
133
- model_params:
134
- model: deepseek-reasoner
135
- context_limit: 128000
136
- thinking:
137
- type: enabled
138
- budget_tokens: 8192
139
- cost:
140
- currency: CNY
141
- input: 2
142
- output: 3
143
- cache_read: 0.2
144
-
145
- - model_name: codex-max
146
- provider: openai-responses
147
- model_params:
148
- model: gpt-5.1-codex-max
149
- thinking:
150
- reasoning_effort: medium
151
- context_limit: 400000
152
- max_tokens: 128000
153
- cost:
154
- input: 1.25
155
- output: 10
156
- cache_read: 0.13
157
-
158
- - model_name: gpt-5.1
159
- provider: openrouter
160
- model_params:
161
- model: openai/gpt-5.1
162
- context_limit: 400000
163
- max_tokens: 128000
164
- verbosity: high
165
- thinking:
166
- reasoning_effort: high
167
- cost:
168
- input: 1.25
169
- output: 10
170
- cache_read: 0.13
171
-
172
- - model_name: kimi@moonshot
173
- provider: moonshot
174
- model_params:
175
- model: kimi-k2-thinking
176
- context_limit: 262144
177
- thinking:
178
- type: enabled
179
- budget_tokens: 8192
180
- cost:
181
- currency: CNY
182
- input: 4
183
- output: 16
184
- cache_read: 1
185
-
186
- - model_name: opus
187
- provider: openrouter
188
- model_params:
189
- model: anthropic/claude-4.5-opus
190
- context_limit: 200000
191
- provider_routing:
192
- only: [ google-vertex ]
193
- verbosity: high
194
- thinking:
195
- type: enabled
196
- budget_tokens: 31999
197
- cost:
198
- input: 5
199
- output: 25
200
- cache_read: 0.5
201
- cache_write: 6.25
202
-
203
- - model_name: gemini
204
- provider: openrouter
205
- model_params:
206
- model: google/gemini-3-pro-preview
207
- context_limit: 1048576
208
- thinking:
209
- reasoning_effort: medium
210
- cost:
211
- input: 2
212
- output: 12
213
- cache_read: 0.2
214
-
215
- - model_name: haiku
216
- provider: anthropic
217
- model_params:
218
- model: claude-haiku-4-5-20251001
219
- context_limit: 200000
220
- cost:
221
- input: 1
222
- output: 5
223
- cache_read: 0.1
224
- cache_write: 1.25
149
+ - provider_name: openrouter
150
+ model_list:
151
+ - model_name: my-custom-model
152
+ model_params:
153
+ model: some-provider/some-model-id
154
+ context_limit: 200000
155
+ ```
156
+
157
+ Your models are merged with built-in models. To override a built-in model, use the same `model_name`.
225
158
 
159
+ ##### Overriding Provider Settings
160
+
161
+ Override provider-level settings (like api_key) while keeping built-in models:
162
+
163
+ ```yaml
164
+ provider_list:
165
+ - provider_name: anthropic
166
+ api_key: sk-my-custom-key # Override the default ${ANTHROPIC_API_KEY}
167
+ # Built-in models (sonnet, opus) are still available
168
+ ```
169
+
170
+ ##### Adding New Providers
171
+
172
+ For providers not in the built-in list, you must specify `protocol`:
173
+
174
+ ```yaml
175
+ provider_list:
176
+ - provider_name: my-azure-openai
177
+ protocol: openai
178
+ api_key: ${AZURE_OPENAI_KEY}
179
+ base_url: https://my-instance.openai.azure.com/
180
+ is_azure: true
181
+ azure_api_version: "2024-02-15-preview"
182
+ model_list:
183
+ - model_name: gpt-4-azure
184
+ model_params:
185
+ model: gpt-4
186
+ context_limit: 128000
187
+ ```
188
+
189
+ ##### Full Example
190
+
191
+ ```yaml
192
+ # User configuration - merged with built-in config
226
193
  main_model: opus
227
194
 
228
195
  sub_agent_models:
229
- oracle: gpt-5.1
230
- explore: haiku
196
+ oracle: gpt-4.1
197
+ explore: sonnet
231
198
  task: opus
232
- webagent: haiku
199
+ webagent: sonnet
233
200
 
201
+ provider_list:
202
+ # Add models to built-in openrouter
203
+ - provider_name: openrouter
204
+ model_list:
205
+ - model_name: qwen-coder
206
+ model_params:
207
+ model: qwen/qwen-2.5-coder-32b-instruct
208
+ context_limit: 131072
209
+
210
+ # Add a completely new provider
211
+ - provider_name: local-ollama
212
+ protocol: openai
213
+ base_url: http://localhost:11434/v1
214
+ api_key: ollama
215
+ model_list:
216
+ - model_name: local-llama
217
+ model_params:
218
+ model: llama3.2
219
+ context_limit: 8192
234
220
  ```
235
221
 
222
+ ##### Supported Protocols
223
+
224
+ - `anthropic` - Anthropic Claude API
225
+ - `openai` - OpenAI-compatible API
226
+ - `responses` - OpenAI Responses API (for o-series, GPT-5, Codex)
227
+ - `openrouter` - OpenRouter API
228
+ - `codex` - OpenAI Codex CLI (OAuth-based)
229
+
236
230
  List configured providers and models:
237
231
 
238
232
  ```bash