klaude-code 1.8.0__py3-none-any.whl → 1.9.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/auth/base.py +101 -0
  2. klaude_code/auth/claude/__init__.py +6 -0
  3. klaude_code/auth/claude/exceptions.py +9 -0
  4. klaude_code/auth/claude/oauth.py +172 -0
  5. klaude_code/auth/claude/token_manager.py +26 -0
  6. klaude_code/auth/codex/token_manager.py +10 -50
  7. klaude_code/cli/auth_cmd.py +127 -46
  8. klaude_code/cli/config_cmd.py +4 -2
  9. klaude_code/cli/cost_cmd.py +14 -9
  10. klaude_code/cli/list_model.py +248 -200
  11. klaude_code/command/prompt-commit.md +73 -0
  12. klaude_code/config/assets/builtin_config.yaml +36 -3
  13. klaude_code/config/config.py +24 -5
  14. klaude_code/config/thinking.py +4 -4
  15. klaude_code/core/prompt.py +1 -1
  16. klaude_code/llm/anthropic/client.py +28 -3
  17. klaude_code/llm/claude/__init__.py +3 -0
  18. klaude_code/llm/claude/client.py +95 -0
  19. klaude_code/llm/codex/client.py +1 -1
  20. klaude_code/llm/registry.py +3 -1
  21. klaude_code/protocol/llm_param.py +2 -1
  22. klaude_code/protocol/sub_agent/__init__.py +1 -2
  23. klaude_code/session/session.py +4 -4
  24. klaude_code/ui/renderers/metadata.py +6 -26
  25. klaude_code/ui/rich/theme.py +6 -5
  26. klaude_code/ui/utils/common.py +46 -0
  27. {klaude_code-1.8.0.dist-info → klaude_code-1.9.0.dist-info}/METADATA +25 -5
  28. {klaude_code-1.8.0.dist-info → klaude_code-1.9.0.dist-info}/RECORD +30 -25
  29. klaude_code/command/prompt-jj-describe.md +0 -32
  30. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
  31. klaude_code/protocol/sub_agent/oracle.py +0 -91
  32. {klaude_code-1.8.0.dist-info → klaude_code-1.9.0.dist-info}/WHEEL +0 -0
  33. {klaude_code-1.8.0.dist-info → klaude_code-1.9.0.dist-info}/entry_points.txt +0 -0
