ripperdoc 0.2.8__py3-none-any.whl → 0.2.9__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 (84) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +28 -115
  3. ripperdoc/cli/commands/__init__.py +0 -1
  4. ripperdoc/cli/commands/agents_cmd.py +6 -3
  5. ripperdoc/cli/commands/clear_cmd.py +1 -4
  6. ripperdoc/cli/commands/config_cmd.py +1 -1
  7. ripperdoc/cli/commands/context_cmd.py +3 -2
  8. ripperdoc/cli/commands/doctor_cmd.py +18 -4
  9. ripperdoc/cli/commands/hooks_cmd.py +27 -53
  10. ripperdoc/cli/commands/models_cmd.py +26 -9
  11. ripperdoc/cli/commands/permissions_cmd.py +27 -9
  12. ripperdoc/cli/commands/resume_cmd.py +5 -3
  13. ripperdoc/cli/commands/status_cmd.py +4 -4
  14. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  15. ripperdoc/cli/ui/file_mention_completer.py +2 -1
  16. ripperdoc/cli/ui/interrupt_handler.py +2 -3
  17. ripperdoc/cli/ui/message_display.py +4 -2
  18. ripperdoc/cli/ui/provider_options.py +247 -0
  19. ripperdoc/cli/ui/rich_ui.py +110 -59
  20. ripperdoc/cli/ui/spinner.py +25 -1
  21. ripperdoc/cli/ui/tool_renderers.py +8 -2
  22. ripperdoc/cli/ui/wizard.py +215 -0
  23. ripperdoc/core/agents.py +9 -3
  24. ripperdoc/core/config.py +49 -12
  25. ripperdoc/core/custom_commands.py +7 -6
  26. ripperdoc/core/default_tools.py +11 -2
  27. ripperdoc/core/hooks/config.py +1 -3
  28. ripperdoc/core/hooks/events.py +23 -28
  29. ripperdoc/core/hooks/executor.py +4 -6
  30. ripperdoc/core/hooks/integration.py +12 -21
  31. ripperdoc/core/hooks/manager.py +40 -15
  32. ripperdoc/core/permissions.py +40 -8
  33. ripperdoc/core/providers/anthropic.py +109 -36
  34. ripperdoc/core/providers/gemini.py +70 -5
  35. ripperdoc/core/providers/openai.py +60 -5
  36. ripperdoc/core/query.py +82 -38
  37. ripperdoc/core/query_utils.py +2 -0
  38. ripperdoc/core/skills.py +9 -3
  39. ripperdoc/core/system_prompt.py +4 -2
  40. ripperdoc/core/tool.py +9 -5
  41. ripperdoc/sdk/client.py +2 -2
  42. ripperdoc/tools/ask_user_question_tool.py +5 -3
  43. ripperdoc/tools/background_shell.py +2 -1
  44. ripperdoc/tools/bash_output_tool.py +1 -1
  45. ripperdoc/tools/bash_tool.py +26 -16
  46. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  47. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  48. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  49. ripperdoc/tools/file_edit_tool.py +8 -4
  50. ripperdoc/tools/file_read_tool.py +8 -4
  51. ripperdoc/tools/file_write_tool.py +9 -5
  52. ripperdoc/tools/glob_tool.py +3 -2
  53. ripperdoc/tools/grep_tool.py +3 -2
  54. ripperdoc/tools/kill_bash_tool.py +1 -1
  55. ripperdoc/tools/ls_tool.py +1 -1
  56. ripperdoc/tools/mcp_tools.py +13 -10
  57. ripperdoc/tools/multi_edit_tool.py +8 -7
  58. ripperdoc/tools/notebook_edit_tool.py +7 -4
  59. ripperdoc/tools/skill_tool.py +1 -1
  60. ripperdoc/tools/task_tool.py +5 -4
  61. ripperdoc/tools/todo_tool.py +2 -2
  62. ripperdoc/tools/tool_search_tool.py +3 -2
  63. ripperdoc/utils/conversation_compaction.py +8 -4
  64. ripperdoc/utils/file_watch.py +8 -2
  65. ripperdoc/utils/json_utils.py +2 -1
  66. ripperdoc/utils/mcp.py +11 -3
  67. ripperdoc/utils/memory.py +4 -2
  68. ripperdoc/utils/message_compaction.py +21 -7
  69. ripperdoc/utils/message_formatting.py +11 -7
  70. ripperdoc/utils/messages.py +105 -66
  71. ripperdoc/utils/path_ignore.py +35 -8
  72. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  73. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  74. ripperdoc/utils/safe_get_cwd.py +2 -1
  75. ripperdoc/utils/session_history.py +13 -6
  76. ripperdoc/utils/todo.py +2 -1
  77. ripperdoc/utils/token_estimation.py +6 -1
  78. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +1 -1
  79. ripperdoc-0.2.9.dist-info/RECORD +123 -0
  80. ripperdoc-0.2.8.dist-info/RECORD +0 -121
  81. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
  82. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
  83. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
  84. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
