deepy-cli 0.2.13__tar.gz → 0.2.15__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 (100) hide show
  1. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/PKG-INFO +26 -9
  2. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/README.md +25 -8
  3. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/pyproject.toml +1 -1
  4. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/__init__.py +1 -1
  5. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/cli.py +259 -29
  6. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/config/__init__.py +44 -0
  7. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/config/settings.py +331 -29
  8. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/input_suggestions.py +14 -5
  9. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/agent.py +14 -0
  10. deepy_cli-0.2.15/src/deepy/llm/provider.py +143 -0
  11. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/thinking.py +13 -0
  12. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/prompts/system.py +1 -1
  13. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/session_cost.py +8 -2
  14. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/status.py +6 -2
  15. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tools/agents.py +129 -26
  16. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/app.py +137 -67
  17. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/screens.py +41 -8
  18. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/widgets.py +91 -12
  19. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/exit_summary.py +11 -2
  20. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/model_picker.py +91 -12
  21. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/status_footer.py +1 -1
  22. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/terminal.py +299 -43
  23. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/welcome.py +9 -2
  24. deepy_cli-0.2.13/src/deepy/llm/provider.py +0 -82
  25. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/__main__.py +0 -0
  26. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/__init__.py +0 -0
  27. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  28. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  29. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  30. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/Search.md +0 -0
  31. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/WebFetch.md +0 -0
  32. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/WebSearch.md +0 -0
  33. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/__init__.py +0 -0
  34. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/apply_patch.md +0 -0
  35. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/edit_text.md +0 -0
  36. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/read_file.md +0 -0
  37. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/shell.md +0 -0
  38. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/todo_write.md +0 -0
  39. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/data/tools/write_file.md +0 -0
  40. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/errors.py +0 -0
  41. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/__init__.py +0 -0
  42. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/compaction.py +0 -0
  43. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/context.py +0 -0
  44. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/events.py +0 -0
  45. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/model_capabilities.py +0 -0
  46. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/replay.py +0 -0
  47. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/llm/runner.py +0 -0
  48. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/mcp.py +0 -0
  49. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/prompts/__init__.py +0 -0
  50. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/prompts/compact.py +0 -0
  51. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/prompts/init_agents.py +0 -0
  52. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/prompts/rules.py +0 -0
  53. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/prompts/runtime_context.py +0 -0
  54. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/prompts/tool_docs.py +0 -0
  55. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/sessions/__init__.py +0 -0
  56. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/sessions/jsonl.py +0 -0
  57. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/sessions/manager.py +0 -0
  58. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/skill_market.py +0 -0
  59. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/skills.py +0 -0
  60. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/todos.py +0 -0
  61. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tools/__init__.py +0 -0
  62. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tools/builtin.py +0 -0
  63. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tools/file_state.py +0 -0
  64. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tools/result.py +0 -0
  65. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tools/search.py +0 -0
  66. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tools/shell_output.py +0 -0
  67. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tools/shell_utils.py +0 -0
  68. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/__init__.py +0 -0
  69. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/commands.py +0 -0
  70. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/compat.py +0 -0
  71. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/diff.py +0 -0
  72. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/runner.py +0 -0
  73. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/tui/state.py +0 -0
  74. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/types/__init__.py +0 -0
  75. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/types/sdk.py +0 -0
  76. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/types/tool_payloads.py +0 -0
  77. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/__init__.py +0 -0
  78. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/app.py +0 -0
  79. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/ask_user_question.py +0 -0
  80. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/file_mentions.py +0 -0
  81. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/loading_text.py +0 -0
  82. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/local_command.py +0 -0
  83. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/markdown.py +0 -0
  84. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/message_view.py +0 -0
  85. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/prompt_buffer.py +0 -0
  86. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/prompt_input.py +0 -0
  87. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/session_list.py +0 -0
  88. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/session_picker.py +0 -0
  89. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/skill_picker.py +0 -0
  90. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/slash_commands.py +0 -0
  91. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/styles.py +0 -0
  92. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/theme_picker.py +0 -0
  93. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/ui/thinking_state.py +0 -0
  94. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/update_check.py +0 -0
  95. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/usage.py +0 -0
  96. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/utils/__init__.py +0 -0
  97. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/utils/debug_logger.py +0 -0
  98. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/utils/error_logger.py +0 -0
  99. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/utils/json.py +0 -0
  100. {deepy_cli-0.2.13 → deepy_cli-0.2.15}/src/deepy/utils/notify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: deepy-cli