@@ -81,14 +81,23 @@ class ProviderConfig(llm_param.LLMConfigProviderParameter):
81
81
  """
82
82
  from klaude_code.protocol.llm_param import LLMClientProtocol
83
83
 
84
- if self.protocol == LLMClientProtocol.CODEX:
84
+ if self.protocol == LLMClientProtocol.CODEX_OAUTH:
85
85
  # Codex uses OAuth authentication, not API key
86
86
  from klaude_code.auth.codex.token_manager import CodexTokenManager
87
87
 
88
88
  token_manager = CodexTokenManager()
89
89
  state = token_manager.get_state()
90
- # Consider available if logged in and token not expired
91
- return state is None or state.is_expired()
90
+ # Consider available if logged in. Token refresh happens on-demand.
91
+ return state is None
92
+
93
+ if self.protocol == LLMClientProtocol.CLAUDE_OAUTH:
94
+ # Claude uses OAuth authentication, not API key
95
+ from klaude_code.auth.claude.token_manager import ClaudeTokenManager
96
+
97
+ token_manager = ClaudeTokenManager()
98
+ state = token_manager.get_state()
99
+ # Consider available if logged in. Token refresh happens on-demand.
100
+ return state is None
92
101
 
93
102
  if self.protocol == LLMClientProtocol.BEDROCK:
94
103
  # Bedrock uses AWS credentials, not API key. Region is always required.
@@ -182,7 +191,17 @@ class Config(BaseModel):
182
191
  for provider in self.provider_list:
183
192
  # Resolve ${ENV_VAR} syntax for api_key
184
193
  api_key = provider.get_resolved_api_key()
185
- if not api_key:
194
+
195
+ # Some protocols do not use API keys for authentication.
196
+ if (
197
+ provider.protocol
198
+ not in {
199
+ llm_param.LLMClientProtocol.CODEX_OAUTH,
200
+ llm_param.LLMClientProtocol.CLAUDE_OAUTH,
201
+ llm_param.LLMClientProtocol.BEDROCK,
202
+ }
203
+ and not api_key
204
+ ):
186
205
  continue
187
206
  for model in provider.model_list:
188
207
  if model.model_name == model_name:
@@ -243,7 +262,7 @@ def get_example_config() -> UserConfig:
243
262
  """Generate example config for user reference (will be commented out)."""
244
263
  return UserConfig(
245
264
  main_model="opus",
246
- sub_agent_models={"explore": "haiku", "oracle": "gpt-5.2", "webagent": "sonnet", "task": "sonnet"},
265
+ sub_agent_models={"explore": "haiku", "webagent": "sonnet", "task": "sonnet"},
247
266
  provider_list=[
248
267
  UserProviderConfig(
249
268
  provider_name="my-provider",
@@ -91,12 +91,12 @@ def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
91
91
 
92
92
  protocol = config.protocol
93
93
 
94
- if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
94
+ if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX_OAUTH):
95
95
  if thinking.reasoning_effort:
96
96
  return f"reasoning_effort={thinking.reasoning_effort}"
97
97
  return "not set"
98
98
 
99
- if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
99
+ if protocol in (llm_param.LLMClientProtocol.ANTHROPIC, llm_param.LLMClientProtocol.CLAUDE_OAUTH):
100
100
  if thinking.type == "disabled":
101
101
  return "off"
102
102
  if thinking.type == "enabled":
@@ -201,7 +201,7 @@ def get_thinking_picker_data(config: llm_param.LLMConfigParameter) -> ThinkingPi
201
201
  model_name = config.model
202
202
  thinking = config.thinking
203
203
 
204
- if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
204
+ if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX_OAUTH):
205
205
  levels = get_levels_for_responses(model_name)
206
206
  return ThinkingPickerData(
207
207
  options=_build_effort_options(levels),
@@ -209,7 +209,7 @@ def get_thinking_picker_data(config: llm_param.LLMConfigParameter) -> ThinkingPi
209
209
  current_value=_get_current_effort_value(thinking),
210
210
  )
211
211
 
212
- if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
212
+ if protocol in (llm_param.LLMClientProtocol.ANTHROPIC, llm_param.LLMClientProtocol.CLAUDE_OAUTH):
213
213
  return ThinkingPickerData(
214
214
  options=_build_budget_options(),
215
215
  message="Select thinking level:",
@@ -101,7 +101,7 @@ def load_system_prompt(
101
101
  file_key = _get_file_key(model_name, protocol)
102
102
  base_prompt = _load_base_prompt(file_key)
103
103
 
104
- if protocol == llm_param.LLMClientProtocol.CODEX:
104
+ if protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
105
105
  # Do not append environment info for Codex protocol
106
106
  return base_prompt
107
107
 
@@ -6,6 +6,7 @@ from typing import Any, override
6
6
  import anthropic
7
7
  import httpx
8
8
  from anthropic import APIError
9
+ from anthropic.types.beta import BetaTextBlockParam
9
10
  from anthropic.types.beta.beta_input_json_delta import BetaInputJSONDelta
10
11
  from anthropic.types.beta.beta_raw_content_block_delta_event import BetaRawContentBlockDeltaEvent
11
12
  from anthropic.types.beta.beta_raw_content_block_start_event import BetaRawContentBlockStartEvent
@@ -27,13 +28,37 @@ from klaude_code.llm.usage import MetadataTracker
27
28
  from klaude_code.protocol import llm_param, model
28
29
  from klaude_code.trace import DebugType, log_debug
29
30
 
31
+ _IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."
30
32
 
31
- def build_payload(param: llm_param.LLMCallParameter) -> MessageCreateParamsStreaming:
32
- """Build Anthropic API request parameters."""
33
+
34
+ def build_payload(
35
+ param: llm_param.LLMCallParameter,
36
+ *,
37
+ extra_betas: list[str] | None = None,
38
+ ) -> MessageCreateParamsStreaming:
39
+ """Build Anthropic API request parameters.
40
+
41
+ Args:
42
+ param: LLM call parameters.
43
+ extra_betas: Additional beta flags to prepend to the betas list.
44
+ """
33
45
  messages = convert_history_to_input(param.input, param.model)
34
46
  tools = convert_tool_schema(param.tools)
35
47
  system = convert_system_to_input(param.system)
36
48
 
49
+ # Add identity block at the beginning of the system prompt
50
+ identity_block: BetaTextBlockParam = {
51
+ "type": "text",
52
+ "text": _IDENTITY,
53
+ "cache_control": {"type": "ephemeral"},
54
+ }
55
+ system = [identity_block, *system]
56
+
57
+ betas = ["interleaved-thinking-2025-05-14"]
58
+ if extra_betas:
59
+ # Prepend extra betas, avoiding duplicates
60
+ betas = [b for b in extra_betas if b not in betas] + betas
61
+
37
62
  payload: MessageCreateParamsStreaming = {
38
63
  "model": str(param.model),
39
64
  "tool_choice": {
@@ -46,7 +71,7 @@ def build_payload(param: llm_param.LLMCallParameter) -> MessageCreateParamsStrea
46
71
  "messages": messages,
47
72
  "system": system,
48
73
  "tools": tools,
49
- "betas": ["interleaved-thinking-2025-05-14", "context-1m-2025-08-07"],
74
+ "betas": betas,
50
75
  }
51
76
 
52
77
  if param.thinking and param.thinking.type == "enabled":
@@ -0,0 +1,3 @@
1
+ from .client import ClaudeClient
2
+
3
+ __all__ = ["ClaudeClient"]
@@ -0,0 +1,95 @@
1
+ import json
2
+ from collections.abc import AsyncGenerator
3
+ from typing import override
4
+
5
+ import anthropic
6
+ import httpx
7
+ from anthropic import APIError
8
+
9
+ from klaude_code.auth.claude.exceptions import ClaudeNotLoggedInError
10
+ from klaude_code.auth.claude.oauth import ClaudeOAuth
11
+ from klaude_code.auth.claude.token_manager import ClaudeTokenManager
12
+ from klaude_code.llm.anthropic.client import build_payload, parse_anthropic_stream
13
+ from klaude_code.llm.client import LLMClientABC
14
+ from klaude_code.llm.input_common import apply_config_defaults
15
+ from klaude_code.llm.registry import register
16
+ from klaude_code.llm.usage import MetadataTracker
17
+ from klaude_code.protocol import llm_param, model
18
+ from klaude_code.trace import DebugType, log_debug
19
+
20
+ _CLAUDE_OAUTH_REQUIRED_BETAS: tuple[str, ...] = (
21
+ "oauth-2025-04-20",
22
+ "fine-grained-tool-streaming-2025-05-14",
23
+ )
24
+
25
+
26
+ @register(llm_param.LLMClientProtocol.CLAUDE_OAUTH)
27
+ class ClaudeClient(LLMClientABC):
28
+ """Claude OAuth client using Anthropic messages API with Bearer auth token."""
29
+
30
+ def __init__(self, config: llm_param.LLMConfigParameter):
31
+ super().__init__(config)
32
+
33
+ if config.base_url:
34
+ raise ValueError("CLAUDE protocol does not support custom base_url")
35
+
36
+ self._token_manager = ClaudeTokenManager()
37
+ self._oauth = ClaudeOAuth(self._token_manager)
38
+
39
+ if not self._token_manager.is_logged_in():
40
+ raise ClaudeNotLoggedInError("Claude authentication required. Run 'klaude login claude' first.")
41
+
42
+ self.client = self._create_client()
43
+
44
+ def _create_client(self) -> anthropic.AsyncAnthropic:
45
+ token = self._oauth.ensure_valid_token()
46
+ return anthropic.AsyncAnthropic(
47
+ auth_token=token,
48
+ timeout=httpx.Timeout(300.0, connect=15.0, read=285.0),
49
+ )
50
+
51
+ def _ensure_valid_token(self) -> None:
52
+ state = self._token_manager.get_state()
53
+ if state is None:
54
+ raise ClaudeNotLoggedInError("Not logged in to Claude. Run 'klaude login claude' first.")
55
+
56
+ if state.is_expired():
57
+ self._oauth.refresh()
58
+ self.client = self._create_client()
59
+
60
+ @classmethod
61
+ @override
62
+ def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
63
+ return cls(config)
64
+
65
+ @override
66
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
67
+ self._ensure_valid_token()
68
+ param = apply_config_defaults(param, self.get_llm_config())
69
+
70
+ metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
71
+
72
+ # Anthropic OAuth requires the oauth beta flag
73
+ extra_betas = list(_CLAUDE_OAUTH_REQUIRED_BETAS)
74
+ payload = build_payload(param, extra_betas=extra_betas)
75
+
76
+ # Keep the interleaved-thinking beta in sync with configured thinking.
77
+ if not (param.thinking and param.thinking.type == "enabled"):
78
+ payload["betas"] = [b for b in payload.get("betas", []) if b != "interleaved-thinking-2025-05-14"]
79
+
80
+ log_debug(
81
+ json.dumps(payload, ensure_ascii=False, default=str),
82
+ style="yellow",
83
+ debug_type=DebugType.LLM_PAYLOAD,
84
+ )
85
+
86
+ stream = self.client.beta.messages.create(
87
+ **payload,
88
+ extra_headers={"extra": json.dumps({"session_id": param.session_id}, sort_keys=True)},
89
+ )
90
+
91
+ try:
92
+ async for item in parse_anthropic_stream(stream, param, metadata_tracker):
93
+ yield item
94
+ except (APIError, httpx.HTTPError) as e:
95
+ yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
@@ -67,7 +67,7 @@ CODEX_HEADERS = {
67
67
  }
68
68
 
69
69
 
70
- @register(llm_param.LLMClientProtocol.CODEX)
70
+ @register(llm_param.LLMClientProtocol.CODEX_OAUTH)
71
71
  class CodexClient(LLMClientABC):
72
72
  """LLM client for Codex API using ChatGPT subscription."""
73
73
 
@@ -23,9 +23,11 @@ def _load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
23
23
  # Import only the needed module to trigger @register decorator
24
24
  if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
25
25
  importlib.import_module("klaude_code.llm.anthropic")
26
+ elif protocol == llm_param.LLMClientProtocol.CLAUDE_OAUTH:
27
+ importlib.import_module("klaude_code.llm.claude")
26
28
  elif protocol == llm_param.LLMClientProtocol.BEDROCK:
27
29
  importlib.import_module("klaude_code.llm.bedrock")
28
- elif protocol == llm_param.LLMClientProtocol.CODEX:
30
+ elif protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
29
31
  importlib.import_module("klaude_code.llm.codex")
30
32
  elif protocol == llm_param.LLMClientProtocol.OPENAI:
31
33
  importlib.import_module("klaude_code.llm.openai_compatible")
@@ -12,8 +12,9 @@ class LLMClientProtocol(Enum):
12
12
  RESPONSES = "responses"
13
13
  OPENROUTER = "openrouter"
14
14
  ANTHROPIC = "anthropic"
15
+ CLAUDE_OAUTH = "claude_oauth"
15
16
  BEDROCK = "bedrock"
16
- CODEX = "codex"
17
+ CODEX_OAUTH = "codex_oauth"
17
18
  GOOGLE = "google"
18
19
 
19
20
 
@@ -37,7 +37,7 @@ class SubAgentProfile:
37
37
  """
