Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit 01a5d76

Browse files
committed
add: cli API
1 parent cdfda3a commit 01a5d76

File tree

10 files changed

+300
-14
lines changed

10 files changed

+300
-14
lines changed

engine/cli/command_line_parser.cc

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
#include <memory>
33
#include <optional>
44
#include <string>
5+
#include <unordered_map>
6+
#include "commands/config_get_cmd.h"
7+
#include "commands/config_upd_cmd.h"
58
#include "commands/cortex_upd_cmd.h"
69
#include "commands/engine_get_cmd.h"
710
#include "commands/engine_install_cmd.h"
@@ -31,6 +34,7 @@ constexpr const auto kInferenceGroup = "Inference";
3134
constexpr const auto kModelsGroup = "Models";
3235
constexpr const auto kEngineGroup = "Engines";
3336
constexpr const auto kSystemGroup = "Server";
37+
constexpr const auto kConfigGroup = "Configurations";
3438
constexpr const auto kSubcommands = "Subcommands";
3539
} // namespace
3640

@@ -57,6 +61,8 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) {
5761

5862
SetupSystemCommands();
5963

64+
SetupConfigsCommands();
65+
6066
app_.add_flag("--verbose", log_verbose, "Get verbose logs");
6167

6268
// Logic is handled in main.cc, just for cli helper command
@@ -301,6 +307,62 @@ void CommandLineParser::SetupModelCommands() {
301307
});
302308
}
303309