3
- Version: 0.2.13
3
+ Version: 0.2.15
4
4
  Summary: Deepy - Vibe coding for DeepSeek models in your terminal
5
5
  Keywords: deepseek,coding-agent,terminal,cli,agents
6
6
  Author: kirineko
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
  <h1 align="center">Deepy</h1>
38
38
 
39
39
  <p align="center">
40
- A terminal coding agent built for DeepSeek.
40
+ A terminal coding agent for DeepSeek and OpenAI-compatible providers.
41
41
  <br>
42
42
  Read projects, edit files, run commands, search the web, and keep long project context in one recoverable terminal session.
43
43
  </p>
@@ -56,7 +56,8 @@ Description-Content-Type: text/markdown
56
56
 
57
57
  ## What Deepy Does
58
58
 
59
- Deepy is a Python CLI coding agent for DeepSeek's OpenAI-compatible models. It
59
+ Deepy is a Python CLI coding agent for DeepSeek and supported
60
+ OpenAI-compatible providers. It
60
61
  keeps the working loop inside your terminal: inspect a project, ask questions,
61
62
  modify code, run validation commands, search or fetch web pages, and resume the
62
63
  same project session later.
@@ -67,9 +68,10 @@ of hiding tool calls behind chat text.
67
68
 
68
69
  ## Why Use It
69
70
 
70
- - **DeepSeek-first defaults**: starts with `deepseek-v4-pro`, thinking enabled,
71
- and `reasoning_effort=max`. Use `/model` to switch V4 Pro / V4 Flash and
72
- choose `none`, `high`, or `max` thinking strength.
71
+ - **Provider-aware model selection**: starts with DeepSeek `deepseek-v4-pro`,
72
+ thinking enabled, and `reasoning_effort=max`. Use `/model` to switch between
73
+ DeepSeek, OpenRouter MiMo, and Xiaomi MiMo models with provider-specific
74
+ thinking choices.
73
75
  - **Project-aware coding tools**: read files, write new files, modify existing
74
76
  files with stale-write protection, run shell commands, and review readable
75
77
  diffs.
@@ -147,7 +149,7 @@ uv tool install deepy-cli
147
149
 
148
150
  The installed command is `deepy`.
149
151
 
150
- 4. Configure your DeepSeek API key and start Deepy:
152
+ 4. Configure your provider API key and start Deepy:
151
153
 
152
154
  ```bash
153
155
  deepy config setup
@@ -250,7 +252,7 @@ uv tool uninstall deepy-cli
250
252
  Inside an interactive Deepy session:
251
253
 
252
254
  ```text
253
- /model Select model and thinking strength
255
+ /model Select provider, model, and thinking mode
254
256
  /status Show usage, context pressure, and DeepSeek balance
255
257
  /resume Resume a previous project session
256
258
  /new Start a fresh session
@@ -279,6 +281,7 @@ Deepy uses TOML configuration at `~/.deepy/config.toml`.
279
281
  ```toml
280
282
  [model]
281
283
  api_key = "sk-..."
284
+ provider = "deepseek"
282
285
  name = "deepseek-v4-pro"
283
286
  base_url = "https://api.deepseek.com"
284
287
  thinking = true
@@ -297,10 +300,24 @@ theme = "auto" # auto, dark, or light
297
300
  Set config without the interactive wizard:
298
301
 
299
302
  ```bash
300
- deepy config init --api-key sk-... --model deepseek-v4-pro
303
+ deepy config init --api-key sk-... --provider deepseek --model deepseek-v4-pro
304
+ deepy config init --api-key sk-or-... --provider openrouter --model xiaomi/mimo-v2.5-pro
305
+ deepy config init --api-key sk-or-... --provider openrouter --model anthropic/claude-sonnet-4.5 --thinking minimal
306
+ deepy config init --api-key sk-... --provider xiaomi --model mimo-v2.5-pro
301
307
  deepy config theme light