38
38
 
39
39
  # Identity - single name used for type, tool_name, config_key, and prompt_key
40
- name: str # e.g., "Task", "Oracle", "Explore"
40
+ name: str # e.g., "Task", "Explore", "WebAgent"
41
41
 
42
42
  # Tool schema
43
43
  description: str # Tool description shown to the main agent
@@ -112,6 +112,5 @@ def sub_agent_tool_names(enabled_only: bool = False, model_name: str | None = No
112
112
 
113
113
  # Import sub-agent modules to trigger registration
114
114
  from klaude_code.protocol.sub_agent import explore as explore # noqa: E402
115
- from klaude_code.protocol.sub_agent import oracle as oracle # noqa: E402
116
115
  from klaude_code.protocol.sub_agent import task as task # noqa: E402
117
116
  from klaude_code.protocol.sub_agent import web as web # noqa: E402
@@ -193,14 +193,14 @@ class Session(BaseModel):
193
193
  self.conversation_history.extend(items)
194
194
  self._invalidate_messages_count_cache()
195
195
 
196
- new_user_messages = [
197
- it.content for it in items if isinstance(it, model.UserMessageItem) and it.content
198
- ]
196
+ new_user_messages = [it.content for it in items if isinstance(it, model.UserMessageItem) and it.content]
199
197
  if new_user_messages:
200
198
  if self._user_messages_cache is None:
201
199
  # Build from full history once to ensure correctness when resuming older sessions.
202
200
  self._user_messages_cache = [
203
- it.content for it in self.conversation_history if isinstance(it, model.UserMessageItem) and it.content
201
+ it.content
202
+ for it in self.conversation_history
203
+ if isinstance(it, model.UserMessageItem) and it.content
204
204
  ]
205
205
  else:
206
206
  self._user_messages_cache.extend(new_user_messages)
@@ -11,7 +11,7 @@ from klaude_code.protocol import events, model
11
11
  from klaude_code.trace import is_debug_enabled
12
12
  from klaude_code.ui.renderers.common import create_grid
13
13
  from klaude_code.ui.rich.theme import ThemeKey
14
- from klaude_code.ui.utils.common import format_number
14
+ from klaude_code.ui.utils.common import format_model_params, format_number
15
15
 
16
16
 
17
17
  def _get_version() -> str:
@@ -184,38 +184,18 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
184
184
  (e.llm_config.provider_name, ThemeKey.WELCOME_INFO),
185
185
  )
