@@ -139,16 +139,21 @@ def _render(sel: int):
139139 _render (selected )
140140
141141
142- def _model_picker (current_model_id : str ) -> str | None :
143- """Arrow-key model picker grouped by provider. Returns chosen model id, or None if cancelled."""
142+ def _model_picker (current_model_id : str , models : list [dict ] | None = None ) -> str | None :
143+ """Arrow-key model picker. With the built-in static catalog (models=None),
144+ groups by provider and shows tier. With a live-fetched list (flat, no
145+ provider/tier metadata), renders a plain flat list instead."""
144146 from .models import MODELS , PROVIDERS , TIER_COLORS
147+ live = models is not None
148+ if not live :
149+ models = MODELS
145150
146151 # Build a flat list of rows: ("header", provider_name) or ("model", model_dict)
147152 rows : list [tuple [str , object ]] = []
148153 last_provider = None
149154 selectable : list [int ] = [] # indices into rows that are selectable
150- for m in MODELS :
151- if m ["provider" ] != last_provider :
155+ for m in models :
156+ if not live and m ["provider" ] != last_provider :
152157 last_provider = m ["provider" ]
153158 p = PROVIDERS .get (last_provider , {})
154159 rows .append (("header" , p .get ("name" , last_provider )))
@@ -168,10 +173,13 @@ def _render():
168173 renderer .console .print (f" [bold]{ val } [/bold]" )
169174 continue
170175 m = val
171- tier_color = TIER_COLORS .get (m ["tier" ], "white" )
172176 is_cur = m ["id" ] == current_model_id
173177 mark = " ◀" if is_cur else ""
174- label = f"{ m ['name' ]:<24} [{ tier_color } ]{ m ['tier' ]:<10} [/{ tier_color } ]{ mark } "
178+ if live :
179+ label = f"{ m ['name' ]:<30} [cyan]{ m ['id' ]} [/cyan]{ mark } "
180+ else :
181+ tier_color = TIER_COLORS .get (m ["tier" ], "white" )
182+ label = f"{ m ['name' ]:<24} [{ tier_color } ]{ m ['tier' ]:<10} [/{ tier_color } ]{ mark } "
175183 if ri == selectable [selected_idx ]:
176184 renderer .console .print (f" [bold { renderer .PERMISSION_BLUE } ]❯ { label } [/bold { renderer .PERMISSION_BLUE } ]" )
177185 else :
@@ -322,9 +330,10 @@ def _build_prompt(context_block: str) -> str:
322330 ("/notify" , "Toggle bell notification on/off" ),
323331 ("/color" , "Set UI accent color — e.g. /color blue, /color #78aaff" ),
324332 ("/context" , "Scan project for context — /context on|off to toggle auto-scan" ),
325- ("/reasoning" , "Set reasoning level — off/low/middle/high/ultra" ),
333+ ("/reasoning" , "Set reasoning level — /reasoning for picker, or off/low/middle/high/ultra" ),
326334 ("/agent" , "Toggle agent mode (file/shell tools)" ),
327335 ("/model" , "Switch model — e.g. /model opus" ),
336+ ("/server" , "Set backend URL/key/models path — /server, /server <url> [key], /server models=<path>, /server reset" ),
328337 ("/models" , "List all 34 models" ),
329338 ("/merge" , "Toggle Merge AI mode" ),
330339 ("/search" , "Toggle Web Search mode" ),
@@ -1277,8 +1286,17 @@ async def _run_bootstrap():
12771286 await _run_bootstrap ()
12781287
12791288 elif cmd == "/model" :
1289+ # Only bother fetching live when a non-default backend is configured —
1290+ # otherwise keep the nicer grouped static catalog for use.ai.
1291+ live_models = await api .fetch_models () if cfg .get ("base_url" ) else None
12801292 if arg :
1281- found = find_model (arg )
1293+ found = None
1294+ if live_models :
1295+ q = arg .lower ().strip ()
1296+ found = next ((m for m in live_models
1297+ if q in m ["id" ].lower () or q in m ["name" ].lower ()), None )
1298+ if not found :
1299+ found = find_model (arg )
12821300 if found :
12831301 model_id = found ["id" ]
12841302 mode = "chat"
@@ -1289,7 +1307,7 @@ async def _run_bootstrap():
12891307 else :
12901308 renderer .print_error (f"No model matching '{ arg } '. Try /models." )
12911309 else :
1292- chosen_id = _model_picker (model_id )
1310+ chosen_id = _model_picker (model_id , models = live_models )
12931311 if chosen_id :
12941312 model_id = chosen_id
12951313 mode = "chat"
@@ -1299,7 +1317,12 @@ async def _run_bootstrap():
12991317 renderer .print_model_status (model_id , mode , agent_mode )
13001318
13011319 elif cmd == "/models" :
1302- renderer .print_models_list (model_id )
1320+ live = await api .fetch_models () if cfg .get ("base_url" ) else None
1321+ if live :
1322+ renderer .print_info (f"Live from { api .get_base_url ()} { api .get_models_path ()} :" )
1323+ renderer .print_live_models_list (live , model_id )
1324+ else :
1325+ renderer .print_models_list (model_id )
13031326
13041327 elif cmd == "/merge" :
13051328 mode = "chat" if mode == "merge" else "merge"
@@ -1360,17 +1383,37 @@ async def _run_bootstrap():
13601383
13611384 elif cmd == "/reasoning" :
13621385 lvl = arg .lower ().strip ()
1363- if lvl == "off" or (not lvl and reasoning_level ):
1386+ if not lvl :
1387+ levels = ["off" ] + list (REASONING_LEVELS .keys ())
1388+ cur = reasoning_level or "off"
1389+ labeled = [f"{ l } (current)" if l == cur else l for l in levels ]
1390+ options = labeled + ["Cancel" ]
1391+ renderer .console .print ()
1392+ renderer .console .print (f" [bold]Reasoning level[/bold] [dim]current: { cur } [/dim]" )
1393+ choice = _pick_option (options , controller )
1394+ # _pick_option's last list item is its dedicated "free text" slot and
1395+ # comes back as the literal string "__free__", not its label — map it
1396+ # back to "Cancel" (the actual last option here) rather than mis-parsing it.
1397+ if choice == "__free__" :
1398+ choice = "Cancel"
1399+ chosen_lvl = None if choice in (None , "Cancel" ) else choice .split (" " , 1 )[0 ]
1400+ if chosen_lvl and chosen_lvl != cur :
1401+ reasoning_level = None if chosen_lvl == "off" else chosen_lvl
1402+ cfg ["reasoning" ] = reasoning_level
1403+ storage .save_config (cfg )
1404+ renderer .print_info (f"Reasoning: { chosen_lvl } " )
1405+ elif lvl == "off" :
13641406 reasoning_level = None
13651407 cfg ["reasoning" ] = None
13661408 renderer .print_info ("Reasoning OFF." )
1409+ storage .save_config (cfg )
13671410 elif lvl in REASONING_LEVELS :
13681411 reasoning_level = lvl
13691412 cfg ["reasoning" ] = lvl
13701413 renderer .print_info (f"Reasoning: { lvl } " )
1414+ storage .save_config (cfg )
13711415 else :
13721416 renderer .print_error (f"Unknown level '{ lvl } '. Use: off, low, middle, high, ultra" )
1373- storage .save_config (cfg )
13741417
13751418 elif cmd == "/color" :
13761419 spec = arg .strip ()
@@ -1393,6 +1436,79 @@ async def _run_bootstrap():
13931436 renderer .print_model_status (model_id , mode , agent_mode )
13941437 renderer .print_info (f"Color set to { spec } ." )
13951438
1439+ elif cmd == "/server" :
1440+ spec = arg .strip ()
1441+ _NO_KEY_WORDS = {"none" , "no" , "-" , "skip" , "n/a" }
1442+ if not spec :
1443+ cur = cfg .get ("base_url" ) or api ._DEFAULT_BASE
1444+ has_key = bool (cfg .get ("api_key" ))
1445+ cur_models_path = cfg .get ("models_path" ) or api ._DEFAULT_MODELS_PATH
1446+ renderer .console .print ()
1447+ renderer .console .print (f" [bold]Backend[/bold] [dim]{ cur } { ' · API key set' if has_key else ' · no API key' } · models: { cur_models_path } [/dim]" )
1448+ options = ["Enter a custom URL" , "Set models endpoint path" ,
1449+ "Reset to default" , "Cancel" , "Keep current" ]
1450+ choice = _pick_option (options , controller )
1451+ if choice in (None , "Cancel" , "Keep current" , "__free__" ):
1452+ pass
1453+ elif choice == "Reset to default" :
1454+ cfg .pop ("base_url" , None )
1455+ cfg .pop ("api_key" , None )
1456+ cfg .pop ("models_path" , None )
1457+ storage .save_config (cfg )
1458+ renderer .print_info (f"Backend reset to default ({ api ._DEFAULT_BASE } )." )
1459+ elif choice == "Set models endpoint path" :
1460+ renderer .print_info (f"Models endpoint path (current: { cur_models_path } ). "
1461+ "Common values: /models or /v1/models" )
1462+ path_in = await controller .read_one () if controller else None
1463+ path_in = (path_in or "" ).strip ()
1464+ if path_in :
1465+ cfg ["models_path" ] = "/" + path_in .strip ("/" )
1466+ storage .save_config (cfg )
1467+ renderer .print_info (f"Models endpoint set to { cfg ['models_path' ]} ." )
1468+ elif choice == "Enter a custom URL" :
1469+ renderer .print_info ("Base URL (http:// or https://):" )
1470+ url_in = await controller .read_one () if controller else None
1471+ url_in = (url_in or "" ).strip ()
1472+ if not (url_in .startswith ("http://" ) or url_in .startswith ("https://" )):
1473+ renderer .print_error ("Cancelled — base URL must start with http:// or https://" )
1474+ else :
1475+ renderer .print_info ("API key — type it, or type none if this backend doesn't need one:" )
1476+ key_in = await controller .read_one () if controller else None
1477+ key_in = (key_in or "" ).strip ()
1478+ cfg ["base_url" ] = url_in
1479+ if key_in and key_in .lower () not in _NO_KEY_WORDS :
1480+ cfg ["api_key" ] = key_in
1481+ else :
1482+ cfg .pop ("api_key" , None )
1483+ storage .save_config (cfg )
1484+ renderer .print_info (f"Backend set to { url_in } { ' with API key' if key_in and key_in .lower () not in _NO_KEY_WORDS else '' } ." )
1485+ elif spec .lower () == "reset" :
1486+ cfg .pop ("base_url" , None )
1487+ cfg .pop ("api_key" , None )
1488+ cfg .pop ("models_path" , None )
1489+ storage .save_config (cfg )
1490+ renderer .print_info (f"Backend reset to default ({ api ._DEFAULT_BASE } )." )
1491+ elif spec .lower ().startswith ("models=" ):
1492+ path_in = spec .split ("=" , 1 )[1 ].strip ()
1493+ if path_in :
1494+ cfg ["models_path" ] = "/" + path_in .strip ("/" )
1495+ storage .save_config (cfg )
1496+ renderer .print_info (f"Models endpoint set to { cfg ['models_path' ]} ." )
1497+ else :
1498+ renderer .print_error ("Usage: /server models=<path> e.g. /server models=/v1/models" )
1499+ else :
1500+ parts_srv = spec .split (None , 1 )
1501+ url = parts_srv [0 ]
1502+ key = parts_srv [1 ].strip () if len (parts_srv ) > 1 else None
1503+ if not (url .startswith ("http://" ) or url .startswith ("https://" )):
1504+ renderer .print_error ("Base URL must start with http:// or https://" )
1505+ else :
1506+ cfg ["base_url" ] = url
1507+ if key and key .lower () not in _NO_KEY_WORDS :
1508+ cfg ["api_key" ] = key
1509+ storage .save_config (cfg )
1510+ renderer .print_info (f"Backend set to { url } { ' with API key' if key and key .lower () not in _NO_KEY_WORDS else '' } ." )
1511+
13961512 elif cmd == "/context" :
13971513 sub = arg .strip ().lower ()
13981514 if sub == "off" :
0 commit comments