302
308
  ```
303
309
 
310
+ Supported provider/model pairs:
311
+
312
+ - `deepseek`: `deepseek-v4-pro`, `deepseek-v4-flash`; thinking modes `none`,
313
+ `high`, `max`.
314
+ - `openrouter`: UI model selection offers `xiaomi/mimo-v2.5-pro`,
315
+ `xiaomi/mimo-v2.5`; setup/init may also use a model id copied from
316
+ OpenRouter. Thinking modes are `enabled`, `disabled`, `xhigh`, `high`,
317
+ `medium`, `low`, `minimal`, `none`.
318
+ - `xiaomi`: `mimo-v2.5-pro`, `mimo-v2.5`; thinking modes `enabled`,
319
+ `disabled`.
320
+
304
321
  WebSearch uses Deepy's hosted SearXNG endpoint by default. You can override it:
305
322
 
306
323
  ```toml
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">Deepy</h1>
6
6
 
7
7
  <p align="center">
8
- A terminal coding agent built for DeepSeek.
8
+ A terminal coding agent for DeepSeek and OpenAI-compatible providers.
9
9
  <br>
10
10
  Read projects, edit files, run commands, search the web, and keep long project context in one recoverable terminal session.
11
11
  </p>
@@ -24,7 +24,8 @@
24
24
 
25
25
  ## What Deepy Does
26
26
 
27
- Deepy is a Python CLI coding agent for DeepSeek's OpenAI-compatible models. It
27
+ Deepy is a Python CLI coding agent for DeepSeek and supported
28
+ OpenAI-compatible providers. It
28
29
  keeps the working loop inside your terminal: inspect a project, ask questions,
29
30
  modify code, run validation commands, search or fetch web pages, and resume the
30
31
  same project session later.
@@ -35,9 +36,10 @@ of hiding tool calls behind chat text.
35
36
 
36
37
  ## Why Use It
37
38
 
38
- - **DeepSeek-first defaults**: starts with `deepseek-v4-pro`, thinking enabled,
39
- and `reasoning_effort=max`. Use `/model` to switch V4 Pro / V4 Flash and
40
- choose `none`, `high`, or `max` thinking strength.
39
+ - **Provider-aware model selection**: starts with DeepSeek `deepseek-v4-pro`,
40
+ thinking enabled, and `reasoning_effort=max`. Use `/model` to switch between
41
+ DeepSeek, OpenRouter MiMo, and Xiaomi MiMo models with provider-specific
42
+ thinking choices.
41
43
  - **Project-aware coding tools**: read files, write new files, modify existing
42
44
  files with stale-write protection, run shell commands, and review readable
43
45
  diffs.
@@ -115,7 +117,7 @@ uv tool install deepy-cli
115
117
 
116
118
  The installed command is `deepy`.
117
119
 
118
- 4. Configure your DeepSeek API key and start Deepy:
120
+ 4. Configure your provider API key and start Deepy:
119
121
 
120
122
  ```bash
121
123
  deepy config setup
@@ -218,7 +220,7 @@ uv tool uninstall deepy-cli
218
220
  Inside an interactive Deepy session:
219
221
 
220
222
  ```text
221
- /model Select model and thinking strength
223
+ /model Select provider, model, and thinking mode
222
224
  /status Show usage, context pressure, and DeepSeek balance
223
225
  /resume Resume a previous project session
224
226
  /new Start a fresh session
@@ -247,6 +249,7 @@ Deepy uses TOML configuration at `~/.deepy/config.toml`.
247
249
  ```toml
248
250
  [model]
249
251
  api_key = "sk-..."
252
+ provider = "deepseek"
250
253
  name = "deepseek-v4-pro"
251
254
  base_url = "https://api.deepseek.com"
252
255
  thinking = true
@@ -265,10 +268,24 @@ theme = "auto" # auto, dark, or light
265
268
  Set config without the interactive wizard:
266
269
 
267
270
  ```bash