186
186
 
187
- # Collect all config items to display
188
- config_items: list[tuple[str, str]] = []
189
-
190
- if e.llm_config.thinking is not None:
191
- if e.llm_config.thinking.reasoning_effort:
192
- config_items.append(("reasoning-effort", e.llm_config.thinking.reasoning_effort))
193
- if e.llm_config.thinking.reasoning_summary:
194
- config_items.append(("reasoning-summary", e.llm_config.thinking.reasoning_summary))
195
- if e.llm_config.thinking.budget_tokens:
196
- config_items.append(("thinking-budget", str(e.llm_config.thinking.budget_tokens)))
197
-
198
- if e.llm_config.verbosity:
199
- config_items.append(("verbosity", str(e.llm_config.verbosity)))
200
-
201
- if pr := e.llm_config.provider_routing:
202
- if pr.sort:
203
- config_items.append(("provider-sort", str(pr.sort)))
204
- if pr.only:
205
- config_items.append(("provider-only", ">".join(pr.only)))
206
- if pr.order:
207
- config_items.append(("provider-order", ">".join(pr.order)))
187
+ # Use format_model_params for consistent formatting
188
+ param_strings = format_model_params(e.llm_config)
208
189
 
209
190
  # Render config items with tree-style prefixes
210
- for i, (key, value) in enumerate(config_items):
211
- is_last = i == len(config_items) - 1
191
+ for i, param_str in enumerate(param_strings):
192
+ is_last = i == len(param_strings) - 1
212
193
  prefix = "└─ " if is_last else "├─ "
