char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
d9f918d2d
char/aliceai-t5-moe
1#include "cli-context.h" 2#include "cli-ui.h" 3 4#include "arg.h" 5#include "base64.hpp" 6#include "log.h" 7#include "console.h" 8 9#include "json.h" 10 11#include <algorithm> 12#include <cctype> 13#include <filesystem> 14#include <fstream> 15#include <map> 16#include <set> 17 18using json = common_json ; 19 20struct cli_context_impl { 21json messages = json::array (); 22json pending_media = json::array ();// staged multimodal content parts 23}; 24 25cli_context::cli_context (const common_params & params ) :params (params ),impl (new cli_context_impl ()) {} 26 27cli_context::~cli_context () { 28shutdown (); 29} 30 31std::atomic < bool > & cli_context::interrupted () { 32static std::atomic < bool > flag = false; 33return flag ; 34} 35 36static bool should_stop () { 37return cli_context::interrupted ().load (); 38} 39 40static constexpr size_t FILE_GLOB_MAX_RESULTS = 100 ; 41 42const char * LLAMA_ASCII_LOGO = R"( 43▄▄ ▄▄ 44██ ██ 45██ ██ ▀▀█▄ ███▄███▄ ▀▀█▄ ▄████ ████▄ ████▄ 46██ ██ ▄█▀██ ██ ██ ██ ▄█▀██ ██ ██ ██ ██ ██ 47██ ██ ▀█▄██ ██ ██ ██ ▀█▄██ ██ ▀████ ████▀ ████▀ 48██ ██ 49▀▀ ▀▀ 50)" ; 51 52// number of values an arg consumes on the command line 53static int arg_num_values (const common_arg & opt ) { 54if (opt .value_hint_2 != nullptr ) { 55return 2 ; 56 } 57if (opt .value_hint != nullptr ) { 58return 1 ; 59 } 60return 0 ; 61} 62 63static std::string format_error_message (const json & err ) { 64if (err .contains ("error" )&& err .at ("error" ).is_object ()) { 65const auto & e = err .at ("error" ); 66if (e .contains ("message" )&& e .at ("message" ).is_string ()) { 67return e .at ("message" ).get < std::string > (); 68 } 69 } 70return err .dump (); 71} 72 73// err is the raw response body of a failed request; it may or may not be JSON 74static std::string format_error_message (const std::string & err ) { 75json parsed = json::parse_no_throw (err ); 76if (!parsed .is_discarded ()) { 77return format_error_message (parsed ); 78 } 79return err ; 80} 81 82static std::string media_type_from_ext (const std::string & fname ) { 83 std::string ext = std::filesystem::path (fname ).extension ().string (); 84 std::transform (ext .begin (),ext .end (),ext .begin (), [](unsigned char c ) {return std::tolower (c ); }); 85if (ext == ".wav" || ext == ".mp3" ) { 86return "audio" ; 87 } 88if (ext == ".mp4" || ext == ".avi" || ext == ".mkv" || ext == ".mov" || ext == ".webm" ) { 89return "video" ; 90 } 91return "image" ; 92} 93 94bool cli_context::init () { 95 ui::init (params ); 96 97 std::optional < ui::spinner > spinner ; 98 99bool use_external_server = !params .server_base .empty (); 100if (use_external_server ) { 101 std::string base = params .server_base ; 102while (!base .empty ()&& base .back ()== '/' ) { 103base .pop_back (); 104 } 105client .server_base = base ; 106 107spinner .emplace ("Connecting to server at " + base ); 108 }else { 109if (params .model .path .empty ()&& params .model .url .empty ()&& 110params .model .hf_repo .empty ()&& params .model .docker_repo .empty ()) { 111 ui::show_error ( 112"no model specified" , 113"use -m <file.gguf> or -hf <user/repo> to run a local model,\n" 114"or --server-base <url> to connect to a running llama-server" 115 ); 116return false; 117 } 118 119spinner .emplace ("\n\nLoading model..." ); 120 121server .emplace (); 122if (!server -> start (params )) { 123 ui::show_error ("server start failed" ); 124return false; 125 } 126if (!server -> wait_ready (should_stop )) { 127if (!should_stop ()) { 128 ui::show_error ("the server exited before becoming ready" ); 129 } 130return false; 131 } 132client .server_base = server -> address (); 133 } 134 135// for --server-base this is the main availability check; for a spawned 136// server it is a cheap sanity check on top of the ready signal 137auto is_aborted = [this ]() { 138return should_stop ()|| (server && !server -> alive ()); 139 }; 140bool healthy = false; 141try { 142healthy = client .wait_health (is_aborted ); 143 }catch (const std::exception & e ) { 144client .last_error = e .what (); 145 } 146if (!healthy ) { 147if (!should_stop ()) { 148 ui::show_error (client .last_error ); 149 } 150return false; 151 } 152 153if (use_external_server ) { 154spinner .reset (); 155try { 156if (!list_and_ask_models ()) { 157return false; 158 } 159 }catch (const common_json_error & e ) { 160 ui::show_error (e .what ()); 161 ui::show_message ("This might be caused by an incorrect server-base endpoint URL" ); 162return false; 163 }catch (const std::exception & e ) { 164 ui::show_error (e .what ()); 165return false; 166 } 167 168// restore the spinner for the next step 169spinner .emplace ("Waiting for server..." ); 170 } 171 172fetch_server_props (); 173 174if (!params .out_file .empty ()) { 175output_file .emplace (params .out_file ); 176if (!output_file -> is_open ()) { 177 ui::show_error (string_format ("failed to open output file '%s'" ,params .out_file .c_str ())); 178return false; 179 } 180 } 181 182return true; 183} 184 185void cli_context::fetch_server_props () { 186try { 187json props = json::parse (client .get ("/props" )); 188model_name = props .value ("model_alias" ,"" ); 189if (model_name .empty ()) { 190const std::string path = props .value ("model_path" ,"" ); 191if (!path .empty ()) { 192model_name = std::filesystem::path (path ).filename ().string (); 193 } 194 } 195model_ftype = props .value ("model_ftype" ,"" ); 196build_info = props .value ("build_info" ,"" ); 197if (props .contains ("modalities" )&& props .at ("modalities" ).is_object ()) { 198const auto & modalities = props .at ("modalities" ); 199has_vision = modalities .value ("vision" , false); 200has_audio = modalities .value ("audio" , false); 201has_video = modalities .value ("video" , false); 202 } 203 }catch (const std::exception & e ) { 204// /props can be disabled on remote servers; not fatal 205LOG_DBG ("failed to fetch /props: %s\n" ,e .what ()); 206 } 207} 208 209bool cli_context::list_and_ask_models () { 210json resp = json::parse (client .get ("/v1/models" )); 211if (!resp .contains ("data" )|| !resp .at ("data" ).is_array ()) { 212throw std::runtime_error ("invalid response from /v1/models" ); 213 } 214 std::vector < std::string > models ; 215 std::vector < std::string > models_display ; 216for (const auto & m :resp .at ("data" )) { 217if (!m .contains ("id" )|| !m .at ("id" ).is_string ()) { 218continue ; 219 } 220 std::string name = m .at ("id" ).get < std::string > (); 221 std::string display = name ; 222if (m .contains ("aliases" )&& m .at ("aliases" ).is_array ()) { 223 std::vector < std::string > aliases ; 224for (const auto & a :m .at ("aliases" )) { 225if (a .is_string ()) { 226aliases .push_back (a .get < std::string > ()); 227 } 228 } 229if (!aliases .empty ()) { 230display += " (" + string_join (aliases ,", " )+ ")" ; 231 } 232 } 233models .push_back (name ); 234models_display .push_back (display ); 235 } 236 237// only one model: use it without asking 238if (models .size ()== 1 ) { 239model_name = models [0 ]; 240client .model = model_name ; 241return true; 242 } 243 244 std::string message = "\nAvailable models:" ; 245for (size_t i = 0 ;i < models_display .size ();++ i ) { 246message += "\n " + std::to_string (i + 1 )+ ". " + models_display [i ]; 247 } 248message += "\n" ; 249 ui::show_message (message ); 250 std::string selection ; 251while (selection .empty ()) { 252if (should_stop ()) { 253return false; 254 } 255 ui::user_turn user_turn ; 256selection = user_turn .read_input (false,"Select model by number: " ); 257if (selection .empty ()) { 258continue ; 259 } 260try { 261size_t idx = std::stoul (selection ); 262if (idx > 0 && idx <=models .size ()) { 263model_name = models [idx - 1 ]; 264client .model = model_name ; 265 ui::show_message ("Selected model: " + model_name ); 266break ; 267 } 268 }catch (...) { 269// ignore 270 } 271 ui::show_error ("Invalid selection. Please enter a valid number." ); 272selection .clear (); 273continue ; 274 } 275return true; 276} 277 278void cli_context::add_system_prompt () { 279if (!params .system_prompt .empty ()) { 280impl -> messages .push_back ({ 281 {"role" ,"system" }, 282 {"content" ,params .system_prompt } 283 }); 284 } 285} 286 287void cli_context::push_user_message (const std::string & text ) { 288json content ; 289if (impl -> pending_media .empty ()) { 290content = text ; 291 }else { 292// multimodal message: media parts first, then the text 293content = impl -> pending_media ; 294content .push_back ({ 295 {"type" ,"text" }, 296 {"text" ,text } 297 }); 298impl -> pending_media = json::array (); 299 } 300impl -> messages .push_back ({ 301 {"role" ,"user" }, 302 {"content" ,content } 303 }); 304} 305 306bool cli_context::stage_media_file (const std::string & fname ,const std::string & type ) { 307 std::ifstream file (fname , std::ios::binary ); 308if (!file ) { 309return false; 310 } 311 std::string data ((std::istreambuf_iterator < char > (file )), std::istreambuf_iterator < char > ()); 312 std::string encoded = base64::encode (data ); 313 314if (type == "audio" ) { 315 std::string ext = std::filesystem::path (fname ).extension ().string (); 316 std::transform (ext .begin (),ext .end (),ext .begin (), [](unsigned char c ) {return std::tolower (c ); }); 317impl -> pending_media .push_back ({ 318 {"type" ,"input_audio" }, 319 {"input_audio" , { 320 {"data" ,encoded }, 321 {"format" ,ext == ".mp3" ?"mp3" :"wav" } 322 }} 323 }); 324 }else if (type == "video" ) { 325impl -> pending_media .push_back ({ 326 {"type" ,"input_video" }, 327 {"input_video" , { 328 {"data" ,encoded } 329 }} 330 }); 331 }else { 332// the server detects the actual image type from the data 333impl -> pending_media .push_back ({ 334 {"type" ,"image_url" }, 335 {"image_url" , { 336 {"url" ,"data:image/unknown;base64," + encoded } 337 }} 338 }); 339 } 340return true; 341} 342 343void cli_context::write_output_file (const std::string & content ) { 344if (output_file ) { 345 (* output_file ) <<content ; 346output_file -> flush (); 347 } 348} 349 350bool cli_context::generate_completion (generated_content & content_out ,cli_timings & timings ) { 351json body = { 352 {"messages" ,impl -> messages }, 353 {"stream" , true}, 354// in order to get timings even when we cancel mid-way 355 {"timings_per_token" , true}, 356 }; 357if (!client .model .empty ()) { 358body ["model" ]= client .model ; 359 } 360 361bool stream_error = false; 362 363 ui::assistant_turn a ; 364 365 std::string err = client .post_sse ("/v1/chat/completions" ,body .dump (),should_stop , [& ](const std::string & payload ) { 366json chunk = json::parse_no_throw (payload ); 367if (chunk .is_discarded ()) { 368return ; 369 } 370if (chunk .contains ("error" )) { 371stream_error = true; 372 ui::show_error (format_error_message (chunk )); 373return ; 374 } 375if (chunk .contains ("timings" )) { 376const auto & t = chunk .at ("timings" ); 377timings .prompt_per_second = t .value ("prompt_per_second" ,0.0 ); 378timings .predicted_per_second = t .value ("predicted_per_second" ,0.0 ); 379 } 380if (!chunk .contains ("choices" )|| !chunk .at ("choices" ).is_array ()|| chunk .at ("choices" ).empty ()) { 381return ; 382 } 383const auto & choice = chunk .at ("choices" ).at (0 ); 384if (!choice .contains ("delta" )) { 385return ; 386 } 387const auto & delta = choice .at ("delta" ); 388if (delta .contains ("reasoning_content" )&& delta .at ("reasoning_content" ).is_string ()) { 389const std::string text = delta .at ("reasoning_content" ).get < std::string > (); 390if (!text .empty ()) { 391content_out .reasoning += text ; 392a .push (ui::ASSISTANT_DISPLAY_MODE_REASONING ,text ); 393 } 394 } 395if (delta .contains ("content" )&& delta .at ("content" ).is_string ()) { 396const std::string text = delta .at ("content" ).get < std::string > (); 397if (!text .empty ()) { 398content_out .content += text ; 399a .push (ui::ASSISTANT_DISPLAY_MODE_CONTENT ,text ); 400 } 401 } 402 }); 403 404 cli_context::interrupted ().store (false); 405 406if (!err .empty ()) { 407 ui::show_error (format_error_message (err )); 408return false; 409 } 410return !stream_error ; 411} 412 413int cli_context::run () { 414add_system_prompt (); 415 416 std::string modalities = "text" ; 417if (has_vision ) { 418modalities += ", vision" ; 419 } 420if (has_audio ) { 421modalities += ", audio" ; 422 } 423if (has_video ) { 424modalities += ", video" ; 425 } 426 427 std::string banner ; 428banner += "\n" ; 429banner += LLAMA_ASCII_LOGO ; 430banner += "\n" ; 431banner += "build : " + build_info + "\n" ; 432banner += "model : " + model_name + "\n" ; 433if (!model_ftype .empty ()) { 434banner += "ftype : " + model_ftype + "\n" ; 435 } 436banner += "modalities : " + modalities + "\n" ; 437if (!params .system_prompt .empty ()) { 438banner += "using custom system prompt\n" ; 439 } 440banner += "\n" ; 441banner += "available commands:\n" ; 442banner += " /exit or Ctrl+C stop or exit\n" ; 443banner += " /regen regenerate the last response\n" ; 444banner += " /clear clear the chat history\n" ; 445banner += " /read <file> add a text file\n" ; 446banner += " /glob <pattern> add text files using globbing pattern\n" ; 447if (has_vision ) { 448banner += " /image <file> add an image file\n" ; 449 } 450if (has_audio ) { 451banner += " /audio <file> add an audio file\n" ; 452 } 453if (has_video ) { 454banner += " /video <file> add a video file\n" ; 455 } 456banner += "\n" ; 457 458 ui::show_message (banner ); 459 460// interactive loop 461 std::string cur_msg ; 462 463auto add_text_file = [& ](const std::string & fname )-> bool { 464 std::ifstream file (fname , std::ios::binary ); 465if (!file ) { 466 ui::show_error (string_format ("file does not exist or cannot be opened: '%s'" ,fname .c_str ())); 467return false; 468 } 469 std::string content ((std::istreambuf_iterator < char > (file )), std::istreambuf_iterator < char > ()); 470cur_msg += "--- File: " ; 471cur_msg += fname ; 472cur_msg += " ---\n" ; 473cur_msg += content ; 474 ui::show_message (string_format ("Loaded text from '%s'" ,fname .c_str ())); 475return true; 476 }; 477 478while (true) { 479 std::string buffer ; 480 { 481 ui::user_turn user_turn ; 482 483if (params .prompt .empty ()) { 484buffer = user_turn .read_input (params .multiline_input ); 485 }else { 486// process input prompt from args 487for (auto & fname :params .image ) { 488if (!stage_media_file (fname ,media_type_from_ext (fname ))) { 489 ui::show_error (string_format ("file does not exist or cannot be opened: '%s'" ,fname .c_str ())); 490break ; 491 } 492 ui::show_message (string_format ("Loaded media from '%s'" ,fname .c_str ())); 493 } 494buffer = params .prompt ; 495user_turn .echo (buffer ); 496params .prompt .clear ();// only use it once 497 } 498 } 499 500if (should_stop ()) { 501 cli_context::interrupted ().store (false); 502break ; 503 } 504 505// remove trailing newline 506if (!buffer .empty ()&& buffer .back ()== '\n' ) { 507buffer .pop_back (); 508 } 509 510// skip empty messages 511if (buffer .empty ()) { 512continue ; 513 } 514 515bool add_user_msg = true; 516 517// process commands 518if (string_starts_with (buffer ,"/exit" )) { 519break ; 520 }else if (string_starts_with (buffer ,"/regen" )) { 521if (impl -> messages .size () >=2 ) { 522size_t last_idx = impl -> messages .size ()- 1 ; 523impl -> messages .erase (last_idx ); 524add_user_msg = false; 525 }else { 526 ui::show_error ("No message to regenerate." ); 527continue ; 528 } 529 }else if (string_starts_with (buffer ,"/clear" )) { 530impl -> messages .clear (); 531add_system_prompt (); 532 533impl -> pending_media = json::array (); 534 ui::show_message ("Chat history cleared." ); 535continue ; 536 }else if ( 537 (string_starts_with (buffer ,"/image " )&& has_vision )|| 538 (string_starts_with (buffer ,"/audio " )&& has_audio )|| 539 (string_starts_with (buffer ,"/video " )&& has_video )) { 540 std::string type = buffer .substr (1 ,5 ); 541// just in case (bad copy-paste for example), we strip all trailing/leading spaces 542 std::string fname = string_strip (buffer .substr (7 )); 543if (!stage_media_file (fname ,type )) { 544 ui::show_error (string_format ("file does not exist or cannot be opened: '%s'" ,fname .c_str ())); 545continue ; 546 } 547 ui::show_message (string_format ("Loaded media from '%s'" ,fname .c_str ())); 548write_output_file (string_format ("User: Added media: %s\n" ,fname .c_str ())); 549continue ; 550 }else if (string_starts_with (buffer ,"/read " )) { 551 std::string fname = string_strip (buffer .substr (6 )); 552add_text_file (fname ); 553write_output_file (string_format ("User: Added text file: %s\n" ,fname .c_str ())); 554continue ; 555 }else if (string_starts_with (buffer ,"/glob " )) { 556 std::error_code ec ; 557size_t count = 0 ; 558auto curdir = std::filesystem::current_path (); 559 std::string pattern = string_strip (buffer .substr (6 )); 560 std::filesystem::path rel_path ; 561 562auto startglob = pattern .find_first_of ("![*?" ); 563if (startglob != std::string::npos && startglob != 0 ) { 564auto endpath = pattern .substr (0 ,startglob ).find_last_of ('/' ); 565if (endpath != std::string::npos ) { 566 std::string rel_pattern = pattern .substr (0 ,endpath ); 567#if !defined(_WIN32 ) 568if (string_starts_with (rel_pattern ,'~' )) { 569const char * home = std::getenv ("HOME" ); 570if (home && home [0 ]) { 571rel_pattern = home + rel_pattern .substr (1 ); 572 } 573 } 574#endif 575rel_path = rel_pattern ; 576pattern .erase (0 ,endpath + 1 ); 577curdir /=rel_path ; 578 } 579 } 580 581for (const auto & entry : std::filesystem::recursive_directory_iterator (curdir , 582 std::filesystem::directory_options::skip_permission_denied ,ec )) { 583if (!entry .is_regular_file ()) { 584continue ; 585 } 586 587 std::string rel = std::filesystem::relative (entry .path (),curdir ,ec ).string (); 588if (ec ) { 589ec .clear (); 590continue ; 591 } 592 std::replace (rel .begin (),rel .end (),'\\' ,'/' ); 593 594if (!glob_match (pattern ,rel )) { 595continue ; 596 } 597 598const std::string full_path = (curdir /rel ).string (); 599if (!add_text_file (full_path )) { 600continue ; 601 } 602write_output_file (string_format ("User: Added text file: %s\n" ,full_path .c_str ())); 603 604if (++ count >=FILE_GLOB_MAX_RESULTS ) { 605 ui::show_error (string_format ("Maximum number of globbed files allowed (%zu) reached." ,FILE_GLOB_MAX_RESULTS )); 606break ; 607 } 608 } 609continue ; 610 }else { 611// not a command 612cur_msg += buffer ; 613 } 614 615// generate response 616if (add_user_msg ) { 617push_user_message (cur_msg ); 618write_output_file (string_format ("User:\n%s\n\n" ,cur_msg .c_str ())); 619cur_msg .clear (); 620 } 621 622cli_timings timings ; 623generated_content content ; 624generate_completion (content ,timings ); 625 626json assistant_msg = { 627 {"role" ,"assistant" }, 628 {"content" ,content .content } 629 }; 630if (!content .reasoning .empty ()) { 631assistant_msg ["reasoning_content" ]= content .reasoning ; 632 } 633impl -> messages .push_back (std::move (assistant_msg )); 634 635if (output_file ) { 636 std::string out_content = "Assistant:\n" ; 637if (!content .reasoning .empty ()) { 638out_content += "[Start thinking]\n\n" ; 639out_content += content .reasoning ; 640out_content += "[End thinking]\n\n" ; 641 } 642out_content += content .content ; 643if (!out_content .empty ()&& out_content .back ()!= '\n' ) { 644out_content += "\n" ; 645 } 646out_content += "\n" ; 647write_output_file (out_content ); 648 } 649 650if (params .show_timings ) { 651 ui::show_info (string_format ( 652"\n[ Prompt: %.1f t/s | Generation: %.1f t/s ]" , 653timings .prompt_per_second , 654timings .predicted_per_second 655 )); 656 } 657 658if (params .single_turn ) { 659break ; 660 } 661 } 662 663 ui::show_message ("\n\nExiting..." ); 664 665return 0 ; 666} 667 668void cli_context::shutdown () { 669if (server ) { 670server -> stop (); 671server .reset (); 672 } 673if (output_file ) { 674output_file -> close (); 675output_file .reset (); 676 } 677}