268
- deepy config init --api-key sk-... --model deepseek-v4-pro
271
+ deepy config init --api-key sk-... --provider deepseek --model deepseek-v4-pro
272
+ deepy config init --api-key sk-or-... --provider openrouter --model xiaomi/mimo-v2.5-pro
273
+ deepy config init --api-key sk-or-... --provider openrouter --model anthropic/claude-sonnet-4.5 --thinking minimal
274
+ deepy config init --api-key sk-... --provider xiaomi --model mimo-v2.5-pro
269
275
  deepy config theme light
270
276
  ```
271
277
 
278
+ Supported provider/model pairs:
279
+
280
+ - `deepseek`: `deepseek-v4-pro`, `deepseek-v4-flash`; thinking modes `none`,
281
+ `high`, `max`.
282
+ - `openrouter`: UI model selection offers `xiaomi/mimo-v2.5-pro`,
283
+ `xiaomi/mimo-v2.5`; setup/init may also use a model id copied from
284
+ OpenRouter. Thinking modes are `enabled`, `disabled`, `xhigh`, `high`,
285
+ `medium`, `low`, `minimal`, `none`.
286
+ - `xiaomi`: `mimo-v2.5-pro`, `mimo-v2.5`; thinking modes `enabled`,
287
+ `disabled`.
288
+
272
289
  WebSearch uses Deepy's hosted SearXNG endpoint by default. You can override it:
273
290
 
274
291
  ```toml
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.2.13"
3
+ version = "0.2.15"
4
4
  description = "Deepy - Vibe coding for DeepSeek models in your terminal"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.2.13"
3
+ __version__ = "0.2.15"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -10,20 +10,24 @@ import tomli_w
10
10
 
11
11
  from . import __version__
12
12
  from .config import (
13
+ PROVIDER_CATALOG,
13
14
  Settings,
15
+ allows_custom_model_for_provider,
16
+ default_base_url_for_provider,
17
+ default_model_for_provider,
18
+ default_thinking_mode_for_provider,
19
+ is_supported_provider,
20
+ is_valid_thinking_mode_for_provider,
14
21
  load_settings,
22
+ provider_info_for,
15
23
  settings_to_toml_dict,
24
+ thinking_modes_for_provider,
16
25
  ui_theme_from_selection,
17
26
  ui_theme_number,
18
27
  update_config_theme,
19
28
  write_config,
20
29
  )
21
- from .config.settings import (
22
- DEFAULT_BASE_URL,
23
- DEFAULT_MODEL,
24
- DEFAULT_UI_THEME,
25
- UI_THEMES,
26
- )
30
+ from .config.settings import DEFAULT_UI_THEME, UI_THEMES
27
31
  from .errors import format_error_display
28
32
  from .llm.provider import build_provider_bundle
29
33
  from .llm.runner import DEFAULT_MAX_TURNS, run_prompt_once
@@ -52,9 +56,11 @@ def _build_parser() -> argparse.ArgumentParser:
52
56
  show_parser.add_argument("--show-secret", action="store_true", help="Show API key.")
53
57
  show_parser.add_argument("--json", action="store_true", help="Print JSON instead of TOML.")
54
58
  init_parser = config_sub.add_parser("init", help="Create a TOML config file.")
55
- init_parser.add_argument("--api-key", help="DeepSeek API key.")
56
- init_parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name.")
57
- init_parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="OpenAI-compatible base URL.")
59
+ init_parser.add_argument("--api-key", help="Provider API key.")
60
+ init_parser.add_argument("--provider", default="deepseek", help="Provider: deepseek, openrouter, or xiaomi.")
61
+ init_parser.add_argument("--model", help="Model name.")
62
+ init_parser.add_argument("--base-url", help="OpenAI-compatible base URL.")
63
+ init_parser.add_argument("--thinking", help="Thinking mode for the provider.")
58
64
  init_parser.add_argument("--theme", default=DEFAULT_UI_THEME, help="UI theme: auto, dark, or light.")
59
65
  init_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
60
66
  setup_parser = config_sub.add_parser("setup", help="Interactively configure Deepy.")