@@ -52,9 +52,7 @@ def _get_scope_info(scope: ScopeType, project_path: Path) -> tuple[str, str]:
52
52
  return "Local (private)", str(project_path / ".ripperdoc" / "config.local.json")
53
53
 
54
54
 
55
- def _get_rules_for_scope(
56
- scope: ScopeType, project_path: Path
57
- ) -> tuple[List[str], List[str]]:
55
+ def _get_rules_for_scope(scope: ScopeType, project_path: Path) -> tuple[List[str], List[str]]:
58
56
  """Return (allow_rules, deny_rules) for a given scope."""
59
57
  if scope == "user":
60
58
  user_config: GlobalConfig = get_global_config()
@@ -76,21 +74,31 @@ def _add_rule(
76
74
  """Add a rule to the specified scope. Returns True if added, False if already exists."""
77
75
  if scope == "user":
78
76
  user_config: GlobalConfig = get_global_config()
79
- rules = user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
77
+ rules = (
78
+ user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
79
+ )
80
80
  if rule in rules:
81
81
  return False
82
82
  rules.append(rule)
83
83
  save_global_config(user_config)
84
84
  elif scope == "project":
85
85
  project_config: ProjectConfig = get_project_config(project_path)
86
- rules = project_config.bash_allow_rules if rule_type == "allow" else project_config.bash_deny_rules
86
+ rules = (
87
+ project_config.bash_allow_rules
88
+ if rule_type == "allow"
89
+ else project_config.bash_deny_rules
90
+ )
87
91
  if rule in rules:
88
92
  return False
89
93
  rules.append(rule)
90
94
  save_project_config(project_config, project_path)
91
95
  else: # local
92
96
  local_config: ProjectLocalConfig = get_project_local_config(project_path)
93
- rules = local_config.local_allow_rules if rule_type == "allow" else local_config.local_deny_rules
97
+ rules = (
98
+ local_config.local_allow_rules
99
+ if rule_type == "allow"
100
+ else local_config.local_deny_rules
101
+ )
94
102
  if rule in rules:
95
103
  return False
96
104
  rules.append(rule)
@@ -107,21 +115,31 @@ def _remove_rule(
107
115
  """Remove a rule from the specified scope. Returns True if removed, False if not found."""
108
116
  if scope == "user":
109
117
  user_config: GlobalConfig = get_global_config()
110
- rules = user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
118
+ rules = (
119
+ user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
120
+ )
111
121
  if rule not in rules:
112
122
  return False
113
123
  rules.remove(rule)
114
124
  save_global_config(user_config)
115
125
  elif scope == "project":
116
126
  project_config: ProjectConfig = get_project_config(project_path)
117
- rules = project_config.bash_allow_rules if rule_type == "allow" else project_config.bash_deny_rules
127
+ rules = (
128
+ project_config.bash_allow_rules
129
+ if rule_type == "allow"
130
+ else project_config.bash_deny_rules
131
+ )
118
132
  if rule not in rules:
119
133
  return False
120
134
  rules.remove(rule)
121
135
  save_project_config(project_config, project_path)
122
136
  else: # local
123
137
  local_config: ProjectLocalConfig = get_project_local_config(project_path)
124
- rules = local_config.local_allow_rules if rule_type == "allow" else local_config.local_deny_rules
138
+ rules = (
139
+ local_config.local_allow_rules
140
+ if rule_type == "allow"
141
+ else local_config.local_deny_rules
142
+ )
125
143
  if rule not in rules:
126
144
  return False
127
145
  rules.remove(rule)
@@ -72,14 +72,14 @@ def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
72
72
  return None
73
73
 
74
74
  # Handle pagination commands
75
- if choice_text == 'n':
75
+ if choice_text == "n":
76
76
  if current_page < total_pages - 1:
77
77
  current_page += 1
78
78
  continue
79
79
  else:
80
80
  ui.console.print("[yellow]Already at the last page.[/yellow]")
81
81
  continue
82
- elif choice_text == 'p':
82
+ elif choice_text == "p":
83
83
  if current_page > 0:
84
84
  current_page -= 1
85
85
  continue
@@ -89,7 +89,9 @@ def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
89
89
 
90
90
  # Handle session selection
91
91
  if not choice_text.isdigit():
92
- ui.console.print("[red]Please enter a session index number, 'n' for next page, or 'p' for previous page.[/red]")
92
+ ui.console.print(
93
+ "[red]Please enter a session index number, 'n' for next page, or 'p' for previous page.[/red]"
94
+ )
93
95
  continue
94
96
 
95
97
  idx = int(choice_text)
@@ -85,7 +85,7 @@ def _setting_sources_summary(
85
85
  profile: Optional[ModelProfile],
86
86
  memory_files: List[MemoryFile],
87
87
  auth_env_var: Optional[str],
88
- safe_mode: bool,
88
+ yolo_mode: bool,
89
89
  verbose: bool,
90
90
  project_path: Path,
91
91
  ) -> str:
@@ -105,9 +105,9 @@ def _setting_sources_summary(
105
105
  if auth_env_var:
106
106
  sources.append("Environment variables")
107
107
 
108
- config_safe_mode = getattr(config, "safe_mode", True)
108
+ config_yolo_mode = getattr(config, "yolo_mode", False)
109
109
  config_verbose = getattr(config, "verbose", False)
110
- if safe_mode != config_safe_mode or verbose != config_verbose:
110
+ if yolo_mode != config_yolo_mode or verbose != config_verbose:
111
111
  sources.append("Command line arguments")
112
112
 
113
113
  if profile and profile.api_key and not auth_env_var:
@@ -133,7 +133,7 @@ def _handle(ui: Any, _: str) -> bool:
133
133
  profile,
134
134
  memory_files,
135
135
  auth_env_var,
136
- ui.safe_mode,
136
+ ui.yolo_mode,
137
137
  ui.verbose,
138
138
  ui.project_path,
139
139
  )
@@ -109,7 +109,8 @@ def _list_tasks(ui: Any) -> bool:
109
109
  table.add_row(escape(task_id), "[red]error[/]", escape(str(exc)), "-")
110
110
  logger.warning(
111
111
  "[tasks_cmd] Failed to read background task status: %s: %s",
112
- type(exc).__name__, exc,
112
+ type(exc).__name__,
113
+ exc,
113
114
  extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
114
115
  )
115
116
  continue
@@ -148,7 +149,8 @@ def _kill_task(ui: Any, task_id: str) -> bool:
148
149
  console.print(f"[red]Failed to read task '{escape(task_id)}': {escape(str(exc))}[/red]")
149
150
  logger.warning(
150
151
  "[tasks_cmd] Failed to read task before kill: %s: %s",
151
- type(exc).__name__, exc,
152
+ type(exc).__name__,
153
+ exc,
152
154
  extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
153
155
  )
154
156
  return True
@@ -172,7 +174,8 @@ def _kill_task(ui: Any, task_id: str) -> bool:
172
174
  console.print(f"[red]Error stopping task {escape(task_id)}: {escape(str(exc))}[/red]")
173
175
  logger.warning(
174
176
  "[tasks_cmd] Error stopping background task: %s: %s",
175
- type(exc).__name__, exc,
177
+ type(exc).__name__,
178
+ exc,
176
179
  extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
177
180
  )
178
181
  return True
@@ -197,7 +200,8 @@ def _show_task(ui: Any, task_id: str) -> bool:
197
200
  console.print(f"[red]Failed to read task '{escape(task_id)}': {escape(str(exc))}[/red]")
198
201
  logger.warning(
199
202
  "[tasks_cmd] Failed to read task for detail view: %s: %s",
200
- type(exc).__name__, exc,
203
+ type(exc).__name__,
204
+ exc,
201
205
  extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
202
206
  )
203
207
  return True
@@ -85,7 +85,7 @@ class FileMentionCompleter(Completer):
85
85
  return
86
86
 
87
87
  # Extract the query after the @ symbol
88
- query = text[at_pos + 1:].strip()
88
+ query = text[at_pos + 1 :].strip()
89
89
 
90
90
  try:
91
91
  matches = []
@@ -143,6 +143,7 @@ class FileMentionCompleter(Completer):
143
143
  continue
144
144
 
145
145
  import fnmatch
146
+
146
147
  if fnmatch.fnmatch(item.name.lower(), pattern.lower()):
147
148
  try:
148
149
  rel_path = item.relative_to(self.project_path)
@@ -14,7 +14,7 @@ from ripperdoc.utils.log import get_logger
14
14
  logger = get_logger()
15
15
 
16
16
  # Keys that trigger interrupt
17
- INTERRUPT_KEYS: Set[str] = {'\x1b', '\x03'} # ESC, Ctrl+C
17
+ INTERRUPT_KEYS: Set[str] = {"\x1b", "\x03"} # ESC, Ctrl+C
18
18
 
19
19
 
20
20
  class InterruptHandler:
@@ -149,8 +149,7 @@ class InterruptHandler:
149
149
 
150
150
  try:
151
151
  done, _ = await asyncio.wait(
152
- {query_task, interrupt_task},
153
- return_when=asyncio.FIRST_COMPLETED
152
+ {query_task, interrupt_task}, return_when=asyncio.FIRST_COMPLETED
154
153
  )
155
154
 
156
155
  # Check if interrupted
@@ -22,15 +22,17 @@ ConversationMessage = Union[UserMessage, AssistantMessage, ProgressMessage]
22
22
  class MessageDisplay:
23
23
  """Handles message rendering and display operations."""
24
24
 
25
- def __init__(self, console: Console, verbose: bool = False):
25
+ def __init__(self, console: Console, verbose: bool = False, show_full_thinking: bool = False):
26
26
  """Initialize the message display handler.
27
27
 
28
28
  Args:
29
29
  console: Rich console for output
30
30
  verbose: Whether to show verbose output
31
+ show_full_thinking: Whether to show full reasoning content instead of truncated preview
31
32
  """
32
33
  self.console = console
33
34
  self.verbose = verbose
35
+ self.show_full_thinking = show_full_thinking
34
36
 
35
37
  def format_tool_args(self, tool_name: str, tool_args: Optional[dict]) -> List[str]:
36
38
  """Render tool arguments into concise display-friendly parts."""
@@ -212,7 +214,7 @@ class MessageDisplay:
212
214
 
213
215
  def print_reasoning(self, reasoning: Any) -> None:
214
216
  """Display a collapsed preview of reasoning/thinking blocks."""
215
- preview = format_reasoning_preview(reasoning)
217
+ preview = format_reasoning_preview(reasoning, self.show_full_thinking)
216
218
  if preview:
217
219
  self.console.print(f"[dim italic]Thinking: {escape(preview)}[/]")
218
220
 
@@ -0,0 +1,247 @@
1
+ """
2
+ Provider metadata used by interactive UI flows.
3
+
4
+ Each provider option represents exactly one protocol endpoint. Vendors that
5
+ offer multiple protocols (for example, DeepSeek exposing both OpenAI-style and
6
+ Anthropic-style APIs on different URLs) should be modeled as distinct provider
7
+ options so that protocol-specific defaults stay unambiguous.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Dict, List, Optional, Sequence, Tuple
12
+
13
+ from ripperdoc.core.config import ProviderType
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ProviderOption:
18
+ """A single provider choice with protocol and UX defaults."""
19
+
20
+ key: str
21
+ protocol: ProviderType
22
+ default_model: str
23
+ model_suggestions: Tuple[str, ...] = ()
24
+ default_api_base: Optional[str] = None
25
+
26
+ @property
27
+ def label(self) -> str:
28
+ """Display label shown to the user."""
29
+ return self.key
30
+
31
+ def with_api_base(self, api_base: Optional[str]) -> "ProviderOption":
32
+ """Return a copy that overrides the default API base."""
33
+ return ProviderOption(
34
+ key=self.key,
35
+ protocol=self.protocol,
36
+ default_model=self.default_model,
37
+ model_suggestions=self.model_suggestions,
38
+ default_api_base=api_base if api_base else self.default_api_base,
39
+ )
40
+
41
+
42
+ class ProviderRegistry:
43
+ """Registry for known providers to keep wizard logic declarative."""
44
+
45
+ def __init__(self, providers: Sequence[ProviderOption], default_key: str) -> None:
46
+ if not providers:
47
+ raise ValueError("Provider registry cannot be empty")
48
+ self._providers: List[ProviderOption] = list(providers)
49
+ self._index: Dict[str, ProviderOption] = {p.key: p for p in providers}
50
+ if default_key not in self._index:
51
+ raise ValueError(f"Default provider '{default_key}' not found in registry")
52
+ self._default_key = default_key
53
+
54
+ @property
55
+ def providers(self) -> List[ProviderOption]:
56
+ """Return providers in display order."""
57
+ return list(self._providers)
58
+
59
+ @property
60
+ def default_choice(self) -> ProviderOption:
61
+ """Return the default provider selection."""
62
+ return self._index[self._default_key]
63
+
64
+ def get(self, key: str) -> Optional[ProviderOption]:
65
+ """Look up a provider option by key."""
66
+ return self._index.get(key)
67
+
68
+ def keys(self) -> List[str]:
69
+ """Return provider keys in display order."""
70
+ return [p.key for p in self._providers]
71
+
72
+
73
+ def default_model_for_protocol(protocol: ProviderType) -> str:
74
+ """Reasonable default model per protocol family."""
75
+ if protocol == ProviderType.ANTHROPIC:
76
+ return "claude-3-5-sonnet-20241022"
77
+ if protocol == ProviderType.GEMINI:
78
+ return "gemini-1.5-pro"
79
+ return "gpt-4o-mini"
80
+
81
+
82
+ KNOWN_PROVIDERS = ProviderRegistry(
83
+ providers=[
84
+ ProviderOption(
85
+ key="deepseek",
86
+ protocol=ProviderType.OPENAI_COMPATIBLE,
87
+ default_model="deepseek-chat",
88
+ model_suggestions=("deepseek-chat", "deepseek-reasoner"),
89
+ default_api_base="https://api.deepseek.com/v1",
90
+ ),
91
+ ProviderOption(
92
+ key="openai",
93
+ protocol=ProviderType.OPENAI_COMPATIBLE,
94
+ default_model="gpt-4o-mini",
95
+ model_suggestions=(
96
+ "gpt-5.1",
97
+ "gpt-5.1-chat",
98
+ "gpt-5.1-codex",
99
+ "gpt-4o",
100
+ "gpt-4-turbo",
101
+ "o1-preview",
102
+ "o1-mini",
103
+ ),
104
+ default_api_base="https://api.openai.com/v1",
105
+ ),
106
+ ProviderOption(
107
+ key="openrouter",
108
+ protocol=ProviderType.OPENAI_COMPATIBLE,
109
+ default_model="openai/gpt-4o-mini",
110
+ model_suggestions=(
111
+ "openai/gpt-4o-mini",
112
+ "meta-llama/llama-3.1-8b-instruct",
113
+ "google/gemini-flash-1.5",
114
+ ),
115
+ default_api_base="https://openrouter.ai/api/v1",
116
+ ),
117
+ ProviderOption(
118
+ key="anthropic",
119
+ protocol=ProviderType.ANTHROPIC,
120
+ default_model="claude-3-5-sonnet-20241022",
121
+ model_suggestions=(
122
+ "claude-3-5-sonnet-20241022",
123
+ "claude-3-5-haiku-20241022",
124
+ "claude-3-opus-20240229",
125
+ "claude-3-sonnet-20240229",
126
+ "claude-3-haiku-20240307",
127
+ ),
128
+ default_api_base=None,
129
+ ),
130
+ ProviderOption(
131
+ key="openai_compatible",
132
+ protocol=ProviderType.OPENAI_COMPATIBLE,
133
+ default_model="gpt-4o-mini",
134
+ model_suggestions=(
135
+ "gpt-4o-mini",
136
+ "gpt-4o",
137
+ "gpt-3.5-turbo",
138
+ ),
139
+ default_api_base=None,
140
+ ),
141
+ ProviderOption(
142
+ key="mistralai",
143
+ protocol=ProviderType.OPENAI_COMPATIBLE,
144
+ default_model="mistral-small-creative",
145
+ model_suggestions=(
146
+ "mistral-small-creative",
147
+ "mistral-large-latest",
148
+ "mistral-small-latest",
149
+ "devstral-2512",
150
+ "ministral-14b-2512",
151
+ "ministral-8b-2512",
152
+ "codestral-latest",
153
+ "pixtral-large-latest",
154
+ ),
155
+ default_api_base="https://api.mistral.ai/v1",
156
+ ),
157
+ ProviderOption(
158
+ key="google",
159
+ protocol=ProviderType.GEMINI,
160
+ default_model="gemini-1.5-pro",
161
+ model_suggestions=(
162
+ "gemini-2.5-pro",
163
+ "gemini-2.5-flash-lite",
164
+ "gemini-2.5-flash",
165
+ "gemini-3-pro-preview",
166
+ "gemini-3-flash-preview",
167
+ ),
168
+ default_api_base="https://generativelanguage.googleapis.com/v1beta",
169
+ ),
170
+ ProviderOption(
171
+ key="moonshot",
172
+ protocol=ProviderType.OPENAI_COMPATIBLE,
173
+ default_model="kimi-k2-turbo-preview",
174
+ model_suggestions=(
175
+ "kimi-k2-0905-preview",
176
+ "kimi-k2-0711-preview",
177
+ "kimi-k2-turbo-preview",
178
+ "kimi-k2-thinking",
179
+ "kimi-k2-thinking-turbo",
180
+ ),
181
+ default_api_base="https://api.moonshot.cn/v1",
182
+ ),
183
+ ProviderOption(
184
+ key="qwen",
185
+ protocol=ProviderType.OPENAI_COMPATIBLE,
186
+ default_model="qwen-turbo",
187
+ model_suggestions=(
188
+ "qwen-turbo",
189
+ "qwen-plus",
190
+ "qwen-max",
191
+ "qwen2.5-32b",
192
+ "qwen2.5-coder-32b",
193
+ ),
194
+ default_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
195
+ ),
196
+ ProviderOption(
197
+ key="zhipu",
198
+ protocol=ProviderType.OPENAI_COMPATIBLE,
199
+ default_model="glm-4-flash",
200
+ model_suggestions=(
201
+ "glm-4-plus",
202
+ "glm-4-air-250414",
203
+ "glm-4-airx",
204
+ "glm-4-long",
205
+ "glm-4-flashx",
206
+ "glm-4-flash-250414",
207
+ "glm-4.6",
208
+ "glm-4.5",
209
+ "glm-4.5-air",
210
+ "glm-4.5-airx",
211
+ "glm-4.5-x",
212
+ "glm-4.5-flash",
213
+ ),
214
+ default_api_base="https://open.bigmodel.cn/api/paas/v4",
215
+ ),
216
+ ProviderOption(
217
+ key="minimax",
218
+ protocol=ProviderType.OPENAI_COMPATIBLE,
219
+ default_model="MiniMax-M2",
220
+ model_suggestions=("MiniMax-M2",),
221
+ default_api_base="https://api.minimax.chat/v1",
222
+ ),
223
+ ProviderOption(
224
+ key="siliconflow",
225
+ protocol=ProviderType.OPENAI_COMPATIBLE,
226
+ default_model="deepseek-ai/DeepSeek-V3.2",
227
+ model_suggestions=(
228
+ "deepseek-ai/DeepSeek-V3.2",
229
+ "Qwen/Qwen2.5-32B-Instruct",
230
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct",
231
+ "zai-org/GLM-4.6",
232
+ "moonshotai/Kimi-K2-Thinking",
233
+ "MiniMaxAI/MiniMax-M2",
234
+ ),
235
+ default_api_base="https://api.siliconflow.cn/v1",
236
+ ),
237
+ ],
238
+ default_key="deepseek",
239
+ )
240
+
241
+
242
+ __all__ = [
243
+ "KNOWN_PROVIDERS",
244
+ "ProviderOption",
245
+ "ProviderRegistry",
246
+ "default_model_for_protocol",
247
+ ]