213
194
  panel_content.append_text(
214
195
  Text.assemble(
215
196
  ("\n", ThemeKey.WELCOME_INFO),
216
197
  (prefix, ThemeKey.LINES),
217
- (f"{key}: ", ThemeKey.WELCOME_INFO),
218
- (value, ThemeKey.WELCOME_INFO),
198
+ (param_str, ThemeKey.WELCOME_INFO),
219
199
  )
220
200
  )
221
201
 
@@ -197,8 +197,9 @@ class ThemeKey(str, Enum):
197
197
  CONFIG_STATUS_PRIMARY = "config.status.primary"
198
198
  CONFIG_STATUS_ERROR = "config.status.error"
199
199
  CONFIG_ITEM_NAME = "config.item.name"
200
+ CONFIG_MODEL_ID = "config.model.id"
200
201
  CONFIG_PARAM_LABEL = "config.param.label"
201
- CONFIG_PANEL_BORDER = "config.panel.border"
202
+
202
203
 
203
204
  def __str__(self) -> str:
204
205
  return self.value
@@ -300,13 +301,13 @@ def get_theme(theme: str | None = None) -> Themes:
300
301
  ThemeKey.RESUME_FLAG.value: "bold reverse " + palette.green,
301
302
  ThemeKey.RESUME_INFO.value: palette.green,