@@ -118,8 +124,10 @@ def _cmd_config_init(args: argparse.Namespace) -> int:
118
124
  _write_config(
119
125
  config_path,
120
126
  api_key=args.api_key or "",
121
- model=args.model,
127
+ provider=args.provider,
128
+ model=args.model or default_model_for_provider(args.provider),
122
129
  base_url=args.base_url,
130
+ thinking_mode=args.thinking,
123
131
  theme=args.theme,
124
132
  )
125
133
  print(f"Wrote {config_path}")
@@ -130,33 +138,88 @@ def _cmd_config_setup(args: argparse.Namespace) -> int:
130
138
  config_path = args.config.expanduser() if args.config else Path.home() / ".deepy" / "config.toml"
131
139
  if config_path.suffix == ".json":
132
140
  raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
141
+ previous_text = config_path.read_text(encoding="utf-8") if config_path.exists() else None
142
+ try:
143
+ _run_config_setup(config_path)
144
+ except (KeyboardInterrupt, EOFError, StopIteration):
145
+ print(_setup_cancelled_message(previous_text), file=sys.stderr)
146
+ return 1
147
+ print(f"Wrote {config_path}")
148
+ return 0
149
+
150
+
151
+ def _run_config_setup(config_path: Path) -> None:
133
152
  if config_path.exists():
134
153
  existing = load_settings(config_path)
135
154
  else:
136
155
  existing = Settings(path=config_path)
137
156
 
138
- print("DeepSeek API keys: https://platform.deepseek.com/api_keys")
157
+ provider = _prompt_provider_value(default=existing.model.provider)
158
+ provider_info = provider_info_for(provider)
159
+ print(f"{provider_info.label} provider selected.")
160
+ if provider_info.api_key_url:
161
+ print(f"Create an API key at {provider_info.api_key_url}")
139
162
  api_key = _prompt_config_value("API key", default=existing.model.api_key or "", is_password=True)
140
- model = _prompt_config_value("Model", default=existing.model.name)
141
- base_url = _prompt_config_value("Base URL", default=existing.model.base_url)
163
+ model = _prompt_model_value(provider, default=existing.model.name)
164
+ base_default = (
165
+ existing.model.base_url
166
+ if existing.model.provider == provider
167
+ else default_base_url_for_provider(provider)
168
+ )
169
+ base_url = _prompt_config_value("Base URL", default=base_default)
170
+ thinking_mode = _prompt_thinking_mode_value(provider, default=existing.model.reasoning_mode)
142
171
  theme = _prompt_theme_value(default=existing.ui.theme)
143
- _write_config(config_path, api_key=api_key, model=model, base_url=base_url, theme=theme)
144
- print(f"Wrote {config_path}")
145
- return 0
172
+ _write_config(
173
+ config_path,
174
+ api_key=api_key,
175
+ provider=provider,
176
+ model=model,
177
+ base_url=base_url,
178
+ thinking_mode=thinking_mode,
179
+ theme=theme,
180
+ )
146
181
 
147
182
 
148
183
  def _cmd_config_reset(args: argparse.Namespace) -> int:
149
184
  config_path = args.config.expanduser() if args.config else Path.home() / ".deepy" / "config.toml"
150
185
  if config_path.suffix == ".json":
151
186
  raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
187
+ previous_text = config_path.read_text(encoding="utf-8") if config_path.exists() else None
152
188
  if config_path.exists():
153
189
  config_path.unlink()
154
190
  print(f"Removed {config_path}")
155
191
  else:
156
192
  print(f"No existing config at {config_path}")
157
193
  print("Starting Deepy configuration setup...")
