Skip to content

Commit d473290

Browse files
Murder the twins: add_model_menu and model_settings_menu on termflow
The two 1,400-line prompt_toolkit Applications are rebuilt as menu + TextInput composition. add_model: searchable provider/model menus with detail previews, Ctrl+E credential editing via masked TextInputs, custom-model entry with validated context parsing, same extra_models.json write path. model_settings: model list preselecting the active model, per-setting editors (choice pickers, validated numeric TextInputs where empty clears the override), custom-params add/edit/delete with key=value validation, r-to-reset. The catalog-snapshot latency invariant survives: the flow loads the model catalog exactly once, test-enforced. All business logic (config building, retry-namespace writes, capability filtering) preserved verbatim in sibling defs modules under the 600-line cap.
1 parent 361e95a commit d473290

11 files changed

Lines changed: 2132 additions & 4686 deletions
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""Detail-pane renderers for the add-model browser.
2+
3+
Plain ANSI strings over the active theme palette; consumed as termflow
4+
Menu previews by :mod:`code_puppy.command_line.add_model_menu` (split
5+
out of that module purely for the 600-line cap).
6+
"""
7+
8+
from code_puppy.models_dev_parser import ModelInfo, ProviderInfo
9+
from code_puppy.provider_credentials import (
10+
credential_display,
11+
credential_hint,
12+
is_credential_set,
13+
)
14+
15+
16+
def _unsupported() -> dict:
17+
# Lazy import: add_model_menu imports this module at load time.
18+
from code_puppy.command_line.add_model_menu import UNSUPPORTED_PROVIDERS
19+
20+
return UNSUPPORTED_PROVIDERS
21+
22+
23+
def _style():
24+
from termflow.render.style import RenderStyle
25+
26+
from code_puppy.command_line.tui_style import menu_style
27+
28+
return menu_style() or RenderStyle.default()
29+
30+
31+
def _ansi(color: str, text: str) -> str:
32+
from termflow.ansi.codes import RESET
33+
from termflow.ansi.color import fg_color
34+
35+
return f"{fg_color(color)}{text}{RESET}"
36+
37+
38+
def provider_details(provider: ProviderInfo) -> str:
39+
s = _style()
40+
lines = [
41+
_ansi(s.bright, "MODEL DETAILS"),
42+
"",
43+
_ansi(s.head, f"{provider.name}"),
44+
f"ID: {provider.id}",
45+
f"Models: {provider.model_count}",
46+
f"API: {provider.api}",
47+
]
48+
if provider.id in _unsupported():
49+
lines += [
50+
"",
51+
_ansi(s.error, "UNSUPPORTED PROVIDER"),
52+
_ansi(s.error, _unsupported()[provider.id]),
53+
_ansi(s.grey, "Models from this provider cannot be added."),
54+
]
55+
if provider.env:
56+
lines += ["", _ansi(s.head, "Credentials:")]
57+
for env_var in provider.env:
58+
color = s.head if is_credential_set(env_var) else s.error
59+
lines.append(_ansi(color, f" {env_var}: {credential_display(env_var)}"))
60+
hint = credential_hint(env_var)
61+
if hint:
62+
lines.append(_ansi(s.grey, f" {hint}"))
63+
if provider.doc:
64+
lines += ["", _ansi(s.head, "Documentation:"), f" {provider.doc}"]
65+
return "\n".join(lines)
66+
67+
68+
def custom_model_details(provider: ProviderInfo) -> str:
69+
s = _style()
70+
lines = [
71+
_ansi(s.bright, "MODEL DETAILS"),
72+
"",
73+
_ansi(s.head, "Custom Model"),
74+
"Add a model not listed in models.dev",
75+
"",
76+
_ansi(s.grey, "1. Press Enter to select"),
77+
_ansi(s.grey, "2. Enter the model ID/name"),
78+
_ansi(s.grey, f"3. Uses {provider.name}'s API endpoint"),
79+
]
80+
if provider.env:
81+
lines += ["", _ansi(s.head, "Required credentials:")]
82+
lines += [_ansi(s.grey, f" {env_var}") for env_var in provider.env]
83+
return "\n".join(lines)
84+
85+
86+
def model_details(model: ModelInfo, provider: ProviderInfo) -> str:
87+
s = _style()
88+
lines = [
89+
_ansi(s.bright, "MODEL DETAILS"),
90+
"",
91+
_ansi(s.head, f"{provider.name} - {model.name}"),
92+
]
93+
if not model.tool_call:
94+
lines += [
95+
"",
96+
_ansi(s.error, "NO TOOL CALLING SUPPORT"),
97+
_ansi(s.error, "This model cannot use tools (file ops, shell"),
98+
_ansi(s.error, "commands, etc). Very limited for coding tasks!"),
99+
]
100+
lines += ["", _ansi(s.head, "Capabilities:")]
101+
for cap_name, has_cap in (
102+
("Vision", model.has_vision),
103+
("Tool Calling", model.tool_call),
104+
("Reasoning", model.reasoning),
105+
("Temperature", model.temperature),
106+
("Structured Output", model.structured_output),
107+
("Attachments", model.attachment),
108+
):
109+
mark = "+" if has_cap else "-"
110+
lines.append(_ansi(s.head if has_cap else s.grey, f" {mark} {cap_name}"))
111+
lines += ["", _ansi(s.head, "Pricing:")]
112+
if model.cost_input is not None or model.cost_output is not None:
113+
if model.cost_input is not None:
114+
lines.append(_ansi(s.grey, f" Input: ${model.cost_input:.6f}/token"))
115+
if model.cost_output is not None:
116+
lines.append(_ansi(s.grey, f" Output: ${model.cost_output:.6f}/token"))
117+
if model.cost_cache_read is not None:
118+
lines.append(
119+
_ansi(s.grey, f" Cache Read: ${model.cost_cache_read:.6f}/token")
120+
)
121+
else:
122+
lines.append(_ansi(s.grey, " Pricing not available"))
123+
lines += ["", _ansi(s.head, "Limits:")]
124+
if model.context_length > 0:
125+
lines.append(_ansi(s.grey, f" Context: {model.context_length:,} tokens"))
126+
if model.max_output > 0:
127+
lines.append(_ansi(s.grey, f" Max Output: {model.max_output:,} tokens"))
128+
if model.input_modalities or model.output_modalities:
129+
lines += ["", _ansi(s.head, "Modalities:")]
130+
if model.input_modalities:
131+
lines.append(_ansi(s.grey, f" Input: {', '.join(model.input_modalities)}"))
132+
if model.output_modalities:
133+
lines.append(
134+
_ansi(s.grey, f" Output: {', '.join(model.output_modalities)}")
135+
)
136+
lines += ["", _ansi(s.head, "Metadata:")]
137+
lines.append(_ansi(s.grey, f" Model ID: {model.model_id}"))
138+
lines.append(_ansi(s.grey, f" Full ID: {model.full_id}"))
139+
if model.knowledge:
140+
lines.append(_ansi(s.grey, f" Knowledge: {model.knowledge}"))
141+
if model.release_date:
142+
lines.append(_ansi(s.grey, f" Released: {model.release_date}"))
143+
lines.append(_ansi(s.grey, f" Open Weights: {model.open_weights}"))
144+
return "\n".join(lines)

0 commit comments

Comments
 (0)