302
303
  # CONFIGURATION DISPLAY
303
- ThemeKey.CONFIG_TABLE_HEADER.value: palette.grey1,
304
+ ThemeKey.CONFIG_TABLE_HEADER.value: "bold " + palette.grey1,
304
305
  ThemeKey.CONFIG_STATUS_OK.value: palette.green,
305
- ThemeKey.CONFIG_STATUS_PRIMARY.value: palette.yellow,
306
+ ThemeKey.CONFIG_STATUS_PRIMARY.value: "bold " + palette.yellow,
306
307
  ThemeKey.CONFIG_STATUS_ERROR.value: palette.red,
307
308
  ThemeKey.CONFIG_ITEM_NAME.value: palette.cyan,
308
- ThemeKey.CONFIG_PARAM_LABEL.value: palette.grey1,
309
- ThemeKey.CONFIG_PANEL_BORDER.value: palette.grey3,
309
+ ThemeKey.CONFIG_MODEL_ID.value: palette.blue,
310
+ ThemeKey.CONFIG_PARAM_LABEL.value: "dim",
310
311
  ThemeKey.CONFIG_PROVIDER.value: palette.cyan + " bold",
311
312
  }
312
313
  ),
@@ -1,6 +1,10 @@
1
1
  import re
2
2
  import subprocess
3
3
  from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from klaude_code.protocol.llm_param import LLMConfigModelParameter, OpenRouterProviderRouting
4
8
 
5
9
  LEADING_NEWLINES_REGEX = re.compile(r"^\n{2,}")
6
10
 
@@ -88,3 +92,45 @@ def show_path_with_tilde(path: Path | None = None):
88
92
  return f"~/{relative_path}"
89
93
  except ValueError:
90
94
  return str(path)
95
+
96
+
97
+ def format_model_params(model_params: "LLMConfigModelParameter") -> list[str]:
98
+ """Format model parameters in a concise style.
99
+
100
+ Returns a list of formatted parameter strings like:
101
+ - "reasoning medium"
102
+ - "thinking budget 10000"
103
+ - "verbosity 2"
104
+ - "provider-routing: {...}"
105
+ """
106
+ parts: list[str] = []
107
+
108
+ if model_params.thinking:
109
+ if model_params.thinking.reasoning_effort:
110
+ parts.append(f"reasoning {model_params.thinking.reasoning_effort}")
111
+ if model_params.thinking.reasoning_summary:
112
+ parts.append(f"reason-summary {model_params.thinking.reasoning_summary}")
113
+ if model_params.thinking.budget_tokens:
114
+ parts.append(f"thinking budget {model_params.thinking.budget_tokens}")
115
+
116
+ if model_params.verbosity:
117
+ parts.append(f"verbosity {model_params.verbosity}")
118
+
119
+ if model_params.provider_routing:
120
+ parts.append(f"provider routing {_format_provider_routing(model_params.provider_routing)}")
121
+
122
+ return parts
123
+
124
+
125
+ def _format_provider_routing(pr: "OpenRouterProviderRouting") -> str:
126
+ """Format provider routing settings concisely."""
127
+ items: list[str] = []
128
+ if pr.sort:
129
+ items.append(pr.sort)
130
+ if pr.only:
131
+ items.append(">".join(pr.only))
132
+ if pr.order:
133
+ items.append(">".join(pr.order))
134
+ if pr.ignore:
135
+ items.append(f"ignore {'>'.join(pr.ignore)}")
136
+ return " · ".join(items) if items else ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -120,11 +120,12 @@ On first run, you'll be prompted to select a model. Your choice is saved as `mai
120
120
  | Provider | Env Variable | Models |