158
- setup_args = argparse.Namespace(config=args.config, force=True)
159
- return _cmd_config_setup(setup_args)
194
+ try:
195
+ _run_config_setup(config_path)
196
+ except (KeyboardInterrupt, EOFError, StopIteration):
197
+ _restore_config_after_failed_setup(config_path, previous_text)
198
+ print(_setup_cancelled_message(previous_text), file=sys.stderr)
199
+ return 1
200
+ print(f"Wrote {config_path}")
201
+ return 0
202
+
203
+
204
+ def _setup_cancelled_message(previous_text: str | None) -> str:
205
+ if previous_text is None:
206
+ return "Configuration setup cancelled. No config was written."
207
+ return "Configuration setup cancelled. Existing config was left unchanged."
208
+
209
+
210
+ def _restore_config_after_failed_setup(config_path: Path, previous_text: str | None) -> None:
211
+ if previous_text is None:
212
+ try:
213
+ config_path.unlink()
214
+ except FileNotFoundError:
215
+ pass
216
+ return
217
+ config_path.parent.mkdir(parents=True, exist_ok=True)
218
+ config_path.write_text(previous_text, encoding="utf-8")
219
+ try:
220
+ config_path.chmod(0o600)
221
+ except OSError:
222
+ pass
160
223
 
161
224
 
162
225
  def _prompt_config_value(label: str, *, default: str, is_password: bool = False) -> str:
@@ -171,6 +234,151 @@ def _prompt_config_value(label: str, *, default: str, is_password: bool = False)
171
234
  return value or default
172
235
 
173
236
 
