ripperdoc 0.3.1__py3-none-any.whl → 0.3.2__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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +9 -1
- ripperdoc/cli/commands/agents_cmd.py +93 -53
- ripperdoc/cli/commands/mcp_cmd.py +3 -0
- ripperdoc/cli/commands/models_cmd.py +768 -283
- ripperdoc/cli/commands/permissions_cmd.py +107 -52
- ripperdoc/cli/commands/resume_cmd.py +61 -51
- ripperdoc/cli/commands/themes_cmd.py +31 -1
- ripperdoc/cli/ui/agents_tui/__init__.py +3 -0
- ripperdoc/cli/ui/agents_tui/textual_app.py +1138 -0
- ripperdoc/cli/ui/choice.py +376 -0
- ripperdoc/cli/ui/models_tui/__init__.py +5 -0
- ripperdoc/cli/ui/models_tui/textual_app.py +698 -0
- ripperdoc/cli/ui/panels.py +19 -4
- ripperdoc/cli/ui/permissions_tui/__init__.py +3 -0
- ripperdoc/cli/ui/permissions_tui/textual_app.py +526 -0
- ripperdoc/cli/ui/provider_options.py +220 -80
- ripperdoc/cli/ui/rich_ui.py +9 -11
- ripperdoc/cli/ui/tips.py +89 -0
- ripperdoc/cli/ui/wizard.py +98 -45
- ripperdoc/core/config.py +3 -0
- ripperdoc/core/permissions.py +25 -70
- ripperdoc/core/providers/anthropic.py +11 -0
- ripperdoc/protocol/stdio.py +3 -1
- ripperdoc/tools/bash_tool.py +2 -0
- ripperdoc/tools/file_edit_tool.py +100 -181
- ripperdoc/tools/file_read_tool.py +101 -25
- ripperdoc/tools/multi_edit_tool.py +239 -91
- ripperdoc/tools/notebook_edit_tool.py +11 -29
- ripperdoc/utils/file_editing.py +164 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +11 -0
- {ripperdoc-0.3.1.dist-info → ripperdoc-0.3.2.dist-info}/METADATA +3 -2
- {ripperdoc-0.3.1.dist-info → ripperdoc-0.3.2.dist-info}/RECORD +37 -28
- {ripperdoc-0.3.1.dist-info → ripperdoc-0.3.2.dist-info}/WHEEL +0 -0
- {ripperdoc-0.3.1.dist-info → ripperdoc-0.3.2.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.3.1.dist-info → ripperdoc-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.3.1.dist-info → ripperdoc-0.3.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import textwrap
|
|
1
3
|
from typing import Any, Optional
|
|
2
4
|
|
|
5
|
+
from rich import box
|
|
6
|
+
from rich.layout import Layout
|
|
3
7
|
from rich.markup import escape
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
4
11
|
|
|
5
12
|
from ripperdoc.cli.ui.helpers import get_profile_for_pointer
|
|
6
13
|
from ripperdoc.core.config import (
|
|
@@ -9,6 +16,7 @@ from ripperdoc.core.config import (
|
|
|
9
16
|
add_model_profile,
|
|
10
17
|
delete_model_profile,
|
|
11
18
|
get_global_config,
|
|
19
|
+
model_supports_vision,
|
|
12
20
|
set_model_pointer,
|
|
13
21
|
)
|
|
14
22
|
from ripperdoc.utils.log import get_logger
|
|
@@ -19,6 +27,749 @@ from .base import SlashCommand
|
|
|
19
27
|
logger = get_logger()
|
|
20
28
|
|
|
21
29
|
|
|
30
|
+
def _parse_int(console: Any, prompt_text: str, default_value: Optional[int]) -> Optional[int]:
|
|
31
|
+
raw = console.input(prompt_text).strip()
|
|
32
|
+
if not raw:
|
|
33
|
+
return default_value
|
|
34
|
+
try:
|
|
35
|
+
return int(raw)
|
|
36
|
+
except ValueError:
|
|
37
|
+
console.print("[yellow]Invalid number, keeping previous value.[/yellow]")
|
|
38
|
+
return default_value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_float(console: Any, prompt_text: str, default_value: float) -> float:
|
|
42
|
+
raw = console.input(prompt_text).strip()
|
|
43
|
+
if not raw:
|
|
44
|
+
return default_value
|
|
45
|
+
try:
|
|
46
|
+
return float(raw)
|
|
47
|
+
except ValueError:
|
|
48
|
+
console.print("[yellow]Invalid number, keeping previous value.[/yellow]")
|
|
49
|
+
return default_value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _prompt_provider(console: Any, default_provider: str) -> Optional[ProviderType]:
|
|
53
|
+
provider_input = (
|
|
54
|
+
console.input(
|
|
55
|
+
f"Protocol ({', '.join(p.value for p in ProviderType)}) [{default_provider}]: "
|
|
56
|
+
)
|
|
57
|
+
.strip()
|
|
58
|
+
.lower()
|
|
59
|
+
or default_provider
|
|
60
|
+
)
|
|
61
|
+
try:
|
|
62
|
+
return ProviderType(provider_input)
|
|
63
|
+
except ValueError:
|
|
64
|
+
console.print(f"[red]Invalid provider: {escape(provider_input)}[/red]")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _prompt_supports_vision_add(console: Any, default_value: Optional[bool]) -> Optional[bool]:
|
|
69
|
+
vision_default_display = (
|
|
70
|
+
"auto" if default_value is None else ("yes" if default_value else "no")
|
|
71
|
+
)
|
|
72
|
+
supports_vision_input = (
|
|
73
|
+
console.input(f"Supports vision (images)? [{vision_default_display}] (Y/n/auto): ")
|
|
74
|
+
.strip()
|
|
75
|
+
.lower()
|
|
76
|
+
)
|
|
77
|
+
if supports_vision_input in ("y", "yes"):
|
|
78
|
+
return True
|
|
79
|
+
if supports_vision_input in ("n", "no"):
|
|
80
|
+
return False
|
|
81
|
+
if supports_vision_input in ("auto", ""):
|
|
82
|
+
return None
|
|
83
|
+
return default_value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _prompt_supports_vision_edit(console: Any, current_value: Optional[bool]) -> Optional[bool]:
|
|
87
|
+
vision_default_display = "auto" if current_value is None else ("yes" if current_value else "no")
|
|
88
|
+
supports_vision_input = (
|
|
89
|
+
console.input(
|
|
90
|
+
f"Supports vision (images)? [{vision_default_display}] (Y/n/auto/C=clear): "
|
|
91
|
+
)
|
|
92
|
+
.strip()
|
|
93
|
+
.lower()
|
|
94
|
+
)
|
|
95
|
+
if supports_vision_input in ("y", "yes"):
|
|
96
|
+
return True
|
|
97
|
+
if supports_vision_input in ("n", "no"):
|
|
98
|
+
return False
|
|
99
|
+
if supports_vision_input in ("c", "clear", "-"):
|
|
100
|
+
return None
|
|
101
|
+
if supports_vision_input in ("auto", ""):
|
|
102
|
+
return current_value
|
|
103
|
+
return current_value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _collect_add_profile_input(
|
|
107
|
+
console: Any,
|
|
108
|
+
config: Any,
|
|
109
|
+
existing_profile: Optional[ModelProfile],
|
|
110
|
+
current_profile: Optional[ModelProfile],
|
|
111
|
+
) -> tuple[Optional[ModelProfile], bool]:
|
|
112
|
+
default_provider = (
|
|
113
|
+
(current_profile.provider.value) if current_profile else ProviderType.ANTHROPIC.value
|
|
114
|
+
)
|
|
115
|
+
provider = _prompt_provider(console, default_provider)
|
|
116
|
+
if provider is None:
|
|
117
|
+
return None, False
|
|
118
|
+
|
|
119
|
+
default_model = (
|
|
120
|
+
existing_profile.model
|
|
121
|
+
if existing_profile
|
|
122
|
+
else (current_profile.model if current_profile else "")
|
|
123
|
+
)
|
|
124
|
+
model_prompt = f"Model name to send{f' [{default_model}]' if default_model else ''}: "
|
|
125
|
+
model_name = console.input(model_prompt).strip() or default_model
|
|
126
|
+
if not model_name:
|
|
127
|
+
console.print("[red]Model name is required.[/red]")
|
|
128
|
+
return None, False
|
|
129
|
+
|
|
130
|
+
api_key_input = prompt_secret("API key (leave blank to keep unset)").strip()
|
|
131
|
+
api_key = api_key_input or (existing_profile.api_key if existing_profile else None)
|
|
132
|
+
|
|
133
|
+
auth_token = existing_profile.auth_token if existing_profile else None
|
|
134
|
+
if provider == ProviderType.ANTHROPIC:
|
|
135
|
+
auth_token_input = prompt_secret(
|
|
136
|
+
"Auth token (Anthropic only, leave blank to keep unset)"
|
|
137
|
+
).strip()
|
|
138
|
+
auth_token = auth_token_input or auth_token
|
|
139
|
+
else:
|
|
140
|
+
auth_token = None
|
|
141
|
+
|
|
142
|
+
api_base_default = existing_profile.api_base if existing_profile else ""
|
|
143
|
+
api_base = (
|
|
144
|
+
console.input(
|
|
145
|
+
f"API base (optional){f' [{api_base_default}]' if api_base_default else ''}: "
|
|
146
|
+
).strip()
|
|
147
|
+
or api_base_default
|
|
148
|
+
or None
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
max_tokens_default = existing_profile.max_tokens if existing_profile else 4096
|
|
152
|
+
max_tokens = (
|
|
153
|
+
_parse_int(
|
|
154
|
+
console,
|
|
155
|
+
f"Max output tokens [{max_tokens_default}]: ",
|
|
156
|
+
max_tokens_default,
|
|
157
|
+
)
|
|
158
|
+
or max_tokens_default
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
temp_default = existing_profile.temperature if existing_profile else 0.7
|
|
162
|
+
temperature = _parse_float(
|
|
163
|
+
console,
|
|
164
|
+
f"Temperature [{temp_default}]: ",
|
|
165
|
+
temp_default,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
context_window_default = existing_profile.context_window if existing_profile else None
|
|
169
|
+
context_prompt = "Context window tokens (optional"
|
|
170
|
+
if context_window_default:
|
|
171
|
+
context_prompt += f", current {context_window_default}"
|
|
172
|
+
context_prompt += "): "
|
|
173
|
+
context_window = _parse_int(console, context_prompt, context_window_default)
|
|
174
|
+
|
|
175
|
+
supports_vision_default = existing_profile.supports_vision if existing_profile else None
|
|
176
|
+
supports_vision = _prompt_supports_vision_add(console, supports_vision_default)
|
|
177
|
+
|
|
178
|
+
default_set_main = (
|
|
179
|
+
not config.model_profiles
|
|
180
|
+
or getattr(config.model_pointers, "main", "") not in config.model_profiles
|
|
181
|
+
)
|
|
182
|
+
set_main_input = (
|
|
183
|
+
console.input(f"Set as main model? ({'Y' if default_set_main else 'y'}/N): ")
|
|
184
|
+
.strip()
|
|
185
|
+
.lower()
|
|
186
|
+
)
|
|
187
|
+
set_as_main = set_main_input in ("y", "yes") if set_main_input else default_set_main
|
|
188
|
+
|
|
189
|
+
profile = ModelProfile(
|
|
190
|
+
provider=provider,
|
|
191
|
+
model=model_name,
|
|
192
|
+
api_key=api_key,
|
|
193
|
+
api_base=api_base,
|
|
194
|
+
max_tokens=max_tokens,
|
|
195
|
+
temperature=temperature,
|
|
196
|
+
context_window=context_window,
|
|
197
|
+
auth_token=auth_token,
|
|
198
|
+
supports_vision=supports_vision,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return profile, set_as_main
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _collect_edit_profile_input(
|
|
205
|
+
console: Any,
|
|
206
|
+
existing_profile: ModelProfile,
|
|
207
|
+
) -> Optional[ModelProfile]:
|
|
208
|
+
provider_default = existing_profile.provider.value
|
|
209
|
+
provider = _prompt_provider(console, provider_default)
|
|
210
|
+
if provider is None:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
model_name = (
|
|
214
|
+
console.input(f"Model name to send [{existing_profile.model}]: ").strip()
|
|
215
|
+
or existing_profile.model
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
api_key_label = "[set]" if existing_profile.api_key else "[not set]"
|
|
219
|
+
api_key_prompt = f"API key {api_key_label} (Enter=keep, '-'=clear)"
|
|
220
|
+
api_key_input = prompt_secret(api_key_prompt).strip()
|
|
221
|
+
if api_key_input == "-":
|
|
222
|
+
api_key = None
|
|
223
|
+
elif api_key_input:
|
|
224
|
+
api_key = api_key_input
|
|
225
|
+
else:
|
|
226
|
+
api_key = existing_profile.api_key
|
|
227
|
+
|
|
228
|
+
auth_token = existing_profile.auth_token
|
|
229
|
+
if provider == ProviderType.ANTHROPIC or existing_profile.provider == ProviderType.ANTHROPIC:
|
|
230
|
+
auth_label = "[set]" if auth_token else "[not set]"
|
|
231
|
+
auth_prompt = f"Auth token (Anthropic only) {auth_label} (Enter=keep, '-'=clear)"
|
|
232
|
+
auth_token_input = prompt_secret(auth_prompt).strip()
|
|
233
|
+
if auth_token_input == "-":
|
|
234
|
+
auth_token = None
|
|
235
|
+
elif auth_token_input:
|
|
236
|
+
auth_token = auth_token_input
|
|
237
|
+
else:
|
|
238
|
+
auth_token = None
|
|
239
|
+
|
|
240
|
+
api_base = (
|
|
241
|
+
console.input(f"API base (optional) [{existing_profile.api_base or ''}]: ").strip()
|
|
242
|
+
or existing_profile.api_base
|
|
243
|
+
)
|
|
244
|
+
if api_base == "":
|
|
245
|
+
api_base = None
|
|
246
|
+
|
|
247
|
+
max_tokens = (
|
|
248
|
+
_parse_int(
|
|
249
|
+
console,
|
|
250
|
+
f"Max output tokens [{existing_profile.max_tokens}]: ",
|
|
251
|
+
existing_profile.max_tokens,
|
|
252
|
+
)
|
|
253
|
+
or existing_profile.max_tokens
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
temperature = _parse_float(
|
|
257
|
+
console,
|
|
258
|
+
f"Temperature [{existing_profile.temperature}]: ",
|
|
259
|
+
existing_profile.temperature,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
context_window = _parse_int(
|
|
263
|
+
console,
|
|
264
|
+
f"Context window tokens [{existing_profile.context_window or 'unset'}]: ",
|
|
265
|
+
existing_profile.context_window,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
supports_vision = _prompt_supports_vision_edit(console, existing_profile.supports_vision)
|
|
269
|
+
|
|
270
|
+
updated_profile = ModelProfile(
|
|
271
|
+
provider=provider,
|
|
272
|
+
model=model_name,
|
|
273
|
+
api_key=api_key,
|
|
274
|
+
api_base=api_base,
|
|
275
|
+
max_tokens=max_tokens,
|
|
276
|
+
temperature=temperature,
|
|
277
|
+
context_window=context_window,
|
|
278
|
+
auth_token=auth_token,
|
|
279
|
+
supports_vision=supports_vision,
|
|
280
|
+
)
|
|
281
|
+
return updated_profile
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _pointer_markers(pointer_map: dict[str, str], name: str) -> list[str]:
|
|
285
|
+
return [ptr for ptr, value in pointer_map.items() if value == name]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _vision_labels(profile: ModelProfile) -> tuple[str, str]:
|
|
289
|
+
if profile.supports_vision is None:
|
|
290
|
+
detected = model_supports_vision(profile)
|
|
291
|
+
return "auto", f"auto (detected {'yes' if detected else 'no'})"
|
|
292
|
+
if profile.supports_vision:
|
|
293
|
+
return "yes", "yes"
|
|
294
|
+
return "no", "no"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _render_models_plain(console: Any, config: Any) -> None:
|
|
298
|
+
pointer_map = config.model_pointers.model_dump()
|
|
299
|
+
if not config.model_profiles:
|
|
300
|
+
console.print(" • No models configured")
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
console.print("\n[bold]Configured Models:[/bold]")
|
|
304
|
+
for name, profile in config.model_profiles.items():
|
|
305
|
+
markers = [ptr for ptr, value in pointer_map.items() if value == name]
|
|
306
|
+
marker_text = f" ({', '.join(markers)})" if markers else ""
|
|
307
|
+
console.print(f" • {escape(name)}{marker_text}", markup=False)
|
|
308
|
+
console.print(f" protocol: {profile.provider.value}", markup=False)
|
|
309
|
+
console.print(f" model: {profile.model}", markup=False)
|
|
310
|
+
if profile.api_base:
|
|
311
|
+
console.print(f" api_base: {profile.api_base}", markup=False)
|
|
312
|
+
if profile.context_window:
|
|
313
|
+
console.print(f" context: {profile.context_window} tokens", markup=False)
|
|
314
|
+
console.print(
|
|
315
|
+
f" max_tokens: {profile.max_tokens}, temperature: {profile.temperature}",
|
|
316
|
+
markup=False,
|
|
317
|
+
)
|
|
318
|
+
console.print(f" api_key: {'***' if profile.api_key else 'Not set'}", markup=False)
|
|
319
|
+
if profile.provider == ProviderType.ANTHROPIC:
|
|
320
|
+
console.print(
|
|
321
|
+
f" auth_token: {'***' if getattr(profile, 'auth_token', None) else 'Not set'}",
|
|
322
|
+
markup=False,
|
|
323
|
+
)
|
|
324
|
+
if profile.openai_tool_mode:
|
|
325
|
+
console.print(f" openai_tool_mode: {profile.openai_tool_mode}", markup=False)
|
|
326
|
+
if profile.thinking_mode:
|
|
327
|
+
console.print(f" thinking_mode: {profile.thinking_mode}", markup=False)
|
|
328
|
+
if profile.supports_vision is None:
|
|
329
|
+
vision_display = "auto-detect"
|
|
330
|
+
elif profile.supports_vision:
|
|
331
|
+
vision_display = "yes"
|
|
332
|
+
else:
|
|
333
|
+
vision_display = "no"
|
|
334
|
+
console.print(f" supports_vision: {vision_display}", markup=False)
|
|
335
|
+
pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
|
|
336
|
+
console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _render_models_table(console: Any, config: Any) -> None:
|
|
340
|
+
pointer_map = config.model_pointers.model_dump()
|
|
341
|
+
table = Table(box=box.SIMPLE_HEAVY, expand=True)
|
|
342
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
343
|
+
table.add_column("Ptr", style="magenta", no_wrap=True)
|
|
344
|
+
table.add_column("Provider", style="green", no_wrap=True)
|
|
345
|
+
table.add_column("Model", style="white", overflow="fold")
|
|
346
|
+
table.add_column("Ctx", style="dim", justify="right", no_wrap=True)
|
|
347
|
+
table.add_column("Max", style="dim", justify="right", no_wrap=True)
|
|
348
|
+
table.add_column("Temp", style="dim", justify="right", no_wrap=True)
|
|
349
|
+
table.add_column("Vision", style="yellow", no_wrap=True)
|
|
350
|
+
table.add_column("Key", style="dim", no_wrap=True)
|
|
351
|
+
table.add_column("API Base", style="dim", overflow="fold", max_width=28)
|
|
352
|
+
|
|
353
|
+
for name, profile in config.model_profiles.items():
|
|
354
|
+
markers = _pointer_markers(pointer_map, name)
|
|
355
|
+
pointer_label = ",".join(markers) if markers else "-"
|
|
356
|
+
context_display = str(profile.context_window) if profile.context_window else "-"
|
|
357
|
+
vision_display = _vision_labels(profile)[0]
|
|
358
|
+
api_base = profile.api_base or "-"
|
|
359
|
+
if profile.api_base:
|
|
360
|
+
api_base = textwrap.shorten(profile.api_base, width=28, placeholder="...")
|
|
361
|
+
key_display = "set" if profile.api_key else "-"
|
|
362
|
+
table.add_row(
|
|
363
|
+
escape(name),
|
|
364
|
+
pointer_label,
|
|
365
|
+
profile.provider.value,
|
|
366
|
+
escape(profile.model),
|
|
367
|
+
context_display,
|
|
368
|
+
str(profile.max_tokens),
|
|
369
|
+
f"{profile.temperature:.2f}",
|
|
370
|
+
vision_display,
|
|
371
|
+
key_display,
|
|
372
|
+
escape(api_base),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
title = f"Models ({len(config.model_profiles)})"
|
|
376
|
+
console.print(Panel(table, title=title, box=box.ROUNDED, padding=(1, 2)))
|
|
377
|
+
pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
|
|
378
|
+
console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _build_model_details_panel(
|
|
382
|
+
name: str, profile: ModelProfile, pointer_map: dict[str, str]
|
|
383
|
+
) -> Panel:
|
|
384
|
+
markers = _pointer_markers(pointer_map, name)
|
|
385
|
+
marker_text = ", ".join(markers) if markers else "-"
|
|
386
|
+
vision_short, vision_detail = _vision_labels(profile)
|
|
387
|
+
|
|
388
|
+
details = Table.grid(padding=(0, 2))
|
|
389
|
+
details.add_column(style="cyan", no_wrap=True)
|
|
390
|
+
details.add_column(style="white")
|
|
391
|
+
details.add_row("Profile", escape(name))
|
|
392
|
+
details.add_row("Pointers", escape(marker_text))
|
|
393
|
+
details.add_row("Provider", escape(profile.provider.value))
|
|
394
|
+
details.add_row("Model", escape(profile.model))
|
|
395
|
+
details.add_row("API base", escape(profile.api_base or "-"))
|
|
396
|
+
details.add_row("Context", escape(str(profile.context_window) if profile.context_window else "auto"))
|
|
397
|
+
details.add_row("Max tokens", escape(str(profile.max_tokens)))
|
|
398
|
+
details.add_row("Temperature", escape(str(profile.temperature)))
|
|
399
|
+
details.add_row("Vision", escape(vision_detail if vision_short == "auto" else vision_short))
|
|
400
|
+
details.add_row("API key", "set" if profile.api_key else "unset")
|
|
401
|
+
if profile.provider == ProviderType.ANTHROPIC:
|
|
402
|
+
details.add_row("Auth token", "set" if getattr(profile, "auth_token", None) else "unset")
|
|
403
|
+
if profile.openai_tool_mode:
|
|
404
|
+
details.add_row("OpenAI tool mode", escape(profile.openai_tool_mode))
|
|
405
|
+
if profile.thinking_mode:
|
|
406
|
+
details.add_row("Thinking mode", escape(profile.thinking_mode))
|
|
407
|
+
|
|
408
|
+
return Panel(
|
|
409
|
+
details,
|
|
410
|
+
title=f"Model: {escape(name)}",
|
|
411
|
+
box=box.ROUNDED,
|
|
412
|
+
padding=(1, 2),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _render_model_details(
|
|
417
|
+
console: Any, name: str, profile: ModelProfile, pointer_map: dict[str, str]
|
|
418
|
+
) -> None:
|
|
419
|
+
console.print(_build_model_details_panel(name, profile, pointer_map))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _default_selected_model(config: Any, preferred: Optional[str] = None) -> Optional[str]:
|
|
423
|
+
if preferred and preferred in config.model_profiles:
|
|
424
|
+
return preferred
|
|
425
|
+
main_pointer = getattr(config.model_pointers, "main", "")
|
|
426
|
+
if main_pointer in config.model_profiles:
|
|
427
|
+
return main_pointer
|
|
428
|
+
if config.model_profiles:
|
|
429
|
+
return next(iter(config.model_profiles))
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _build_models_list_panel(
|
|
434
|
+
config: Any, selected_name: Optional[str], pointer_map: dict[str, str]
|
|
435
|
+
) -> Panel:
|
|
436
|
+
table = Table(show_header=False, box=None, padding=(0, 1), expand=True)
|
|
437
|
+
table.add_column("#", width=3, no_wrap=True, style="dim")
|
|
438
|
+
table.add_column("Sel", width=2, no_wrap=True)
|
|
439
|
+
table.add_column("Name", no_wrap=True)
|
|
440
|
+
table.add_column("Ptr", style="magenta", no_wrap=True)
|
|
441
|
+
table.add_column("Model", style="dim")
|
|
442
|
+
|
|
443
|
+
for idx, (name, profile) in enumerate(config.model_profiles.items(), start=1):
|
|
444
|
+
selected = name == selected_name
|
|
445
|
+
marker_text = Text(">", style="bold yellow") if selected else Text(" ", style="dim")
|
|
446
|
+
index_text = Text(str(idx), style="dim")
|
|
447
|
+
name_text = Text(name, style="bold cyan" if selected else "cyan")
|
|
448
|
+
markers = _pointer_markers(pointer_map, name)
|
|
449
|
+
pointer_label = ",".join(markers) if markers else "-"
|
|
450
|
+
pointer_text = Text(pointer_label, style="magenta" if markers else "dim")
|
|
451
|
+
model_label = f"{profile.provider.value} • {profile.model}"
|
|
452
|
+
model_text = Text(model_label, style="dim")
|
|
453
|
+
table.add_row(index_text, marker_text, name_text, pointer_text, model_text)
|
|
454
|
+
|
|
455
|
+
if not config.model_profiles:
|
|
456
|
+
table.add_row(
|
|
457
|
+
Text(" ", style="dim"),
|
|
458
|
+
Text(" ", style="dim"),
|
|
459
|
+
Text("No models configured", style="dim"),
|
|
460
|
+
"",
|
|
461
|
+
"",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
return Panel(table, title="Models", box=box.ROUNDED, padding=(1, 2))
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _build_models_header_panel(config: Any, selected_name: Optional[str]) -> Panel:
|
|
468
|
+
pointer_map = config.model_pointers.model_dump()
|
|
469
|
+
pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
|
|
470
|
+
selected_label = selected_name or "-"
|
|
471
|
+
|
|
472
|
+
header = Text()
|
|
473
|
+
header.append("Models ", style="bold")
|
|
474
|
+
header.append(str(len(config.model_profiles)), style="cyan")
|
|
475
|
+
header.append(" Selected: ", style="dim")
|
|
476
|
+
header.append(selected_label, style="bold cyan")
|
|
477
|
+
header.append(" Pointers: ", style="dim")
|
|
478
|
+
header.append(pointer_labels, style="magenta")
|
|
479
|
+
|
|
480
|
+
return Panel(header, box=box.ROUNDED, padding=(0, 1))
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _build_models_footer_panel() -> Panel:
|
|
484
|
+
footer = Text(
|
|
485
|
+
"↑/↓ move A add E edit D delete M set main K set quick Q exit R refresh",
|
|
486
|
+
style="dim",
|
|
487
|
+
)
|
|
488
|
+
return Panel(footer, box=box.ROUNDED, padding=(0, 1))
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _render_models_dashboard(console: Any, config: Any, selected_name: Optional[str]) -> None:
|
|
492
|
+
pointer_map = config.model_pointers.model_dump()
|
|
493
|
+
layout = Layout()
|
|
494
|
+
layout.split_column(
|
|
495
|
+
Layout(_build_models_header_panel(config, selected_name), name="header", size=3),
|
|
496
|
+
Layout(name="body", ratio=1),
|
|
497
|
+
Layout(_build_models_footer_panel(), name="footer", size=3),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
list_panel = _build_models_list_panel(config, selected_name, pointer_map)
|
|
501
|
+
if selected_name and selected_name in config.model_profiles:
|
|
502
|
+
details_panel = _build_model_details_panel(
|
|
503
|
+
selected_name, config.model_profiles[selected_name], pointer_map
|
|
504
|
+
)
|
|
505
|
+
else:
|
|
506
|
+
details_panel = Panel("Select a model to view details.", box=box.ROUNDED, padding=(1, 2))
|
|
507
|
+
|
|
508
|
+
layout["body"].split_row(
|
|
509
|
+
Layout(list_panel, name="left", ratio=2),
|
|
510
|
+
Layout(details_panel, name="right", ratio=3),
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
console.print(layout)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _confirm_action(console: Any, prompt_text: str, default: bool = False) -> bool:
|
|
517
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
518
|
+
raw = console.input(f"{prompt_text} {suffix}: ").strip().lower()
|
|
519
|
+
if not raw:
|
|
520
|
+
return default
|
|
521
|
+
return raw in ("y", "yes")
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _prompt_models_command(console: Any) -> str:
|
|
525
|
+
try:
|
|
526
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
527
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
528
|
+
except (ImportError, OSError, RuntimeError):
|
|
529
|
+
return console.input("Command (a/e/d/m/k/q/r or model #/name): ").strip()
|
|
530
|
+
|
|
531
|
+
key_bindings = KeyBindings()
|
|
532
|
+
|
|
533
|
+
def _exit_with(value: str) -> None:
|
|
534
|
+
def _handler(event: Any) -> None: # noqa: ANN001
|
|
535
|
+
event.app.exit(result=value)
|
|
536
|
+
|
|
537
|
+
return _handler # type: ignore[return-value]
|
|
538
|
+
|
|
539
|
+
for key in ("a", "A"):
|
|
540
|
+
key_bindings.add(key, eager=True)(_exit_with("a"))
|
|
541
|
+
for key in ("e", "E"):
|
|
542
|
+
key_bindings.add(key, eager=True)(_exit_with("e"))
|
|
543
|
+
for key in ("d", "D"):
|
|
544
|
+
key_bindings.add(key, eager=True)(_exit_with("d"))
|
|
545
|
+
for key in ("m", "M"):
|
|
546
|
+
key_bindings.add(key, eager=True)(_exit_with("m"))
|
|
547
|
+
for key in ("k", "K"):
|
|
548
|
+
key_bindings.add(key, eager=True)(_exit_with("k"))
|
|
549
|
+
for key in ("q", "Q", "escape"):
|
|
550
|
+
key_bindings.add(key, eager=True)(_exit_with("q"))
|
|
551
|
+
for key in ("r", "R"):
|
|
552
|
+
key_bindings.add(key, eager=True)(_exit_with("r"))
|
|
553
|
+
|
|
554
|
+
key_bindings.add("up", eager=True)(_exit_with("__up"))
|
|
555
|
+
key_bindings.add("down", eager=True)(_exit_with("__down"))
|
|
556
|
+
key_bindings.add("pageup", eager=True)(_exit_with("__page_up"))
|
|
557
|
+
key_bindings.add("pagedown", eager=True)(_exit_with("__page_down"))
|
|
558
|
+
|
|
559
|
+
@key_bindings.add("enter")
|
|
560
|
+
def _enter(event: Any) -> None: # noqa: ANN001
|
|
561
|
+
text = event.current_buffer.text
|
|
562
|
+
event.app.exit(result=text)
|
|
563
|
+
|
|
564
|
+
@key_bindings.add("c-c", eager=True)
|
|
565
|
+
def _ctrl_c(event: Any) -> None: # noqa: ANN001
|
|
566
|
+
event.app.exit(result="q")
|
|
567
|
+
|
|
568
|
+
return pt_prompt("Command: ", key_bindings=key_bindings)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _resolve_model_selection(
|
|
572
|
+
raw: str, config: Any, selected_name: Optional[str]
|
|
573
|
+
) -> Optional[str]:
|
|
574
|
+
if not raw:
|
|
575
|
+
return selected_name
|
|
576
|
+
raw = raw.strip()
|
|
577
|
+
if raw.isdigit():
|
|
578
|
+
idx = int(raw)
|
|
579
|
+
if idx <= 0:
|
|
580
|
+
return selected_name
|
|
581
|
+
names = list(config.model_profiles.keys())
|
|
582
|
+
if 1 <= idx <= len(names):
|
|
583
|
+
return names[idx - 1]
|
|
584
|
+
return selected_name
|
|
585
|
+
if raw in config.model_profiles:
|
|
586
|
+
return raw
|
|
587
|
+
# Case-insensitive match
|
|
588
|
+
lower_map = {name.lower(): name for name in config.model_profiles}
|
|
589
|
+
return lower_map.get(raw.lower(), selected_name)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _handle_models_rich_tui(ui: Any) -> bool:
|
|
593
|
+
console = ui.console
|
|
594
|
+
if not sys.stdin.isatty():
|
|
595
|
+
console.print("[yellow]Interactive UI requires a TTY. Showing plain list instead.[/yellow]")
|
|
596
|
+
_render_models_plain(console, get_global_config())
|
|
597
|
+
return True
|
|
598
|
+
|
|
599
|
+
selected_name: Optional[str] = None
|
|
600
|
+
|
|
601
|
+
while True:
|
|
602
|
+
config = get_global_config()
|
|
603
|
+
selected_name = _default_selected_model(config, selected_name)
|
|
604
|
+
console.print()
|
|
605
|
+
_render_models_dashboard(console, config, selected_name)
|
|
606
|
+
|
|
607
|
+
if not config.model_profiles:
|
|
608
|
+
if _confirm_action(console, "No models configured. Add one now?", default=True):
|
|
609
|
+
profile_name = console.input("Profile name: ").strip()
|
|
610
|
+
if not profile_name:
|
|
611
|
+
console.print("[red]Model profile name is required.[/red]")
|
|
612
|
+
continue
|
|
613
|
+
existing_profile = config.model_profiles.get(profile_name)
|
|
614
|
+
if existing_profile:
|
|
615
|
+
if not _confirm_action(
|
|
616
|
+
console, f"Profile '{profile_name}' exists. Overwrite?"
|
|
617
|
+
):
|
|
618
|
+
continue
|
|
619
|
+
profile, set_as_main = _collect_add_profile_input(
|
|
620
|
+
console, config, existing_profile, get_profile_for_pointer("main")
|
|
621
|
+
)
|
|
622
|
+
if not profile:
|
|
623
|
+
continue
|
|
624
|
+
try:
|
|
625
|
+
add_model_profile(
|
|
626
|
+
profile_name,
|
|
627
|
+
profile,
|
|
628
|
+
overwrite=bool(existing_profile),
|
|
629
|
+
set_as_main=set_as_main,
|
|
630
|
+
)
|
|
631
|
+
except (OSError, IOError, ValueError, TypeError, PermissionError) as exc:
|
|
632
|
+
console.print(f"[red]Failed to save model: {escape(str(exc))}[/red]")
|
|
633
|
+
continue
|
|
634
|
+
marker = " (main)" if set_as_main else ""
|
|
635
|
+
console.print(f"[green]✓ Model '{escape(profile_name)}' saved{marker}[/green]")
|
|
636
|
+
continue
|
|
637
|
+
return True
|
|
638
|
+
|
|
639
|
+
command = _prompt_models_command(console).strip().lower()
|
|
640
|
+
if command in ("__up", "__down", "__page_up", "__page_down"):
|
|
641
|
+
names = list(config.model_profiles.keys())
|
|
642
|
+
if not names:
|
|
643
|
+
continue
|
|
644
|
+
current_index = names.index(selected_name) if selected_name in names else 0
|
|
645
|
+
if command == "__up":
|
|
646
|
+
current_index = max(0, current_index - 1)
|
|
647
|
+
elif command == "__down":
|
|
648
|
+
current_index = min(len(names) - 1, current_index + 1)
|
|
649
|
+
elif command == "__page_up":
|
|
650
|
+
current_index = max(0, current_index - 5)
|
|
651
|
+
elif command == "__page_down":
|
|
652
|
+
current_index = min(len(names) - 1, current_index + 5)
|
|
653
|
+
selected_name = names[current_index]
|
|
654
|
+
continue
|
|
655
|
+
if command in ("", "r", "refresh"):
|
|
656
|
+
continue
|
|
657
|
+
if command in ("q", "quit", "exit"):
|
|
658
|
+
return True
|
|
659
|
+
if command in ("a", "add"):
|
|
660
|
+
profile_name = console.input("Profile name: ").strip()
|
|
661
|
+
if not profile_name:
|
|
662
|
+
console.print("[red]Model profile name is required.[/red]")
|
|
663
|
+
continue
|
|
664
|
+
existing_profile = config.model_profiles.get(profile_name)
|
|
665
|
+
if existing_profile:
|
|
666
|
+
if not _confirm_action(console, f"Profile '{profile_name}' exists. Overwrite?"):
|
|
667
|
+
continue
|
|
668
|
+
profile, set_as_main = _collect_add_profile_input(
|
|
669
|
+
console, config, existing_profile, get_profile_for_pointer("main")
|
|
670
|
+
)
|
|
671
|
+
if not profile:
|
|
672
|
+
continue
|
|
673
|
+
try:
|
|
674
|
+
add_model_profile(
|
|
675
|
+
profile_name,
|
|
676
|
+
profile,
|
|
677
|
+
overwrite=bool(existing_profile),
|
|
678
|
+
set_as_main=set_as_main,
|
|
679
|
+
)
|
|
680
|
+
except (OSError, IOError, ValueError, TypeError, PermissionError) as exc:
|
|
681
|
+
console.print(f"[red]Failed to save model: {escape(str(exc))}[/red]")
|
|
682
|
+
continue
|
|
683
|
+
marker = " (main)" if set_as_main else ""
|
|
684
|
+
console.print(f"[green]✓ Model '{escape(profile_name)}' saved{marker}[/green]")
|
|
685
|
+
selected_name = profile_name
|
|
686
|
+
continue
|
|
687
|
+
|
|
688
|
+
if command in ("e", "edit", "d", "delete", "del", "remove", "m", "main", "k", "quick"):
|
|
689
|
+
if not selected_name:
|
|
690
|
+
console.print("[yellow]No model selected.[/yellow]")
|
|
691
|
+
continue
|
|
692
|
+
model_name = selected_name
|
|
693
|
+
else:
|
|
694
|
+
model_name = _resolve_model_selection(command, config, selected_name)
|
|
695
|
+
if not model_name:
|
|
696
|
+
console.print("[yellow]Unknown model or command.[/yellow]")
|
|
697
|
+
continue
|
|
698
|
+
if model_name != selected_name:
|
|
699
|
+
selected_name = model_name
|
|
700
|
+
continue
|
|
701
|
+
|
|
702
|
+
profile = config.model_profiles.get(model_name or "")
|
|
703
|
+
if not profile:
|
|
704
|
+
console.print(f"[yellow]Model '{escape(model_name or '')}' not found.[/yellow]")
|
|
705
|
+
continue
|
|
706
|
+
|
|
707
|
+
if command in ("e", "edit"):
|
|
708
|
+
updated_profile = _collect_edit_profile_input(console, profile)
|
|
709
|
+
if not updated_profile:
|
|
710
|
+
continue
|
|
711
|
+
try:
|
|
712
|
+
add_model_profile(
|
|
713
|
+
model_name,
|
|
714
|
+
updated_profile,
|
|
715
|
+
overwrite=True,
|
|
716
|
+
set_as_main=False,
|
|
717
|
+
)
|
|
718
|
+
except (OSError, IOError, ValueError, TypeError, PermissionError) as exc:
|
|
719
|
+
console.print(f"[red]Failed to update model: {escape(str(exc))}[/red]")
|
|
720
|
+
continue
|
|
721
|
+
console.print(f"[green]✓ Model '{escape(model_name)}' updated[/green]")
|
|
722
|
+
continue
|
|
723
|
+
|
|
724
|
+
if command in ("m", "main", "k", "quick"):
|
|
725
|
+
pointer = "main" if command in ("m", "main") else "quick"
|
|
726
|
+
try:
|
|
727
|
+
set_model_pointer(pointer, model_name)
|
|
728
|
+
console.print(
|
|
729
|
+
f"[green]✓ Pointer '{escape(pointer)}' set to '{escape(model_name)}'[/green]"
|
|
730
|
+
)
|
|
731
|
+
except (ValueError, KeyError, OSError, IOError, PermissionError) as exc:
|
|
732
|
+
console.print(f"[red]{escape(str(exc))}[/red]")
|
|
733
|
+
continue
|
|
734
|
+
|
|
735
|
+
if command in ("d", "delete", "del", "remove"):
|
|
736
|
+
if not _confirm_action(console, f"Delete model '{model_name}'?"):
|
|
737
|
+
continue
|
|
738
|
+
try:
|
|
739
|
+
delete_model_profile(model_name)
|
|
740
|
+
console.print(f"[green]✓ Deleted model '{escape(model_name)}'[/green]")
|
|
741
|
+
selected_name = None
|
|
742
|
+
except KeyError as exc:
|
|
743
|
+
console.print(f"[yellow]{escape(str(exc))}[/yellow]")
|
|
744
|
+
except (OSError, IOError, PermissionError) as exc:
|
|
745
|
+
console.print(f"[red]Failed to delete model: {escape(str(exc))}[/red]")
|
|
746
|
+
continue
|
|
747
|
+
|
|
748
|
+
return True
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _handle_models_tui(ui: Any) -> bool:
|
|
752
|
+
console = ui.console
|
|
753
|
+
if not sys.stdin.isatty():
|
|
754
|
+
console.print("[yellow]Interactive UI requires a TTY. Showing plain list instead.[/yellow]")
|
|
755
|
+
_render_models_plain(console, get_global_config())
|
|
756
|
+
return True
|
|
757
|
+
|
|
758
|
+
try:
|
|
759
|
+
from ripperdoc.cli.ui.models_tui import run_models_tui
|
|
760
|
+
except (ImportError, ModuleNotFoundError) as exc:
|
|
761
|
+
console.print(
|
|
762
|
+
f"[yellow]Textual UI not available ({escape(str(exc))}). Falling back to Rich UI.[/yellow]"
|
|
763
|
+
)
|
|
764
|
+
return _handle_models_rich_tui(ui)
|
|
765
|
+
|
|
766
|
+
try:
|
|
767
|
+
return bool(run_models_tui())
|
|
768
|
+
except Exception as exc: # noqa: BLE001 - fail safe in interactive UI
|
|
769
|
+
console.print(f"[red]Textual UI failed: {escape(str(exc))}[/red]")
|
|
770
|
+
return _handle_models_rich_tui(ui)
|
|
771
|
+
|
|
772
|
+
|
|
22
773
|
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
23
774
|
console = ui.console
|
|
24
775
|
tokens = trimmed_arg.split()
|
|
@@ -26,11 +777,13 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
26
777
|
config = get_global_config()
|
|
27
778
|
logger.info(
|
|
28
779
|
"[models_cmd] Handling /models command",
|
|
29
|
-
extra={"subcommand": subcmd or "
|
|
780
|
+
extra={"subcommand": subcmd or "tui", "session_id": getattr(ui, "session_id", None)},
|
|
30
781
|
)
|
|
31
782
|
|
|
32
783
|
def print_models_usage() -> None:
|
|
33
|
-
console.print("[bold]/models[/bold] —
|
|
784
|
+
console.print("[bold]/models[/bold] — open interactive models UI")
|
|
785
|
+
console.print("[bold]/models tui[/bold] — open interactive models UI")
|
|
786
|
+
console.print("[bold]/models list[/bold] — list configured models (plain)")
|
|
34
787
|
console.print("[bold]/models add <name>[/bold] — add or update a model profile")
|
|
35
788
|
console.print("[bold]/models edit <name>[/bold] — edit an existing model profile")
|
|
36
789
|
console.print("[bold]/models delete <name>[/bold] — delete a model profile")
|
|
@@ -39,30 +792,17 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
39
792
|
"[bold]/models use <pointer> <name>[/bold] — set a specific pointer (main/quick)"
|
|
40
793
|
)
|
|
41
794
|
|
|
42
|
-
def parse_int(prompt_text: str, default_value: Optional[int]) -> Optional[int]:
|
|
43
|
-
raw = console.input(prompt_text).strip()
|
|
44
|
-
if not raw:
|
|
45
|
-
return default_value
|
|
46
|
-
try:
|
|
47
|
-
return int(raw)
|
|
48
|
-
except ValueError:
|
|
49
|
-
console.print("[yellow]Invalid number, keeping previous value.[/yellow]")
|
|
50
|
-
return default_value
|
|
51
|
-
|
|
52
|
-
def parse_float(prompt_text: str, default_value: float) -> float:
|
|
53
|
-
raw = console.input(prompt_text).strip()
|
|
54
|
-
if not raw:
|
|
55
|
-
return default_value
|
|
56
|
-
try:
|
|
57
|
-
return float(raw)
|
|
58
|
-
except ValueError:
|
|
59
|
-
console.print("[yellow]Invalid number, keeping previous value.[/yellow]")
|
|
60
|
-
return default_value
|
|
61
|
-
|
|
62
795
|
if subcmd in ("help", "-h", "--help"):
|
|
63
796
|
print_models_usage()
|
|
64
797
|
return True
|
|
65
798
|
|
|
799
|
+
if subcmd in ("", "tui", "ui"):
|
|
800
|
+
return _handle_models_tui(ui)
|
|
801
|
+
|
|
802
|
+
if subcmd in ("list", "ls"):
|
|
803
|
+
_render_models_plain(console, config)
|
|
804
|
+
return True
|
|
805
|
+
|
|
66
806
|
if subcmd in ("add", "create"):
|
|
67
807
|
profile_name = tokens[1] if len(tokens) > 1 else console.input("Profile name: ").strip()
|
|
68
808
|
if not profile_name:
|
|
@@ -82,124 +822,12 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
82
822
|
return True
|
|
83
823
|
overwrite = True
|
|
84
824
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
(current_profile.provider.value) if current_profile else ProviderType.ANTHROPIC.value
|
|
825
|
+
profile, set_as_main = _collect_add_profile_input(
|
|
826
|
+
console, config, existing_profile, get_profile_for_pointer("main")
|
|
88
827
|
)
|
|
89
|
-
|
|
90
|
-
console.input(
|
|
91
|
-
f"Protocol ({', '.join(p.value for p in ProviderType)}) [{default_provider}]: "
|
|
92
|
-
)
|
|
93
|
-
.strip()
|
|
94
|
-
.lower()
|
|
95
|
-
or default_provider
|
|
96
|
-
)
|
|
97
|
-
try:
|
|
98
|
-
provider = ProviderType(provider_input)
|
|
99
|
-
except ValueError:
|
|
100
|
-
console.print(f"[red]Invalid provider: {escape(provider_input)}[/red]")
|
|
101
|
-
print_models_usage()
|
|
102
|
-
return True
|
|
103
|
-
|
|
104
|
-
default_model = (
|
|
105
|
-
existing_profile.model
|
|
106
|
-
if existing_profile
|
|
107
|
-
else (current_profile.model if current_profile else "")
|
|
108
|
-
)
|
|
109
|
-
model_prompt = f"Model name to send{f' [{default_model}]' if default_model else ''}: "
|
|
110
|
-
model_name = console.input(model_prompt).strip() or default_model
|
|
111
|
-
if not model_name:
|
|
112
|
-
console.print("[red]Model name is required.[/red]")
|
|
828
|
+
if not profile:
|
|
113
829
|
return True
|
|
114
830
|
|
|
115
|
-
api_key_input = prompt_secret("API key (leave blank to keep unset)").strip()
|
|
116
|
-
api_key = api_key_input or (existing_profile.api_key if existing_profile else None)
|
|
117
|
-
|
|
118
|
-
auth_token = existing_profile.auth_token if existing_profile else None
|
|
119
|
-
if provider == ProviderType.ANTHROPIC:
|
|
120
|
-
auth_token_input = prompt_secret(
|
|
121
|
-
"Auth token (Anthropic only, leave blank to keep unset)"
|
|
122
|
-
).strip()
|
|
123
|
-
auth_token = auth_token_input or auth_token
|
|
124
|
-
else:
|
|
125
|
-
auth_token = None
|
|
126
|
-
|
|
127
|
-
api_base_default = existing_profile.api_base if existing_profile else ""
|
|
128
|
-
api_base = (
|
|
129
|
-
console.input(
|
|
130
|
-
f"API base (optional){f' [{api_base_default}]' if api_base_default else ''}: "
|
|
131
|
-
).strip()
|
|
132
|
-
or api_base_default
|
|
133
|
-
or None
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
max_tokens_default = existing_profile.max_tokens if existing_profile else 4096
|
|
137
|
-
max_tokens = (
|
|
138
|
-
parse_int(
|
|
139
|
-
f"Max output tokens [{max_tokens_default}]: ",
|
|
140
|
-
max_tokens_default,
|
|
141
|
-
)
|
|
142
|
-
or max_tokens_default
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
temp_default = existing_profile.temperature if existing_profile else 0.7
|
|
146
|
-
temperature = parse_float(
|
|
147
|
-
f"Temperature [{temp_default}]: ",
|
|
148
|
-
temp_default,
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
context_window_default = existing_profile.context_window if existing_profile else None
|
|
152
|
-
context_prompt = "Context window tokens (optional"
|
|
153
|
-
if context_window_default:
|
|
154
|
-
context_prompt += f", current {context_window_default}"
|
|
155
|
-
context_prompt += "): "
|
|
156
|
-
context_window = parse_int(context_prompt, context_window_default)
|
|
157
|
-
|
|
158
|
-
# Vision support prompt
|
|
159
|
-
supports_vision_default = existing_profile.supports_vision if existing_profile else None
|
|
160
|
-
supports_vision = None
|
|
161
|
-
vision_default_display = (
|
|
162
|
-
"auto"
|
|
163
|
-
if supports_vision_default is None
|
|
164
|
-
else ("yes" if supports_vision_default else "no")
|
|
165
|
-
)
|
|
166
|
-
supports_vision_input = (
|
|
167
|
-
console.input(f"Supports vision (images)? [{vision_default_display}] (Y/n/auto): ")
|
|
168
|
-
.strip()
|
|
169
|
-
.lower()
|
|
170
|
-
)
|
|
171
|
-
if supports_vision_input in ("y", "yes"):
|
|
172
|
-
supports_vision = True
|
|
173
|
-
elif supports_vision_input in ("n", "no"):
|
|
174
|
-
supports_vision = False
|
|
175
|
-
elif supports_vision_input in ("auto", ""):
|
|
176
|
-
supports_vision = None
|
|
177
|
-
else:
|
|
178
|
-
supports_vision = supports_vision_default
|
|
179
|
-
|
|
180
|
-
default_set_main = (
|
|
181
|
-
not config.model_profiles
|
|
182
|
-
or getattr(config.model_pointers, "main", "") not in config.model_profiles
|
|
183
|
-
)
|
|
184
|
-
set_main_input = (
|
|
185
|
-
console.input(f"Set as main model? ({'Y' if default_set_main else 'y'}/N): ")
|
|
186
|
-
.strip()
|
|
187
|
-
.lower()
|
|
188
|
-
)
|
|
189
|
-
set_as_main = set_main_input in ("y", "yes") if set_main_input else default_set_main
|
|
190
|
-
|
|
191
|
-
profile = ModelProfile(
|
|
192
|
-
provider=provider,
|
|
193
|
-
model=model_name,
|
|
194
|
-
api_key=api_key,
|
|
195
|
-
api_base=api_base,
|
|
196
|
-
max_tokens=max_tokens,
|
|
197
|
-
temperature=temperature,
|
|
198
|
-
context_window=context_window,
|
|
199
|
-
auth_token=auth_token,
|
|
200
|
-
supports_vision=supports_vision,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
831
|
try:
|
|
204
832
|
add_model_profile(
|
|
205
833
|
profile_name,
|
|
@@ -228,114 +856,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
228
856
|
console.print("[red]Model profile not found.[/red]")
|
|
229
857
|
print_models_usage()
|
|
230
858
|
return True
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
provider_input = (
|
|
234
|
-
console.input(
|
|
235
|
-
f"Protocol ({', '.join(p.value for p in ProviderType)}) [{provider_default}]: "
|
|
236
|
-
)
|
|
237
|
-
.strip()
|
|
238
|
-
.lower()
|
|
239
|
-
or provider_default
|
|
240
|
-
)
|
|
241
|
-
try:
|
|
242
|
-
provider = ProviderType(provider_input)
|
|
243
|
-
except ValueError:
|
|
244
|
-
console.print(f"[red]Invalid provider: {escape(provider_input)}[/red]")
|
|
859
|
+
updated_profile = _collect_edit_profile_input(console, existing_profile)
|
|
860
|
+
if not updated_profile:
|
|
245
861
|
return True
|
|
246
862
|
|
|
247
|
-
model_name = (
|
|
248
|
-
console.input(f"Model name to send [{existing_profile.model}]: ").strip()
|
|
249
|
-
or existing_profile.model
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
api_key_label = "[set]" if existing_profile.api_key else "[not set]"
|
|
253
|
-
api_key_prompt = f"API key {api_key_label} (Enter=keep, '-'=clear)"
|
|
254
|
-
api_key_input = prompt_secret(api_key_prompt).strip()
|
|
255
|
-
if api_key_input == "-":
|
|
256
|
-
api_key = None
|
|
257
|
-
elif api_key_input:
|
|
258
|
-
api_key = api_key_input
|
|
259
|
-
else:
|
|
260
|
-
api_key = existing_profile.api_key
|
|
261
|
-
|
|
262
|
-
auth_token = existing_profile.auth_token
|
|
263
|
-
if (
|
|
264
|
-
provider == ProviderType.ANTHROPIC
|
|
265
|
-
or existing_profile.provider == ProviderType.ANTHROPIC
|
|
266
|
-
):
|
|
267
|
-
auth_label = "[set]" if auth_token else "[not set]"
|
|
268
|
-
auth_prompt = f"Auth token (Anthropic only) {auth_label} (Enter=keep, '-'=clear)"
|
|
269
|
-
auth_token_input = prompt_secret(auth_prompt).strip()
|
|
270
|
-
if auth_token_input == "-":
|
|
271
|
-
auth_token = None
|
|
272
|
-
elif auth_token_input:
|
|
273
|
-
auth_token = auth_token_input
|
|
274
|
-
else:
|
|
275
|
-
auth_token = None
|
|
276
|
-
|
|
277
|
-
api_base = (
|
|
278
|
-
console.input(f"API base (optional) [{existing_profile.api_base or ''}]: ").strip()
|
|
279
|
-
or existing_profile.api_base
|
|
280
|
-
)
|
|
281
|
-
if api_base == "":
|
|
282
|
-
api_base = None
|
|
283
|
-
|
|
284
|
-
max_tokens = (
|
|
285
|
-
parse_int(
|
|
286
|
-
f"Max output tokens [{existing_profile.max_tokens}]: ",
|
|
287
|
-
existing_profile.max_tokens,
|
|
288
|
-
)
|
|
289
|
-
or existing_profile.max_tokens
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
temperature = parse_float(
|
|
293
|
-
f"Temperature [{existing_profile.temperature}]: ",
|
|
294
|
-
existing_profile.temperature,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
context_window = parse_int(
|
|
298
|
-
f"Context window tokens [{existing_profile.context_window or 'unset'}]: ",
|
|
299
|
-
existing_profile.context_window,
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
# Vision support prompt
|
|
303
|
-
vision_default_display = (
|
|
304
|
-
"auto"
|
|
305
|
-
if existing_profile.supports_vision is None
|
|
306
|
-
else ("yes" if existing_profile.supports_vision else "no")
|
|
307
|
-
)
|
|
308
|
-
supports_vision_input = (
|
|
309
|
-
console.input(
|
|
310
|
-
f"Supports vision (images)? [{vision_default_display}] (Y/n/auto/C=clear): "
|
|
311
|
-
)
|
|
312
|
-
.strip()
|
|
313
|
-
.lower()
|
|
314
|
-
)
|
|
315
|
-
supports_vision = None
|
|
316
|
-
if supports_vision_input in ("y", "yes"):
|
|
317
|
-
supports_vision = True
|
|
318
|
-
elif supports_vision_input in ("n", "no"):
|
|
319
|
-
supports_vision = False
|
|
320
|
-
elif supports_vision_input in ("c", "clear", "-"):
|
|
321
|
-
supports_vision = None
|
|
322
|
-
elif supports_vision_input in ("auto", ""):
|
|
323
|
-
supports_vision = existing_profile.supports_vision
|
|
324
|
-
else:
|
|
325
|
-
supports_vision = existing_profile.supports_vision
|
|
326
|
-
|
|
327
|
-
updated_profile = ModelProfile(
|
|
328
|
-
provider=provider,
|
|
329
|
-
model=model_name,
|
|
330
|
-
api_key=api_key,
|
|
331
|
-
api_base=api_base,
|
|
332
|
-
max_tokens=max_tokens,
|
|
333
|
-
temperature=temperature,
|
|
334
|
-
context_window=context_window,
|
|
335
|
-
auth_token=auth_token,
|
|
336
|
-
supports_vision=supports_vision,
|
|
337
|
-
)
|
|
338
|
-
|
|
339
863
|
try:
|
|
340
864
|
add_model_profile(
|
|
341
865
|
profile_name,
|
|
@@ -433,46 +957,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
433
957
|
return True
|
|
434
958
|
|
|
435
959
|
print_models_usage()
|
|
436
|
-
|
|
437
|
-
if not config.model_profiles:
|
|
438
|
-
console.print(" • No models configured")
|
|
439
|
-
return True
|
|
440
|
-
|
|
441
|
-
console.print("\n[bold]Configured Models:[/bold]")
|
|
442
|
-
for name, profile in config.model_profiles.items():
|
|
443
|
-
markers = [ptr for ptr, value in pointer_map.items() if value == name]
|
|
444
|
-
marker_text = f" ({', '.join(markers)})" if markers else ""
|
|
445
|
-
console.print(f" • {escape(name)}{marker_text}", markup=False)
|
|
446
|
-
console.print(f" protocol: {profile.provider.value}", markup=False)
|
|
447
|
-
console.print(f" model: {profile.model}", markup=False)
|
|
448
|
-
if profile.api_base:
|
|
449
|
-
console.print(f" api_base: {profile.api_base}", markup=False)
|
|
450
|
-
if profile.context_window:
|
|
451
|
-
console.print(f" context: {profile.context_window} tokens", markup=False)
|
|
452
|
-
console.print(
|
|
453
|
-
f" max_tokens: {profile.max_tokens}, temperature: {profile.temperature}",
|
|
454
|
-
markup=False,
|
|
455
|
-
)
|
|
456
|
-
console.print(f" api_key: {'***' if profile.api_key else 'Not set'}", markup=False)
|
|
457
|
-
if profile.provider == ProviderType.ANTHROPIC:
|
|
458
|
-
console.print(
|
|
459
|
-
f" auth_token: {'***' if getattr(profile, 'auth_token', None) else 'Not set'}",
|
|
460
|
-
markup=False,
|
|
461
|
-
)
|
|
462
|
-
if profile.openai_tool_mode:
|
|
463
|
-
console.print(f" openai_tool_mode: {profile.openai_tool_mode}", markup=False)
|
|
464
|
-
if profile.thinking_mode:
|
|
465
|
-
console.print(f" thinking_mode: {profile.thinking_mode}", markup=False)
|
|
466
|
-
# Display vision support
|
|
467
|
-
if profile.supports_vision is None:
|
|
468
|
-
vision_display = "auto-detect"
|
|
469
|
-
elif profile.supports_vision:
|
|
470
|
-
vision_display = "yes"
|
|
471
|
-
else:
|
|
472
|
-
vision_display = "no"
|
|
473
|
-
console.print(f" supports_vision: {vision_display}", markup=False)
|
|
474
|
-
pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
|
|
475
|
-
console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
|
|
960
|
+
_render_models_plain(console, config)
|
|
476
961
|
return True
|
|
477
962
|
|
|
478
963
|
|