121
121
  |-------------|-----------------------|-------------------------------------------------------------------------------|
122
122
  | anthropic | `ANTHROPIC_API_KEY` | sonnet, opus |
123
+ | claude | N/A (OAuth) | sonnet@claude, opus@claude (requires Claude Pro/Max subscription) |
123
124
  | openai | `OPENAI_API_KEY` | gpt-5.2 |
124
125
  | openrouter | `OPENROUTER_API_KEY` | gpt-5.2, gpt-5.2-fast, gpt-5.1-codex-max, sonnet, opus, haiku, kimi, gemini-* |
125
126
  | deepseek | `DEEPSEEK_API_KEY` | deepseek |
126
127
  | moonshot | `MOONSHOT_API_KEY` | kimi@moonshot |
127
- | codex | N/A (OAuth) | gpt-5.2-codex |
128
+ | codex | N/A (OAuth) | gpt-5.2-codex (requires ChatGPT Pro subscription) |
128
129
 
129
130
  List all configured providers and models:
130
131
 
@@ -134,6 +135,26 @@ klaude list
134
135
 
135
136
  Models from providers without a valid API key are shown as dimmed/unavailable.
136
137
 
138
+ #### OAuth Login
139
+
140
+ For subscription-based providers (Claude Pro/Max, ChatGPT Pro), use the login command:
141
+
142
+ ```bash
143
+ # Interactive provider selection
144
+ klaude login
145
+
146
+ # Or specify provider directly
147
+ klaude login claude # Claude Pro/Max subscription
148
+ klaude login codex # ChatGPT Pro subscription
149
+ ```
150
+
151
+ To logout:
152
+
153
+ ```bash
154
+ klaude logout claude
155
+ klaude logout codex
156
+ ```
157
+
137
158
  #### Custom Configuration
138
159
 
139
160
  User config file: `~/.klaude/klaude-config.yaml`
@@ -240,7 +261,6 @@ provider_list:
240
261
  main_model: opus
241
262
 
242
263
  sub_agent_models:
243
- oracle: gpt-4.1
244
264
  explore: sonnet
245
265
  task: opus
246
266
  webagent: sonnet
@@ -269,12 +289,13 @@ provider_list:
269
289
  ##### Supported Protocols
270
290
 
271
291
  - `anthropic` - Anthropic Claude API
292
+ - `claude_oauth` - Claude OAuth (for Claude Pro/Max subscribers)
272
293
  - `openai` - OpenAI-compatible API
273
294
  - `responses` - OpenAI Responses API (for o-series, GPT-5, Codex)
274
295
  - `openrouter` - OpenRouter API
275
296
  - `google` - Google Gemini API
276
297
  - `bedrock` - AWS Bedrock (uses AWS credentials instead of api_key)
277
- - `codex` - OpenAI Codex CLI (OAuth-based)
298
+ - `codex_oauth` - OpenAI Codex CLI (OAuth-based, for ChatGPT Pro subscribers)
278
299
 
279
300
  List configured providers and models:
280
301
 
@@ -374,4 +395,3 @@ The main agent can spawn specialized sub-agents for specific tasks:
374
395
  | **Explore** | Fast codebase exploration - find files, search code, answer questions about the codebase |
375
396
  | **Task** | Handle complex multi-step tasks autonomously |
376
397
  | **WebAgent** | Search the web, fetch pages, and analyze content |
377
- | **Oracle** | Advanced reasoning advisor for code reviews, architecture planning, and bug analysis |