237
+ def _prompt_provider_value(*, default: str = "deepseek") -> str:
238
+ print("Provider:")
239
+ for index, provider in enumerate(PROVIDER_CATALOG, 1):
240
+ print(f"{index}. {provider.id} {provider.description}")
241
+ value = _prompt_config_value("Provider number or name", default=_provider_number(default))
242
+ return _provider_from_selection(value, default=default)
243
+
244
+
245
+ def _provider_number(provider: str) -> str:
246
+ for index, item in enumerate(PROVIDER_CATALOG, 1):
247
+ if item.id == provider:
248
+ return str(index)
249
+ return "1"
250
+
251
+
252
+ def _provider_from_selection(value: str, *, default: str = "deepseek") -> str:
253
+ normalized = value.strip().lower()
254
+ by_number = {str(index): item.id for index, item in enumerate(PROVIDER_CATALOG, 1)}
255
+ if normalized in by_number:
256
+ return by_number[normalized]
257
+ if is_supported_provider(normalized):
258
+ return normalized
259
+ return default if is_supported_provider(default) else "deepseek"
260
+
261
+
262
+ def _prompt_model_value(provider: str, *, default: str) -> str:
263
+ provider_info = provider_info_for(provider)
264
+ print("Model:")
265
+ for index, model in enumerate(provider_info.models, 1):
266
+ print(f"{index}. {model.name} {model.description}")
267
+ if allows_custom_model_for_provider(provider):
268
+ print("Or paste any model name copied from the OpenRouter models page.")
269
+ default_value = default if default in {model.name for model in provider_info.models} else provider_info.default_model
270
+ value = _prompt_config_value("Model number or name", default=_model_number(provider, default_value))
271
+ return _model_from_selection(provider, value, default=default_value)
272
+
273
+
274
+ def _model_number(provider: str, model: str) -> str:
275
+ for index, item in enumerate(provider_info_for(provider).models, 1):
276
+ if item.name == model:
277
+ return str(index)
278
+ return "1"
279
+
280
+
281
+ def _model_from_selection(provider: str, value: str, *, default: str) -> str:
282
+ normalized = value.strip()
283
+ models = provider_info_for(provider).models
284
+ by_number = {str(index): item.name for index, item in enumerate(models, 1)}
285
+ if normalized in by_number:
286
+ return by_number[normalized]
287
+ if normalized in {item.name for item in models}:
288
+ return normalized
289
+ if allows_custom_model_for_provider(provider) and normalized:
290
+ return normalized
291
+ return default_model_for_provider(provider) if not default else default
292
+
293
+
294
+ def _prompt_thinking_mode_value(provider: str, *, default: str) -> str:
295
+ if provider == "openrouter":
296
+ return _prompt_openrouter_thinking_mode(default=default)
297
+ modes = thinking_modes_for_provider(provider)
298
+ print("Thinking:")
299
+ for index, mode in enumerate(modes, 1):
300
+ print(f"{index}. {mode}")
301
+ default_value = default if is_valid_thinking_mode_for_provider(default, provider) else default_thinking_mode_for_provider(provider)
302
+ value = _prompt_config_value("Thinking number or name", default=_thinking_mode_number(provider, default_value))
303
+ return _thinking_mode_from_selection(provider, value, default=default_value)
304
+
305
+
306
+ def _thinking_mode_number(provider: str, mode: str) -> str:
307
+ for index, item in enumerate(thinking_modes_for_provider(provider), 1):
308
+ if item == mode:
309
+ return str(index)
310
+ return "1"
311
+
312
+
313
+ def _prompt_openrouter_thinking_mode(*, default: str) -> str:
314
+ current_enabled = default not in {"none", "disabled"}
315
+ print("Thinking:")
316
+ print("1. enabled Reasoning enabled")
317
+ print("2. disabled Reasoning disabled")
318
+ state_default = "1" if current_enabled else "2"
319
+ state_value = _prompt_config_value("Thinking number or name", default=state_default)
320
+ state = _openrouter_thinking_state_from_selection(state_value, default="enabled" if current_enabled else "disabled")
321
+ if state == "disabled":
322
+ return "none"
323
+ print("Reasoning effort:")
324
+ print("1. default Use the model default reasoning strength")
325
+ for index, effort in enumerate(("xhigh", "high", "medium", "low", "minimal"), 2):
326
+ print(f"{index}. {effort}")
327
+ effort_default = _openrouter_effort_number(default)
328
+ effort_value = _prompt_config_value("Reasoning effort number or name", default=effort_default)
329
+ return _openrouter_effort_from_selection(effort_value, default=default)
330
+
331
+
332
+ def _openrouter_thinking_state_from_selection(value: str, *, default: str) -> str:
333
+ normalized = value.strip().lower()
334
+ if normalized in {"1", "enabled", "enable", "on", "true", "yes"}:
335
+ return "enabled"
336
+ if normalized in {"2", "disabled", "disable", "off", "false", "no", "none"}:
337
+ return "disabled"
338
+ return default
339
+
340
+
341
+ def _openrouter_effort_number(mode: str) -> str:
342
+ return {
343
+ "enabled": "1",
344
+ "xhigh": "2",
345
+ "high": "3",
346
+ "medium": "4",
347
+ "low": "5",
348
+ "minimal": "6",
349
+ }.get(mode, "1")
350
+
351
+
352
+ def _openrouter_effort_from_selection(value: str, *, default: str) -> str:
353
+ normalized = value.strip().lower()
354
+ by_number = {
355
+ "1": "enabled",
356
+ "2": "xhigh",
357
+ "3": "high",
358
+ "4": "medium",
359
+ "5": "low",
360
+ "6": "minimal",
361
+ }
362
+ if normalized in by_number:
363
+ return by_number[normalized]
364
+ if normalized in {"default", "enabled"}:
365
+ return "enabled"
366
+ if normalized in {"xhigh", "high", "medium", "low", "minimal"}:
367
+ return normalized
368
+ return default if default in {"enabled", "xhigh", "high", "medium", "low", "minimal"} else "enabled"
369
+
370
+
371
+ def _thinking_mode_from_selection(provider: str, value: str, *, default: str) -> str:
372
+ normalized = value.strip().lower()
373
+ modes = thinking_modes_for_provider(provider)
374
+ by_number = {str(index): mode for index, mode in enumerate(modes, 1)}
375
+ if normalized in by_number:
376
+ return by_number[normalized]
377
+ if normalized in modes:
378
+ return normalized
379
+ return default
380
+
381
+
174
382
  def _prompt_theme_value(*, default: str = DEFAULT_UI_THEME) -> str:
175
383
  print("UI theme:")
176
384
  print("1. auto Detect when possible; falls back to dark")
@@ -180,8 +388,25 @@ def _prompt_theme_value(*, default: str = DEFAULT_UI_THEME) -> str:
180
388
  return ui_theme_from_selection(value, default=default)
181
389
 
182
390
 
