klaude-code 2.2.0__tar.gz → 2.3.0__tar.gz

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 (238) hide show
  1. {klaude_code-2.2.0 → klaude_code-2.3.0}/PKG-INFO +1 -1
  2. {klaude_code-2.2.0 → klaude_code-2.3.0}/pyproject.toml +11 -1
  3. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/app/runtime.py +2 -15
  4. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/list_model.py +27 -10
  5. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/main.py +25 -9
  6. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/config/assets/builtin_config.yaml +25 -16
  7. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/config/config.py +144 -7
  8. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/config/select_model.py +38 -13
  9. klaude_code-2.3.0/src/klaude_code/config/sub_agent_model_helper.py +217 -0
  10. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/const.py +1 -1
  11. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/agent_profile.py +43 -5
  12. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/executor.py +75 -0
  13. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/manager/llm_clients_builder.py +17 -11
  14. klaude_code-2.3.0/src/klaude_code/core/prompts/prompt-nano-banana.md +1 -0
  15. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/sub_agent_tool.py +2 -1
  16. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/anthropic/client.py +7 -4
  17. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/anthropic/input.py +54 -29
  18. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/google/client.py +1 -1
  19. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/google/input.py +23 -2
  20. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openai_compatible/input.py +22 -13
  21. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openrouter/input.py +37 -25
  22. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/responses/input.py +96 -57
  23. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/commands.py +1 -2
  24. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/events/system.py +4 -0
  25. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/op.py +17 -0
  26. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/op_handler.py +5 -0
  27. klaude_code-2.3.0/src/klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  28. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/sub_agent/__init__.py +10 -14
  29. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/sub_agent/image_gen.py +2 -1
  30. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/session/codec.py +2 -6
  31. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/session/session.py +9 -1
  32. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  33. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/__init__.py +3 -6
  34. klaude_code-2.3.0/src/klaude_code/tui/command/model_cmd.py +57 -0
  35. klaude_code-2.3.0/src/klaude_code/tui/command/model_select.py +144 -0
  36. klaude_code-2.3.0/src/klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
  37. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/bash_syntax.py +1 -1
  38. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/common.py +1 -1
  39. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/developer.py +0 -5
  40. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/metadata.py +1 -63
  41. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  42. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/status.py +49 -3
  43. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/theme.py +2 -0
  44. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/sub_agent.py +25 -46
  45. klaude_code-2.3.0/src/klaude_code/tui/components/welcome.py +99 -0
  46. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/input/prompt_toolkit.py +14 -1
  47. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/renderer.py +2 -3
  48. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/terminal/selector.py +5 -3
  49. klaude_code-2.2.0/src/klaude_code/core/prompts/prompt-nano-banana.md +0 -1
  50. klaude_code-2.2.0/src/klaude_code/tui/command/help_cmd.py +0 -51
  51. klaude_code-2.2.0/src/klaude_code/tui/command/model_cmd.py +0 -94
  52. klaude_code-2.2.0/src/klaude_code/tui/command/model_select.py +0 -84
  53. klaude_code-2.2.0/src/klaude_code/tui/command/release_notes_cmd.py +0 -85
  54. {klaude_code-2.2.0 → klaude_code-2.3.0}/README.md +0 -0
  55. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/__init__.py +0 -0
  56. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/app/__init__.py +0 -0
  57. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/__init__.py +0 -0
  58. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/base.py +0 -0
  59. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/claude/__init__.py +0 -0
  60. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/claude/exceptions.py +0 -0
  61. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/claude/oauth.py +0 -0
  62. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/claude/token_manager.py +0 -0
  63. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/codex/__init__.py +0 -0
  64. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/codex/exceptions.py +0 -0
  65. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/codex/jwt_utils.py +0 -0
  66. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/codex/oauth.py +0 -0
  67. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/auth/codex/token_manager.py +0 -0
  68. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/__init__.py +0 -0
  69. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/auth_cmd.py +0 -0
  70. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/config_cmd.py +0 -0
  71. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/cost_cmd.py +0 -0
  72. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/debug.py +0 -0
  73. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/self_update.py +0 -0
  74. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/cli/session_cmd.py +0 -0
  75. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/config/__init__.py +0 -0
  76. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/config/assets/__init__.py +0 -0
  77. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/config/builtin_config.py +0 -0
  78. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/config/thinking.py +0 -0
  79. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/__init__.py +0 -0
  80. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/agent.py +0 -0
  81. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/manager/__init__.py +0 -0
  82. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/manager/llm_clients.py +0 -0
  83. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/manager/sub_agent_manager.py +0 -0
  84. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-claude-code.md +0 -0
  85. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -0
  86. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +0 -0
  87. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-codex.md +0 -0
  88. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-gemini.md +0 -0
  89. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-minimal.md +0 -0
  90. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-sub-agent-explore.md +0 -0
  91. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-sub-agent-image-gen.md +0 -0
  92. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-sub-agent-web.md +0 -0
  93. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/prompts/prompt-sub-agent.md +0 -0
  94. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/reminders.py +0 -0
  95. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/task.py +0 -0
  96. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/__init__.py +0 -0
  97. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/context.py +0 -0
  98. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/__init__.py +0 -0
  99. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/_utils.py +0 -0
  100. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/apply_patch.py +0 -0
  101. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/apply_patch_tool.md +0 -0
  102. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/apply_patch_tool.py +0 -0
  103. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/diff_builder.py +0 -0
  104. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/edit_tool.md +0 -0
  105. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/edit_tool.py +0 -0
  106. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/read_tool.md +0 -0
  107. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/read_tool.py +0 -0
  108. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/write_tool.md +0 -0
  109. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/file/write_tool.py +0 -0
  110. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/report_back_tool.py +0 -0
  111. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/shell/__init__.py +0 -0
  112. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/shell/bash_tool.md +0 -0
  113. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/shell/bash_tool.py +0 -0
  114. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/shell/command_safety.py +0 -0
  115. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/todo/__init__.py +0 -0
  116. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/todo/todo_write_tool.md +0 -0
  117. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/todo/todo_write_tool.py +0 -0
  118. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/todo/todo_write_tool_raw.md +0 -0
  119. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/todo/update_plan_tool.md +0 -0
  120. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/todo/update_plan_tool.py +0 -0
  121. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/tool_abc.py +0 -0
  122. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/tool_registry.py +0 -0
  123. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/tool_runner.py +0 -0
  124. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/truncation.py +0 -0
  125. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/web/__init__.py +0 -0
  126. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/web/mermaid_tool.md +0 -0
  127. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/web/mermaid_tool.py +0 -0
  128. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/web/web_fetch_tool.md +0 -0
  129. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/web/web_fetch_tool.py +0 -0
  130. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/web/web_search_tool.md +0 -0
  131. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/tool/web/web_search_tool.py +0 -0
  132. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/core/turn.py +0 -0
  133. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/__init__.py +0 -0
  134. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/anthropic/__init__.py +0 -0
  135. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/bedrock/__init__.py +0 -0
  136. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/bedrock/client.py +0 -0
  137. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/claude/__init__.py +0 -0
  138. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/claude/client.py +0 -0
  139. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/client.py +0 -0
  140. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/codex/__init__.py +0 -0
  141. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/codex/client.py +0 -0
  142. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/google/__init__.py +0 -0
  143. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/image.py +0 -0
  144. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/input_common.py +0 -0
  145. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openai_compatible/__init__.py +0 -0
  146. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openai_compatible/client.py +0 -0
  147. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openai_compatible/stream.py +0 -0
  148. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -0
  149. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openrouter/__init__.py +0 -0
  150. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openrouter/client.py +0 -0
  151. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/openrouter/reasoning.py +0 -0
  152. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/registry.py +0 -0
  153. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/responses/__init__.py +0 -0
  154. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/responses/client.py +0 -0
  155. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/llm/usage.py +0 -0
  156. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/log.py +0 -0
  157. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/__init__.py +0 -0
  158. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/events/__init__.py +0 -0
  159. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/events/base.py +0 -0
  160. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/events/chat.py +0 -0
  161. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/events/lifecycle.py +0 -0
  162. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/events/metadata.py +0 -0
  163. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/events/streaming.py +0 -0
  164. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/events/tools.py +0 -0
  165. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/llm_param.py +0 -0
  166. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/message.py +0 -0
  167. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/model.py +0 -0
  168. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/sub_agent/explore.py +0 -0
  169. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/sub_agent/task.py +0 -0
  170. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/sub_agent/web.py +0 -0
  171. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/protocol/tools.py +0 -0
  172. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/session/__init__.py +0 -0
  173. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/session/export.py +0 -0
  174. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/session/selector.py +0 -0
  175. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/session/store.py +0 -0
  176. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/session/templates/export_session.html +0 -0
  177. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/session/templates/mermaid_viewer.html +0 -0
  178. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/__init__.py +0 -0
  179. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/assets/deslop/SKILL.md +0 -0
  180. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/assets/handoff/SKILL.md +0 -0
  181. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/assets/jj-workspace/SKILL.md +0 -0
  182. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/assets/skill-creator/SKILL.md +0 -0
  183. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/loader.py +0 -0
  184. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/manager.py +0 -0
  185. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/skill/system_skills.py +0 -0
  186. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/__init__.py +0 -0
  187. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/clear_cmd.py +0 -0
  188. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/command_abc.py +0 -0
  189. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/copy_cmd.py +0 -0
  190. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/debug_cmd.py +0 -0
  191. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/export_cmd.py +0 -0
  192. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/export_online_cmd.py +0 -0
  193. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/fork_session_cmd.py +0 -0
  194. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/prompt-init.md +0 -0
  195. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/prompt_command.py +0 -0
  196. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/refresh_cmd.py +0 -0
  197. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/registry.py +0 -0
  198. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/resume_cmd.py +0 -0
  199. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/status_cmd.py +0 -0
  200. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/terminal_setup_cmd.py +0 -0
  201. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/command/thinking_cmd.py +0 -0
  202. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/commands.py +0 -0
  203. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/__init__.py +0 -0
  204. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/assistant.py +0 -0
  205. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/diffs.py +0 -0
  206. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/errors.py +0 -0
  207. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/mermaid_viewer.py +0 -0
  208. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/__init__.py +0 -0
  209. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/code_panel.py +0 -0
  210. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/live.py +0 -0
  211. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/markdown.py +0 -0
  212. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/quote.py +0 -0
  213. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/rich/searchable_text.py +0 -0
  214. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/thinking.py +0 -0
  215. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/tools.py +0 -0
  216. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/components/user_input.py +0 -0
  217. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/display.py +0 -0
  218. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/input/__init__.py +0 -0
  219. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/input/clipboard.py +0 -0
  220. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/input/completers.py +0 -0
  221. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/input/key_bindings.py +0 -0
  222. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/machine.py +0 -0
  223. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/runner.py +0 -0
  224. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/terminal/__init__.py +0 -0
  225. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/terminal/color.py +0 -0
  226. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/terminal/control.py +0 -0
  227. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/terminal/image.py +0 -0
  228. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/terminal/notifier.py +0 -0
  229. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/tui/terminal/progress_bar.py +0 -0
  230. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/ui/__init__.py +0 -0
  231. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/ui/common.py +0 -0
  232. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/ui/core/__init__.py +0 -0
  233. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/ui/core/display.py +0 -0
  234. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/ui/core/input.py +0 -0
  235. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/ui/debug_mode.py +0 -0
  236. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/ui/terminal/__init__.py +0 -0
  237. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/ui/terminal/title.py +0 -0
  238. {klaude_code-2.2.0 → klaude_code-2.3.0}/src/klaude_code/update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "klaude-code"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "Minimal code agent CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -40,6 +40,7 @@ dev = [