310+
void CommandLineParser::SetupConfigsCommands() {
311+
auto config_cmd =
312+
app_.add_subcommand("config", "Subcommands for managing configurations");
313+
config_cmd->usage(
314+
"Usage:\n" + commands::GetCortexBinary() +
315+
" config status for listing all API server configuration.\n" +
316+
commands::GetCortexBinary() +
317+
" config --cors [on/off] to toggle CORS.\n" +
318+
commands::GetCortexBinary() +
319+
" config --allowed_origins [comma separated origin] to set a list of "
320+
"allowed origin");
321+
config_cmd->group(kConfigGroup);
322+
auto config_status_cmd =
323+
config_cmd->add_subcommand("status", "Print all configurations");
324+
config_status_cmd->callback([this] {
325+
if (std::exchange(executed_, true))
326+
return;
327+
commands::ConfigGetCmd().Exec(cml_data_.config.apiServerHost,
328+
std::stoi(cml_data_.config.apiServerPort));
329+
});
330+
331+
// TODO: this can be improved
332+
std::vector<std::string> avai_opts{"cors", "allowed_origins"};
333+
std::unordered_map<std::string, std::string> description{
334+
{"cors", "[on/off] Toggling CORS."},
335+
{"allowed_origins",
336+
"Allowed origins for CORS. Comma separated. E.g. "
337+
"http://localhost,https://cortex.so"}};
338+
for (const auto& opt : avai_opts) {
339+
std::string option = "--" + opt;
340+
config_cmd->add_option(option, config_update_opts_[opt], description[opt])
341+
->expected(0, 1)
342+
->default_str("*");
343+
}
344+
345+
config_cmd->callback([this, config_cmd] {
346+
if (std::exchange(executed_, true))
347+
return;
348+
349+
auto is_empty = true;
350+
for (const auto& [key, value] : config_update_opts_) {
351+
if (!value.empty()) {
352+
is_empty = false;
353+
break;
354+
}
355+
}
356+
if (is_empty) {
357+
CLI_LOG(config_cmd->help());
358+
return;
359+
}
360+
commands::ConfigUpdCmd().Exec(cml_data_.config.apiServerHost,
361+
std::stoi(cml_data_.config.apiServerPort),
362+
config_update_opts_);
363+
});
364+
}
365+
304366
void CommandLineParser::SetupEngineCommands() {
305367
auto engines_cmd =
306368
app_.add_subcommand("engines", "Subcommands for managing engines");
@@ -339,7 +401,7 @@ void CommandLineParser::SetupEngineCommands() {
339401
CLI_LOG(install_cmd->help());
340402
}
341403
});
342-
for (auto& engine : engine_service_.kSupportEngines) {
404+
for (const auto& engine : engine_service_.kSupportEngines) {
343405
std::string engine_name{engine};
344406
EngineInstall(install_cmd, engine_name, cml_data_.engine_version,
345407
cml_data_.engine_src);

engine/cli/command_line_parser.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <memory>
4+
#include <unordered_map>
45
#include "CLI/CLI.hpp"
56
#include "services/engine_service.h"
67
#include "services/model_service.h"
@@ -22,6 +23,8 @@ class CommandLineParser {
2223

2324
void SetupSystemCommands();
2425

26+
void SetupConfigsCommands();
27+
2528
void EngineInstall(CLI::App* parent, const std::string& engine_name,
2629
std::string& version, std::string& src);
2730

@@ -62,5 +65,6 @@ class CommandLineParser {
6265
std::unordered_map<std::string, std::string> model_update_options;
6366
};
6467
CmlData cml_data_;
68+
std::unordered_map<std::string, std::string> config_update_opts_;
6569
bool executed_ = false;
6670
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#include "config_get_cmd.h"
2+
#include <tabulate/table.hpp>
3+
#include "commands/server_start_cmd.h"
4+
#include "utils/curl_utils.h"
5+
#include "utils/logging_utils.h"
6+
#include "utils/url_parser.h"
7+
8+
void commands::ConfigGetCmd::Exec(const std::string& host, int port) {
9+
// Start server if server is not started yet
10+
if (!commands::IsServerAlive(host, port)) {
11+
CLI_LOG("Starting server ...");
12+
commands::ServerStartCmd ssc;
13+
if (!ssc.Exec(host, port)) {
14+
return;
15+
}
16+
}
17+
auto url = url_parser::Url{
18+
.protocol = "http",
19+
.host = host + ":" + std::to_string(port),
20+
.pathParams = {"v1", "configs"},
21+
};
22+
23+
auto get_config_result = curl_utils::SimpleGetJson(url.ToFullPath());
24+
if (get_config_result.has_error()) {
25+
CLI_LOG_ERROR(
26+
"Failed to get configurations: " << get_config_result.error());
27+
return;
28+
}
29+
30+
auto json_value = get_config_result.value();
31+
tabulate::Table table;
32+
table.add_row({"Config name", "Value"});
33+
34+
for (const auto& key : json_value.getMemberNames()) {
35+
if (json_value[key].isArray()) {
36+
for (const auto& value : json_value[key]) {
37+
table.add_row({key, value.asString()});
38+
}
39+
} else {
40+
table.add_row({key, json_value[key].asString()});
41+
}
42+
}
43+
44+
std::cout << table << std::endl;
45+
return;
46+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#pragma once
2+
3+
#include <string>
4+
5+
namespace commands {
6+
class ConfigGetCmd {
7+
public:
8+
void Exec(const std::string& host, int port);
9+
};
10+
} // namespace commands
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#include "config_upd_cmd.h"
2+
#include "commands/server_start_cmd.h"
3+
#include "utils/curl_utils.h"
4+
#include "utils/logging_utils.h"
5+
#include "utils/string_utils.h"
6+
#include "utils/url_parser.h"
7+
8+
namespace {
9+
const std::vector<std::string> config_keys{"cors", "allowed_origins"};
10+
11+
inline Json::Value NormalizeJson(
12+
const std::unordered_map<std::string, std::string> options) {
13+
Json::Value root;
14+
for (const auto& [key, value] : options) {
15+
if (std::find(config_keys.begin(), config_keys.end(), key) ==
16+
config_keys.end()) {
17+
continue;
18+
}
19+
20+
if (key == "cors") {
21+
if (string_utils::EqualsIgnoreCase("on", value)) {
22+
root["cors"] = true;
23+
} else if (string_utils::EqualsIgnoreCase("off", value)) {
24+
root["cors"] = false;
25+
}
26+
} else if (key == "allowed_origins") {
27+
auto origins = string_utils::SplitBy(value, ",");
28+
Json::Value origin_array(Json::arrayValue);
29+
for (const auto& origin : origins) {
30+
origin_array.append(origin);
31+
}
32+
root[key] = origin_array;
33+
}
34+
}
35+
36+
CTL_DBG("Normalized config update request: " << root.toStyledString());
37+
38+
return root;
39+
}
40+
}; // namespace
41+
42+
void commands::ConfigUpdCmd::Exec(
43+
const std::string& host, int port,
44+
const std::unordered_map<std::string, std::string>& options) {
45+
if (!commands::IsServerAlive(host, port)) {
46+
CLI_LOG("Starting server ...");
47+
commands::ServerStartCmd ssc;
48+
if (!ssc.Exec(host, port)) {
49+
return;
50+
}
51+
}
52+
53+
auto url = url_parser::Url{
54+
.protocol = "http",
55+
.host = host + ":" + std::to_string(port),
56+
.pathParams = {"v1", "configs"},
57+
};
58+
59+
auto json = NormalizeJson(options);
60+
if (json.empty()) {
61+
CLI_LOG_ERROR("Invalid configuration options provided");
62+
return;
63+
}
64+
65+
auto update_cnf_result =
66+
curl_utils::SimplePatch(url.ToFullPath(), json.toStyledString());
67+
if (update_cnf_result.has_error()) {
68+
CLI_LOG_ERROR(update_cnf_result.error());
69+
return;
70+
}
71+
72+
CLI_LOG("Configuration updated successfully!");
73+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <unordered_map>
5+
6+
namespace commands {
7+
class ConfigUpdCmd {
8+
public:
9+
void Exec(const std::string& host, int port,
10+
const std::unordered_map<std::string, std::string>& options);
11+
};
12+
} // namespace commands

engine/controllers/configs.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ void Configs::GetConfigurations(
66
auto get_config_result = config_service_->GetApiServerConfiguration();
77
if (get_config_result.has_error()) {
88
Json::Value error_json;
9-
error_json["error"] = get_config_result.error();
9+
error_json["message"] = get_config_result.error();
1010
auto resp = drogon::HttpResponse::newHttpJsonResponse(error_json);
1111
resp->setStatusCode(drogon::k400BadRequest);
1212
callback(resp);
@@ -24,9 +24,9 @@ void Configs::UpdateConfigurations(
2424
const HttpRequestPtr& req,
2525
std::function<void(const HttpResponsePtr&)>&& callback) {
2626
auto json_body = req->getJsonObject();
27-
if (!json_body) {
27+
if (json_body == nullptr) {
2828
Json::Value error_json;
29-
error_json["error"] = "Configuration must be provided via JSON body";
29+
error_json["message"] = "Configuration must be provided via JSON body";
3030
auto resp = drogon::HttpResponse::newHttpJsonResponse(error_json);
3131
resp->setStatusCode(drogon::k400BadRequest);
3232
callback(resp);
@@ -36,7 +36,7 @@ void Configs::UpdateConfigurations(
3636
config_service_->UpdateApiServerConfiguration(*json_body);
3737
if (update_config_result.has_error()) {
3838
Json::Value error_json;
39-
error_json["error"] = update_config_result.error();
39+
error_json["message"] = update_config_result.error();
4040
auto resp = drogon::HttpResponse::newHttpJsonResponse(error_json);
4141
resp->setStatusCode(drogon::k400BadRequest);
4242
callback(resp);

engine/test/components/test_string_utils.cc

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ TEST_F(StringUtilsTestSuite, ParsePrompt) {
2323

2424
TEST_F(StringUtilsTestSuite, TestSplitBy) {
2525
auto input = "this is a test";
26-
std::string delimiter{' '};
27-
auto result = SplitBy(input, delimiter);
26+
auto result = SplitBy(input, " ");
2827

2928
EXPECT_EQ(result.size(), 4);
3029
EXPECT_EQ(result[0], "this");
@@ -35,16 +34,14 @@ TEST_F(StringUtilsTestSuite, TestSplitBy) {
3534

3635
TEST_F(StringUtilsTestSuite, TestSplitByWithEmptyString) {
3736
auto input = "";
38-
std::string delimiter{' '};
39-
auto result = SplitBy(input, delimiter);
37+
auto result = SplitBy(input, " ");
4038

4139
EXPECT_EQ(result.size(), 0);
4240
}
4341

4442
TEST_F(StringUtilsTestSuite, TestSplitModelHandle) {
4543
auto input = "cortexso/tinyllama";
46-
std::string delimiter{'/'};
47-
auto result = SplitBy(input, delimiter);
44+
auto result = SplitBy(input, "/");
4845

4946
EXPECT_EQ(result.size(), 2);
5047
EXPECT_EQ(result[0], "cortexso");
@@ -53,13 +50,20 @@ TEST_F(StringUtilsTestSuite, TestSplitModelHandle) {
5350

5451
TEST_F(StringUtilsTestSuite, TestSplitModelHandleWithEmptyModelName) {
5552
auto input = "cortexso/";
56-
std::string delimiter{'/'};
57-
auto result = SplitBy(input, delimiter);
53+
auto result = SplitBy(input, "/");
5854

5955
EXPECT_EQ(result.size(), 1);
6056
EXPECT_EQ(result[0], "cortexso");
6157
}
6258

59+
TEST_F(StringUtilsTestSuite, TestSplitIfNotContainDelimiter) {
60+
auto input = "https://cortex.so";
61+
auto result = SplitBy(input, ",");
62+
63+
EXPECT_EQ(result.size(), 1);
64+
EXPECT_EQ(result[0], "https://cortex.so");
65+
}
66+
6367
TEST_F(StringUtilsTestSuite, TestStartsWith) {
6468
auto input = "this is a test";
6569
auto prefix = "this";

0 commit comments

Comments
 (0)