183
- def _write_config(config_path: Path, *, api_key: str, model: str, base_url: str, theme: str) -> None:
184
- write_config(config_path, api_key=api_key, model=model, base_url=base_url, theme=theme)
391
+ def _write_config(
392
+ config_path: Path,
393
+ *,
394
+ api_key: str,
395
+ provider: str,
396
+ model: str,
397
+ base_url: str | None,
398
+ theme: str,
399
+ thinking_mode: str | None,
400
+ ) -> None:
401
+ write_config(
402
+ config_path,
403
+ api_key=api_key,
404
+ provider=provider,
405
+ model=model,
406
+ base_url=base_url,
407
+ theme=theme,
408
+ thinking_mode=thinking_mode,
409
+ )
185
410
 
186
411
 
187
412
  def _cmd_config_theme(args: argparse.Namespace) -> int:
@@ -215,6 +440,7 @@ def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
215
440
  "configured" if settings.model.api_key else "missing; run `deepy config setup`",
216
441
  )
217
442
  check("model", bool(settings.model.name), settings.model.name)
443
+ check("provider", bool(settings.model.provider), settings.model.provider)
218
444
  check("base_url", bool(settings.model.base_url), settings.model.base_url)
219
445
  check(
220
446
  "context_window",
@@ -245,10 +471,11 @@ def _doctor(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
245
471
  return 0 if ok else 1, {
246
472
  "ok": ok,
247
473
  "checks": checks,
248
- "thinking": {
249
- "enabled": settings.model.thinking_enabled,
250
- "reasoning_effort": settings.model.reasoning_effort,
251
- "reasoning_mode": settings.model.reasoning_mode,
474
+ "thinking": {
475
+ "provider": settings.model.provider,
476
+ "enabled": settings.model.thinking_enabled,
477
+ "reasoning_effort": settings.model.reasoning_effort,
478
+ "reasoning_mode": settings.model.reasoning_mode,
252
479
  },
253
480
  }
254
481
 
@@ -278,6 +505,7 @@ async def _doctor_live(settings: Settings) -> dict[str, Any]:
278
505
  usage = usage_from_run_result(result)
279
506
  return {
280
507
  "ok": True,
508
+ "provider": settings.model.provider,
281
509
  "model": settings.model.name,
282
510
  "base_url": settings.model.base_url,
283
511
  "api_key": "configured",
@@ -314,14 +542,15 @@ def _cmd_doctor(args: argparse.Namespace) -> int:
314
542
  status = "ok" if item["ok"] else "fail"
315
543
  print(f"{status:4} {item['name']}: {item['detail']}")
316
544
  thinking = report["thinking"]
317
- print(f"info reasoning: mode={thinking['reasoning_mode']}")
545
+ print(f"info provider: {thinking['provider']}")
546
+ print(f"info thinking: mode={thinking['reasoning_mode']}")
318
547
  live = report.get("live")
319
548
  if isinstance(live, dict):
320
549
  if live.get("ok"):
321
550
  usage = live.get("usage")
322
551
  print(
323
- "ok live: "
324
- f"model={live.get('model')} base_url={live.get('base_url')} "
552
+ "ok live: "
553
+ f"provider={live.get('provider')} model={live.get('model')} base_url={live.get('base_url')} "
325
554
  f"response={live.get('response_summary')!r} "
326
555
  f"{format_usage_line(usage if isinstance(usage, dict) else TokenUsage())}"
327
556
  )
@@ -415,9 +644,10 @@ def _cmd_status(args: argparse.Namespace) -> int:
415
644
  def _ensure_interactive_settings(args: argparse.Namespace) -> Settings:
416
645
  settings = load_settings(args.config)
417
646
  if not settings.model.api_key:
418
- print("Deepy needs a DeepSeek API key before starting interactive mode.")
647
+ print("Deepy needs a provider API key before starting interactive mode.")
419
648
  setup_args = argparse.Namespace(config=args.config, force=True)
420
- _cmd_config_setup(setup_args)
649
+ if _cmd_config_setup(setup_args) != 0:
650
+ raise SystemExit(1)
421
651
  settings = load_settings(args.config)
422
652
  if settings.path is not None and not settings.ui.theme_configured:
423
653
  theme = _prompt_theme_value(default=settings.ui.theme)