40
40
  "pyright>=1.1.407",
41
41
  "pytest>=8.4.1",
42
42
  "pytest-cov>=7.0.0",
43
+ "ty>=0.0.8",
43
44
  ]
44
45
 
45
46
 
@@ -74,6 +75,15 @@ exclude = [".venv/"]
74
75
  root = "."
75
76
  extraPaths = ["src"]
76
77
 
78
+ [tool.ty.environment]
79
+ python-version = "3.13"
80
+
81
+ [tool.ty.src]
82
+ include = [
83
+ "src",
84
+ "tests",
85
+ ]
86
+
77
87
  [tool.importlinter]
78
88
  root_packages = ["klaude_code"]
79
89
  include_external_packages = false
@@ -55,24 +55,11 @@ async def initialize_app_components(
55
55
 
56
56
  config = load_config()
57
57
 
58
- if init_config.banana:
59
- # Banana mode is strict: it requires the built-in Nano Banana image model to be available.
60
- required_model = "nano-banana-pro@or"
61
- available = {m.model_name for m in config.iter_model_entries(only_available=True)}
62
- if required_model not in available:
63
- log(
64
- (
65
- f"Error: --banana requires model '{required_model}', but it is not available in the current environment",
66
- "red",
67
- )
68
- )
69
- log(("Hint: set OPENROUTER_API_KEY (Nano Banana Pro is configured via OpenRouter by default)", "yellow"))
70
- raise typer.Exit(2)
71
-
72
58
  try:
73
59
  llm_clients = build_llm_clients(
74
60
  config,
75
61
  model_override=init_config.model,
62
+ skip_sub_agents=init_config.vanilla or init_config.banana,
76
63
  )
77
64
  except ValueError as exc:
78
65
  if init_config.model:
@@ -92,7 +79,7 @@ async def initialize_app_components(
92
79
  elif init_config.vanilla:
93
80
  model_profile_provider = VanillaModelProfileProvider()
94
81
  else:
95
- model_profile_provider = DefaultModelProfileProvider()
82
+ model_profile_provider = DefaultModelProfileProvider(config=config)
96
83
 
97
84
  event_queue: asyncio.Queue[events.Event] = asyncio.Queue()
98
85
 
@@ -243,18 +243,34 @@ def _build_provider_info_panel(provider: ProviderConfig, available: bool) -> Quo
243
243
 
244
244
 
245
245
  def _build_models_table(
246
- provider: ProviderConfig, main_model: str | None, sub_agent_models: dict[str, str] | None = None
246
+ provider: ProviderConfig,
247
+ config: Config,
247
248
  ) -> Table:
248
249
  """Build a table for models under a provider."""
249
250
  provider_available = not provider.is_api_key_missing()
250
251
 
252
+ def _resolve_selector(value: str | None) -> str | None:
253
+ if not value:
254
+ return None
255
+ try:
256
+ resolved = config.resolve_model_location_prefer_available(value) or config.resolve_model_location(value)
257
+ except ValueError:
258
+ return None
259
+ if resolved is None:
260
+ return None
261
+ return f"{resolved[0]}@{resolved[1]}"
262
+
263
+ default_selector = _resolve_selector(config.main_model)
264
+
251
265
  # Build reverse mapping: model_name -> list of agent roles using it
252
266
  model_to_agents: dict[str, list[str]] = {}
253
- if sub_agent_models:
254
- for agent_role, model_name in sub_agent_models.items():
255
- if model_name not in model_to_agents:
256
- model_to_agents[model_name] = []
257
- model_to_agents[model_name].append(agent_role)
267
+ for agent_role, model_name in (config.sub_agent_models or {}).items():
268
+ selector = _resolve_selector(model_name)
269
+ if selector is None:
270
+ continue
271
+ if selector not in model_to_agents:
272
+ model_to_agents[selector] = []
273
+ model_to_agents[selector].append(agent_role)
258
274
 
259
275
  models_table = Table.grid(
260
276
  padding=(0, 2),
@@ -275,10 +291,11 @@ def _build_models_table(
275
291
  else:
276
292
  # Build role tags for this model
277
293
  roles: list[str] = []
278
- if model.model_name == main_model:
294
+ selector = f"{model.model_name}@{provider.provider_name}"
295
+ if selector == default_selector:
279
296
  roles.append("default")
280
- if model.model_name in model_to_agents:
281
- roles.extend(role.lower() for role in model_to_agents[model.model_name])
297
+ if selector in model_to_agents:
298
+ roles.extend(role.lower() for role in model_to_agents[selector])
282
299
 
283
300
  if roles:
284
301
  name = Text.assemble(
@@ -350,6 +367,6 @@ def display_models_and_providers(config: Config, *, show_all: bool = False):
350
367
  console.print()
351
368
 
352
369
  # Models table for this provider
353
- models_table = _build_models_table(provider, config.main_model, config.sub_agent_models)
370
+ models_table = _build_models_table(provider, config)
354
371
  console.print(models_table)
355
372
  console.print("\n")
@@ -124,18 +124,28 @@ def main_callback(
124
124
  raise typer.Exit(2)
125
125
 
126
126
  from klaude_code.app.runtime import AppInitConfig
127
- from klaude_code.tui.command.model_select import select_model_interactive
127
+ from klaude_code.tui.command.model_select import ModelSelectStatus, select_model_interactive
128
128
  from klaude_code.tui.runner import run_interactive
129
129
 
130
130
  update_terminal_title()
131
131
 
132
132
  chosen_model = model
133
133
  if banana:
134
- # Banana mode always uses the built-in Nano Banana Pro image model.
135
- chosen_model = "nano-banana-pro@or"
134
+ keywords = ["gemini-3-pro-image", "gemini-2.5-flash-image"]
135
+ model_result = select_model_interactive(keywords=keywords)
136
+ if model_result.status == ModelSelectStatus.SELECTED and model_result.model is not None:
137
+ chosen_model = model_result.model
138
+ elif model_result.status == ModelSelectStatus.CANCELLED:
139
+ return
140
+ else:
141
+ log(("Error: no available nano-banana model", "red"))
142
+ log(("Hint: set OPENROUTER_API_KEY or GOOGLE_API_KEY to enable nano-banana models", "yellow"))
143
+ raise typer.Exit(2)
136
144
  elif model or select_model:
137
- chosen_model = select_model_interactive(preferred=model)
138
- if chosen_model is None:
145
+ model_result = select_model_interactive(preferred=model)
146
+ if model_result.status == ModelSelectStatus.SELECTED and model_result.model is not None:
147
+ chosen_model = model_result.model
148
+ else:
139
149
  return
140
150
 
141
151
  # Resolve session id before entering asyncio loop
@@ -162,7 +172,12 @@ def main_callback(
162
172
  cfg = load_config()
163
173
 
164
174
  if session_meta.model_config_name:
165
- if any(m.model_name == session_meta.model_config_name for m in cfg.iter_model_entries()):
175
+ try:
176
+ model_is_known = cfg.has_model_config_name(session_meta.model_config_name)
177
+ except ValueError:
178
+ model_is_known = False
179
+
180
+ if model_is_known:
166
181
  chosen_model = session_meta.model_config_name
167
182
  else:
168
183
  log(
@@ -176,7 +191,7 @@ def main_callback(
176
191
  raw_model = session_meta.model_name.strip()
177
192
  if raw_model:
178
193
  matches = [
179
- m.model_name
194
+ m.selector
180
195
  for m in cfg.iter_model_entries()
181
196
  if (m.model_params.model or "").strip().lower() == raw_model.lower()
182
197
  ]
@@ -189,9 +204,10 @@ def main_callback(
189
204
 
190
205
  cfg = load_config()
191
206
  if cfg.main_model is None:
192
- chosen_model = select_model_interactive()
193
- if chosen_model is None:
207
+ model_result = select_model_interactive()
208
+ if model_result.status != ModelSelectStatus.SELECTED or model_result.model is None:
194
209
  raise typer.Exit(1)
210
+ chosen_model = model_result.model
195
211
  # Save the selection as default
196
212
  cfg.main_model = chosen_model
197
213
  from klaude_code.config.config import config_path
@@ -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@ant
10
+ - model_name: sonnet
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@ant
21
+ - model_name: opus
22
22
  model_params:
23
23
  model: claude-opus-4-5-20251101
24
24
  context_limit: 200000
@@ -187,10 +187,10 @@ provider_list:
187
187
  input: 0.5
188
188
  output: 3.0
189
189
  cache_read: 0.05
190
- - model_name: nano-banana-pro@or
190
+ - model_name: nano-banana-pro
191
191
  model_params:
192
192
  model: google/gemini-3-pro-image-preview
193
- context_limit: 1048576
193
+ context_limit: 66000
194
194
  modalities:
195
195
  - image
196
196
  - text
@@ -199,6 +199,18 @@ provider_list:
199
199
  output: 12
200
200
  cache_read: 0.2
201
201
  image: 120
202
+ - model_name: nano-banana
203
+ model_params:
204
+ model: google/gemini-2.5-flash-image
205
+ context_limit: 33000
206
+ modalities:
207
+ - image
208
+ - text
209
+ cost:
210
+ input: 0.3
211
+ output: 2.5
212
+ cache_read: 0.03
213
+ image: 30
202
214
  - model_name: grok
203
215
  model_params:
204
216
  model: x-ai/grok-4.1-fast
@@ -234,7 +246,7 @@ provider_list:
234
246
  protocol: google
235
247
  api_key: ${GOOGLE_API_KEY}
236
248
  model_list:
237
- - model_name: gemini-pro@google
249
+ - model_name: gemini-pro
238
250
  model_params:
239
251
  model: gemini-3-pro-preview
240
252
  context_limit: 1048576
@@ -242,7 +254,7 @@ provider_list:
242
254
  input: 2.0
243
255
  output: 12.0
244
256
  cache_read: 0.2
245
- - model_name: gemini-flash@google
257
+ - model_name: gemini-flash
246
258
  model_params:
247
259
  model: gemini-3-flash-preview
248
260
  context_limit: 1048576
@@ -250,10 +262,10 @@ provider_list:
250
262
  input: 0.5
251
263
  output: 3.0
252
264
  cache_read: 0.05
253
- - model_name: nano-banana-pro@google
265
+ - model_name: nano-banana-pro
254
266
  model_params:
255
267
  model: gemini-3-pro-image-preview
256
- context_limit: 1048576
268
+ context_limit: 66000
257
269
  modalities:
258
270
  - image
259
271
  - text
@@ -269,7 +281,7 @@ provider_list:
269
281
  aws_secret_key: ${AWS_SECRET_ACCESS_KEY}
270
282
  aws_region: ${AWS_REGION}
271
283
  model_list:
272
- - model_name: sonnet@bedrock
284
+ - model_name: sonnet
273
285
  model_params:
274
286
  model: us.anthropic.claude-sonnet-4-5-20250929-v1:0
275
287
  context_limit: 200000
@@ -302,7 +314,7 @@ provider_list:
302
314
  api_key: ${MOONSHOT_API_KEY}
303
315
  base_url: https://api.moonshot.cn/anthropic
304
316
  model_list:
305
- - model_name: kimi@moonshot
317
+ - model_name: kimi
306
318
  model_params:
307
319
  model: kimi-k2-thinking
308
320
  context_limit: 262144
@@ -318,7 +330,7 @@ provider_list:
318
330
  - provider_name: claude-max
319
331
  protocol: claude_oauth
320
332
  model_list:
321
- - model_name: sonnet@claude-max
333
+ - model_name: sonnet
322
334
  model_params:
323
335
  model: claude-sonnet-4-5-20250929
324
336
  context_limit: 200000
@@ -327,7 +339,7 @@ provider_list:
327
339
  output: 15.0
328
340
  cache_read: 0.3
329
341
  cache_write: 3.75
330
- - model_name: opus@claude-max
342
+ - model_name: opus
331
343
  model_params:
332
344
  model: claude-opus-4-5-20251101
333
345
  context_limit: 200000
@@ -340,7 +352,7 @@ provider_list:
340
352
  output: 25.0
341
353
  cache_read: 0.5
342
354
  cache_write: 6.25
343
- - model_name: haiku@claude-max
355
+ - model_name: haiku
344
356
  model_params:
345
357
  model: claude-haiku-4-5-20251001
346
358
  context_limit: 200000
@@ -364,6 +376,3 @@ provider_list:
364
376
  input: 1.75
365
377
  output: 14.0
366
378
  cache_read: 0.17
367
-
368
- sub_agent_models:
369
- ImageGen: nano-banana-pro@or
@@ -140,6 +140,16 @@ class ModelEntry(BaseModel):
140
140
  provider: str
141
141
  model_params: llm_param.LLMConfigModelParameter
142
142
 
143
+ @property
144
+ def selector(self) -> str:
145
+ """Return a provider-qualified model selector.
146
+
147
+ This selector can be persisted in user config (e.g. ``sonnet@openrouter``)
148
+ and later resolved via :meth:`Config.get_model_config`.
149
+ """
150
+
151
+ return f"{self.model_name}@{self.provider}"
152
+
143
153
 
144
154
  class UserConfig(BaseModel):
145
155
  """User configuration (what gets saved to disk)."""
@@ -191,8 +201,103 @@ class Config(BaseModel):
191
201
  """Set the user config reference for saving."""
192
202
  object.__setattr__(self, "_user_config", user_config)
193
203
 
204
+ @classmethod
205
+ def _split_model_selector(cls, model_selector: str) -> tuple[str, str | None]:
206
+ """Split a model selector into (model_name, provider_name).
207
+
208
+ Supported forms:
209
+ - ``sonnet``: unqualified; caller should pick the first matching provider.
210
+ - ``sonnet@openrouter``: provider-qualified.
211
+
212
+ Note: the provider segment is normalized for backwards compatibility.
213
+ """
214
+
215
+ trimmed = model_selector.strip()
216
+ if "@" not in trimmed:
217
+ return trimmed, None
218
+
219
+ base, provider = trimmed.rsplit("@", 1)
220
+ base = base.strip()
221
+ provider = provider.strip()
222
+ if not base or not provider:
223
+ raise ValueError(f"Invalid model selector: {model_selector!r}")
224
+ return base, provider
225
+
226
+ def has_model_config_name(self, model_selector: str) -> bool:
227
+ """Return True if the selector points to a configured model.
228
+
229
+ This check is configuration-only: it does not require a valid API key or
230
+ OAuth login.
231
+ """
232
+
233
+ model_name, provider_name = self._split_model_selector(model_selector)
234
+ if provider_name is not None:
235
+ for provider in self.provider_list:
236
+ if provider.provider_name.casefold() != provider_name.casefold():
237
+ continue
238
+ return any(m.model_name == model_name for m in provider.model_list)
239
+ return False
240
+
241
+ return any(any(m.model_name == model_name for m in provider.model_list) for provider in self.provider_list)
242
+
243
+ def resolve_model_location(self, model_selector: str) -> tuple[str, str] | None:
244
+ """Resolve a selector to (model_name, provider_name), without auth checks.
245
+
246
+ - If the selector is provider-qualified, returns that provider.
247
+ - If unqualified, returns the first provider that defines the model.
248
+ """
249
+
250
+ model_name, provider_name = self._split_model_selector(model_selector)
251
+ if provider_name is not None:
252
+ for provider in self.provider_list:
253
+ if provider.provider_name.casefold() != provider_name.casefold():
254
+ continue
255
+ if any(m.model_name == model_name for m in provider.model_list):
256
+ return model_name, provider.provider_name
257
+ return None
258
+
259
+ for provider in self.provider_list:
260
+ if any(m.model_name == model_name for m in provider.model_list):
261
+ return model_name, provider.provider_name
262
+ return None
263
+
264
+ def resolve_model_location_prefer_available(self, model_selector: str) -> tuple[str, str] | None:
265
+ """Resolve a selector to (model_name, provider_name), preferring usable providers.
266
+
267
+ This uses the same availability logic as :meth:`get_model_config` (API-key
268
+ presence for non-OAuth protocols).
269
+ """
270
+
271
+ requested_model, requested_provider = self._split_model_selector(model_selector)
272
+
273
+ for provider in self.provider_list:
274
+ if requested_provider is not None and provider.provider_name.casefold() != requested_provider.casefold():
275
+ continue
276
+
277
+ api_key = provider.get_resolved_api_key()
278
+ if (
279
+ provider.protocol
280
+ not in {
281
+ llm_param.LLMClientProtocol.CODEX_OAUTH,
282
+ llm_param.LLMClientProtocol.CLAUDE_OAUTH,
283
+ llm_param.LLMClientProtocol.BEDROCK,
284
+ }
285
+ and not api_key
286
+ ):
287
+ continue
288
+
289
+ if any(m.model_name == requested_model for m in provider.model_list):
290
+ return requested_model, provider.provider_name
291
+
292
+ return None
293
+
194
294
  def get_model_config(self, model_name: str) -> llm_param.LLMConfigParameter:
295
+ requested_model, requested_provider = self._split_model_selector(model_name)
296
+
195
297
  for provider in self.provider_list:
298
+ if requested_provider is not None and provider.provider_name.casefold() != requested_provider.casefold():
299
+ continue
300
+
196
301
  # Resolve ${ENV_VAR} syntax for api_key
197
302
  api_key = provider.get_resolved_api_key()
198
303
 
@@ -206,15 +311,22 @@ class Config(BaseModel):
206
311
  }
207
312
  and not api_key
208
313
  ):
314
+ # When provider is explicitly requested, fail fast with a clearer error.
315
+ if requested_provider is not None:
316
+ raise ValueError(
317
+ f"Provider '{provider.provider_name}' is not available (missing API key) for: {model_name}"
318
+ )
209
319
  continue
320
+
210
321
  for model in provider.model_list:
211
- if model.model_name == model_name:
212
- provider_dump = provider.model_dump(exclude={"model_list"})
213
- provider_dump["api_key"] = api_key
214
- return llm_param.LLMConfigParameter(
215
- **provider_dump,
216
- **model.model_params.model_dump(),
217
- )
322
+ if model.model_name != requested_model:
323
+ continue
324
+ provider_dump = provider.model_dump(exclude={"model_list"})
325
+ provider_dump["api_key"] = api_key
326
+ return llm_param.LLMConfigParameter(
327
+ **provider_dump,
328
+ **model.model_params.model_dump(),
329
+ )
218
330
 
219
331
  raise ValueError(f"Unknown model: {model_name}")
220
332
 
@@ -235,6 +347,27 @@ class Config(BaseModel):
235
347
  for model in provider.model_list
236
348
  ]
237
349
 
350
+ def has_available_image_model(self) -> bool:
351
+ """Check if any image generation model is available."""
352
+ for entry in self.iter_model_entries(only_available=True):
353
+ if entry.model_params.modalities and "image" in entry.model_params.modalities:
354
+ return True
355
+ return False
356
+
357
+ def get_first_available_nano_banana_model(self) -> str | None:
358
+ """Get the first available nano-banana model, or None."""
359
+ for entry in self.iter_model_entries(only_available=True):
360
+ if "nano-banana" in entry.model_name:
361
+ return entry.model_name
362
+ return None
363
+
364
+ def get_first_available_image_model(self) -> str | None:
365
+ """Get the first available image generation model, or None."""
366
+ for entry in self.iter_model_entries(only_available=True):
367
+ if entry.model_params.modalities and "image" in entry.model_params.modalities:
368
+ return entry.model_name
369
+ return None
370
+
238
371
  async def save(self) -> None:
239
372
  """Save user config to file (excludes builtin providers).
240
373
 
@@ -418,6 +551,10 @@ def create_example_config() -> bool:
418
551
  header = "# Example configuration for klaude-code\n"
419
552
  header += "# Copy this file to klaude-config.yaml and modify as needed.\n"
420
553
  header += "# Run `klaude list` to see available models.\n"
554
+ header += "# Tip: you can pick a provider explicitly with `model@provider` (e.g. `sonnet@openrouter`).\n"
555
+ header += (
556
+ "# If you omit `@provider` (e.g. `sonnet`), klaude picks the first configured provider with credentials.\n"
557
+ )
421
558
  header += "#\n"
422
559
  header += "# Built-in providers (anthropic, openai, openrouter, deepseek) are available automatically.\n"
423
560
  header += "# Just set the corresponding API key environment variable to use them.\n\n"
@@ -50,7 +50,8 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
50
50
 
51
51
  # Only show models from providers with valid API keys
52
52
  models: list[ModelEntry] = sorted(
53
- config.iter_model_entries(only_available=True), key=lambda m: m.model_name.lower()
53
+ config.iter_model_entries(only_available=True),
54
+ key=lambda m: (m.model_name.lower(), m.provider.lower()),
54
55
  )
55
56
 
56
57
  if not models:
@@ -62,26 +63,42 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
62
63
  error_message="No models available",
63
64
  )
64
65
 
65
- names: list[str] = [m.model_name for m in models]
66
+ selectors: list[str] = [m.selector for m in models]
66
67
 
67
68
  # Try to match preferred model name
68
69
  filter_hint = preferred
69
70
  if preferred and preferred.strip():
70
71
  preferred = preferred.strip()
71
- # Exact match
72
- if preferred in names:
72
+
73
+ # Exact match on selector (e.g. sonnet@openrouter)
74
+ if preferred in selectors:
73
75
  return ModelMatchResult(matched_model=preferred, filtered_models=models, filter_hint=None)
74
76
 
77
+ # Exact match on base model name (e.g. sonnet)
78
+ exact_base_matches = [m for m in models if m.model_name == preferred]
79
+ if len(exact_base_matches) == 1:
80
+ return ModelMatchResult(
81
+ matched_model=exact_base_matches[0].selector,
82
+ filtered_models=models,
83
+ filter_hint=None,
84
+ )
85
+ if len(exact_base_matches) > 1:
86
+ return ModelMatchResult(matched_model=None, filtered_models=exact_base_matches, filter_hint=filter_hint)
87
+
75
88
  preferred_lower = preferred.lower()
76
- # Case-insensitive exact match (model_name or model_params.model)
89
+ # Case-insensitive exact match (selector/model_name/model_params.model)
77
90
  exact_ci_matches = [
78
91
  m
79
92
  for m in models
80
- if preferred_lower == m.model_name.lower() or preferred_lower == (m.model_params.model or "").lower()
93
+ if preferred_lower == m.selector.lower()
94
+ or preferred_lower == m.model_name.lower()
95
+ or preferred_lower == (m.model_params.model or "").lower()
81
96
  ]
82
97
  if len(exact_ci_matches) == 1:
83
98
  return ModelMatchResult(
84
- matched_model=exact_ci_matches[0].model_name, filtered_models=models, filter_hint=None
99
+ matched_model=exact_ci_matches[0].selector,
100
+ filtered_models=models,
101
+ filter_hint=None,
85
102
  )
86
103
 
87
104
  # Normalized matching (e.g. gpt52 == gpt-5.2, gpt52 in gpt-5.2-2025-...)
@@ -91,24 +108,30 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
91
108
  normalized_matches = [
92
109
  m
93
110
  for m in models
94
- if preferred_norm == _normalize_model_key(m.model_name)
111
+ if preferred_norm == _normalize_model_key(m.selector)
112
+ or preferred_norm == _normalize_model_key(m.model_name)
95
113
  or preferred_norm == _normalize_model_key(m.model_params.model or "")
96
114
  ]
97
115
  if len(normalized_matches) == 1:
98
116
  return ModelMatchResult(
99
- matched_model=normalized_matches[0].model_name, filtered_models=models, filter_hint=None
117
+ matched_model=normalized_matches[0].selector,
118
+ filtered_models=models,
119
+ filter_hint=None,
100
120
  )
101
121
 
102
122
  if not normalized_matches and len(preferred_norm) >= 4:
103
123
  normalized_matches = [
104
124
  m
105
125
  for m in models
106
- if preferred_norm in _normalize_model_key(m.model_name)
126
+ if preferred_norm in _normalize_model_key(m.selector)
127
+ or preferred_norm in _normalize_model_key(m.model_name)
107
128
  or preferred_norm in _normalize_model_key(m.model_params.model or "")
108
129
  ]
109
130
  if len(normalized_matches) == 1:
110
131
  return ModelMatchResult(
111
- matched_model=normalized_matches[0].model_name, filtered_models=models, filter_hint=None
132
+ matched_model=normalized_matches[0].selector,
133
+ filtered_models=models,
134
+ filter_hint=None,
112
135
  )
113
136
 
114
137
  # Partial match (case-insensitive) on model_name or model_params.model.
@@ -116,10 +139,12 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
116
139
  matches = normalized_matches or [
117
140
  m
118
141
  for m in models
119
- if preferred_lower in m.model_name.lower() or preferred_lower in (m.model_params.model or "").lower()
142
+ if preferred_lower in m.selector.lower()
143
+ or preferred_lower in m.model_name.lower()
144
+ or preferred_lower in (m.model_params.model or "").lower()
120
145
  ]
121
146
  if len(matches) == 1:
122
- return ModelMatchResult(matched_model=matches[0].model_name, filtered_models=models, filter_hint=None)
147
+ return ModelMatchResult(matched_model=matches[0].selector, filtered_models=models, filter_hint=None)
123
148
  if matches:
124
149
  # Multiple matches: filter the list for interactive selection
125
150
  return ModelMatchResult(matched_model=None, filtered_models=matches, filter_hint=filter_hint)