1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@ managed_components/
|
||||
main/assets/lang_config.h
|
||||
main/mmap_generate_emoji.h
|
||||
mmap_generate_*.h
|
||||
mimi_secrets.h
|
||||
|
||||
# IDE / Editor
|
||||
.vscode/
|
||||
|
||||
@@ -102,6 +102,24 @@ static int cmd_set_model(int argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- set_model_provider command --- */
|
||||
static struct {
|
||||
struct arg_str *provider;
|
||||
struct arg_end *end;
|
||||
} provider_args;
|
||||
|
||||
static int cmd_set_model_provider(int argc, char **argv)
|
||||
{
|
||||
int nerrors = arg_parse(argc, argv, (void **)&provider_args);
|
||||
if (nerrors != 0) {
|
||||
arg_print_errors(stderr, provider_args.end, argv[0]);
|
||||
return 1;
|
||||
}
|
||||
llm_set_provider(provider_args.provider->sval[0]);
|
||||
printf("Model provider set.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- memory_read command --- */
|
||||
static int cmd_memory_read(int argc, char **argv)
|
||||
{
|
||||
@@ -223,6 +241,15 @@ static int cmd_set_search_key(int argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- wifi_scan command --- */
|
||||
static int cmd_wifi_scan(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
wifi_manager_scan_and_print();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- config_show command --- */
|
||||
static void print_config(const char *label, const char *ns, const char *key,
|
||||
const char *build_val, bool mask)
|
||||
@@ -263,6 +290,7 @@ static int cmd_config_show(int argc, char **argv)
|
||||
print_config("TG Token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN, MIMI_SECRET_TG_TOKEN, true);
|
||||
print_config("API Key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_API_KEY, true);
|
||||
print_config("Model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL, false);
|
||||
print_config("Provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER, MIMI_SECRET_MODEL_PROVIDER, false);
|
||||
print_config("Proxy Host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST, MIMI_SECRET_PROXY_HOST, false);
|
||||
print_config("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT, false);
|
||||
print_config("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true);
|
||||
@@ -332,6 +360,14 @@ esp_err_t serial_cli_init(void)
|
||||
};
|
||||
esp_console_cmd_register(&wifi_status_cmd);
|
||||
|
||||
/* wifi_scan */
|
||||
esp_console_cmd_t wifi_scan_cmd = {
|
||||
.command = "wifi_scan",
|
||||
.help = "Scan and list nearby WiFi APs",
|
||||
.func = &cmd_wifi_scan,
|
||||
};
|
||||
esp_console_cmd_register(&wifi_scan_cmd);
|
||||
|
||||
/* set_tg_token */
|
||||
tg_token_args.token = arg_str1(NULL, NULL, "<token>", "Telegram bot token");
|
||||
tg_token_args.end = arg_end(1);
|
||||
@@ -344,11 +380,11 @@ esp_err_t serial_cli_init(void)
|
||||
esp_console_cmd_register(&tg_token_cmd);
|
||||
|
||||
/* set_api_key */
|
||||
api_key_args.key = arg_str1(NULL, NULL, "<key>", "Anthropic API key");
|
||||
api_key_args.key = arg_str1(NULL, NULL, "<key>", "LLM API key");
|
||||
api_key_args.end = arg_end(1);
|
||||
esp_console_cmd_t api_key_cmd = {
|
||||
.command = "set_api_key",
|
||||
.help = "Set Claude API key",
|
||||
.help = "Set LLM API key",
|
||||
.func = &cmd_set_api_key,
|
||||
.argtable = &api_key_args,
|
||||
};
|
||||
@@ -365,6 +401,17 @@ esp_err_t serial_cli_init(void)
|
||||
};
|
||||
esp_console_cmd_register(&model_cmd);
|
||||
|
||||
/* set_model_provider */
|
||||
provider_args.provider = arg_str1(NULL, NULL, "<provider>", "Model provider (anthropic|openai)");
|
||||
provider_args.end = arg_end(1);
|
||||
esp_console_cmd_t provider_cmd = {
|
||||
.command = "set_model_provider",
|
||||
.help = "Set LLM model provider (default: " MIMI_LLM_PROVIDER_DEFAULT ")",
|
||||
.func = &cmd_set_model_provider,
|
||||
.argtable = &provider_args,
|
||||
};
|
||||
esp_console_cmd_register(&provider_cmd);
|
||||
|
||||
/* memory_read */
|
||||
esp_console_cmd_t mem_read_cmd = {
|
||||
.command = "memory_read",
|
||||
|
||||
@@ -15,6 +15,17 @@ static const char *TAG = "llm";
|
||||
|
||||
static char s_api_key[128] = {0};
|
||||
static char s_model[64] = MIMI_LLM_DEFAULT_MODEL;
|
||||
static char s_provider[16] = MIMI_LLM_PROVIDER_DEFAULT;
|
||||
|
||||
static void safe_copy(char *dst, size_t dst_size, const char *src)
|
||||
{
|
||||
if (!dst || dst_size == 0) return;
|
||||
if (!src) {
|
||||
dst[0] = '\0';
|
||||
return;
|
||||
}
|
||||
snprintf(dst, dst_size, "%s", src);
|
||||
}
|
||||
|
||||
/* ── Response buffer ──────────────────────────────────────────── */
|
||||
|
||||
@@ -67,16 +78,41 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ── Provider helpers ──────────────────────────────────────────── */
|
||||
|
||||
static bool provider_is_openai(void)
|
||||
{
|
||||
return strcmp(s_provider, "openai") == 0;
|
||||
}
|
||||
|
||||
static const char *llm_api_url(void)
|
||||
{
|
||||
return provider_is_openai() ? MIMI_OPENAI_API_URL : MIMI_LLM_API_URL;
|
||||
}
|
||||
|
||||
static const char *llm_api_host(void)
|
||||
{
|
||||
return provider_is_openai() ? "api.openai.com" : "api.anthropic.com";
|
||||
}
|
||||
|
||||
static const char *llm_api_path(void)
|
||||
{
|
||||
return provider_is_openai() ? "/v1/chat/completions" : "/v1/messages";
|
||||
}
|
||||
|
||||
/* ── Init ─────────────────────────────────────────────────────── */
|
||||
|
||||
esp_err_t llm_proxy_init(void)
|
||||
{
|
||||
/* Start with build-time defaults */
|
||||
if (MIMI_SECRET_API_KEY[0] != '\0') {
|
||||
strncpy(s_api_key, MIMI_SECRET_API_KEY, sizeof(s_api_key) - 1);
|
||||
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_API_KEY);
|
||||
}
|
||||
if (MIMI_SECRET_MODEL[0] != '\0') {
|
||||
strncpy(s_model, MIMI_SECRET_MODEL, sizeof(s_model) - 1);
|
||||
safe_copy(s_model, sizeof(s_model), MIMI_SECRET_MODEL);
|
||||
}
|
||||
if (MIMI_SECRET_MODEL_PROVIDER[0] != '\0') {
|
||||
safe_copy(s_provider, sizeof(s_provider), MIMI_SECRET_MODEL_PROVIDER);
|
||||
}
|
||||
|
||||
/* NVS overrides take highest priority (set via CLI) */
|
||||
@@ -85,18 +121,23 @@ esp_err_t llm_proxy_init(void)
|
||||
char tmp[128] = {0};
|
||||
size_t len = sizeof(tmp);
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_API_KEY, tmp, &len) == ESP_OK && tmp[0]) {
|
||||
strncpy(s_api_key, tmp, sizeof(s_api_key) - 1);
|
||||
safe_copy(s_api_key, sizeof(s_api_key), tmp);
|
||||
}
|
||||
len = sizeof(tmp);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_MODEL, tmp, &len) == ESP_OK && tmp[0]) {
|
||||
strncpy(s_model, tmp, sizeof(s_model) - 1);
|
||||
safe_copy(s_model, sizeof(s_model), tmp);
|
||||
}
|
||||
len = sizeof(tmp);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, tmp, &len) == ESP_OK && tmp[0]) {
|
||||
safe_copy(s_provider, sizeof(s_provider), tmp);
|
||||
}
|
||||
nvs_close(nvs);
|
||||
}
|
||||
|
||||
if (s_api_key[0]) {
|
||||
ESP_LOGI(TAG, "LLM proxy initialized (model: %s)", s_model);
|
||||
ESP_LOGI(TAG, "LLM proxy initialized (provider: %s, model: %s)", s_provider, s_model);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No API key. Use CLI: set_api_key <KEY>");
|
||||
}
|
||||
@@ -108,7 +149,7 @@ esp_err_t llm_proxy_init(void)
|
||||
static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out_status)
|
||||
{
|
||||
esp_http_client_config_t config = {
|
||||
.url = MIMI_LLM_API_URL,
|
||||
.url = llm_api_url(),
|
||||
.event_handler = http_event_handler,
|
||||
.user_data = rb,
|
||||
.timeout_ms = 120 * 1000,
|
||||
@@ -122,8 +163,16 @@ static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out
|
||||
|
||||
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
if (provider_is_openai()) {
|
||||
if (s_api_key[0]) {
|
||||
char auth[192];
|
||||
snprintf(auth, sizeof(auth), "Bearer %s", s_api_key);
|
||||
esp_http_client_set_header(client, "Authorization", auth);
|
||||
}
|
||||
} else {
|
||||
esp_http_client_set_header(client, "x-api-key", s_api_key);
|
||||
esp_http_client_set_header(client, "anthropic-version", MIMI_LLM_API_VERSION);
|
||||
}
|
||||
esp_http_client_set_post_field(client, post_data, strlen(post_data));
|
||||
|
||||
esp_err_t err = esp_http_client_perform(client);
|
||||
@@ -136,20 +185,32 @@ static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out
|
||||
|
||||
static esp_err_t llm_http_via_proxy(const char *post_data, resp_buf_t *rb, int *out_status)
|
||||
{
|
||||
proxy_conn_t *conn = proxy_conn_open("api.anthropic.com", 443, 30000);
|
||||
proxy_conn_t *conn = proxy_conn_open(llm_api_host(), 443, 30000);
|
||||
if (!conn) return ESP_ERR_HTTP_CONNECT;
|
||||
|
||||
int body_len = strlen(post_data);
|
||||
char header[512];
|
||||
int hlen = snprintf(header, sizeof(header),
|
||||
"POST /v1/messages HTTP/1.1\r\n"
|
||||
"Host: api.anthropic.com\r\n"
|
||||
int hlen = 0;
|
||||
if (provider_is_openai()) {
|
||||
hlen = snprintf(header, sizeof(header),
|
||||
"POST %s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Authorization: Bearer %s\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: close\r\n\r\n",
|
||||
llm_api_path(), llm_api_host(), s_api_key, body_len);
|
||||
} else {
|
||||
hlen = snprintf(header, sizeof(header),
|
||||
"POST %s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"x-api-key: %s\r\n"
|
||||
"anthropic-version: %s\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: close\r\n\r\n",
|
||||
s_api_key, MIMI_LLM_API_VERSION, body_len);
|
||||
llm_api_path(), llm_api_host(), s_api_key, MIMI_LLM_API_VERSION, body_len);
|
||||
}
|
||||
|
||||
if (proxy_conn_write(conn, header, hlen) < 0 ||
|
||||
proxy_conn_write(conn, post_data, body_len) < 0) {
|
||||
@@ -199,7 +260,7 @@ static esp_err_t llm_http_call(const char *post_data, resp_buf_t *rb, int *out_s
|
||||
|
||||
/* ── Parse text from JSON response ────────────────────────────── */
|
||||
|
||||
static void extract_text(cJSON *root, char *buf, size_t size)
|
||||
static void extract_text_anthropic(cJSON *root, char *buf, size_t size)
|
||||
{
|
||||
buf[0] = '\0';
|
||||
cJSON *content = cJSON_GetObjectItem(root, "content");
|
||||
@@ -220,6 +281,190 @@ static void extract_text(cJSON *root, char *buf, size_t size)
|
||||
buf[off] = '\0';
|
||||
}
|
||||
|
||||
static void extract_text_openai(cJSON *root, char *buf, size_t size)
|
||||
{
|
||||
buf[0] = '\0';
|
||||
cJSON *choices = cJSON_GetObjectItem(root, "choices");
|
||||
if (!choices || !cJSON_IsArray(choices)) return;
|
||||
cJSON *choice0 = cJSON_GetArrayItem(choices, 0);
|
||||
if (!choice0) return;
|
||||
cJSON *message = cJSON_GetObjectItem(choice0, "message");
|
||||
if (!message) return;
|
||||
cJSON *content = cJSON_GetObjectItem(message, "content");
|
||||
if (!content || !cJSON_IsString(content)) return;
|
||||
strncpy(buf, content->valuestring, size - 1);
|
||||
buf[size - 1] = '\0';
|
||||
}
|
||||
|
||||
static cJSON *convert_tools_openai(const char *tools_json)
|
||||
{
|
||||
if (!tools_json) return NULL;
|
||||
cJSON *arr = cJSON_Parse(tools_json);
|
||||
if (!arr || !cJSON_IsArray(arr)) {
|
||||
cJSON_Delete(arr);
|
||||
return NULL;
|
||||
}
|
||||
cJSON *out = cJSON_CreateArray();
|
||||
cJSON *tool;
|
||||
cJSON_ArrayForEach(tool, arr) {
|
||||
cJSON *name = cJSON_GetObjectItem(tool, "name");
|
||||
cJSON *desc = cJSON_GetObjectItem(tool, "description");
|
||||
cJSON *schema = cJSON_GetObjectItem(tool, "input_schema");
|
||||
if (!name || !cJSON_IsString(name)) continue;
|
||||
|
||||
cJSON *func = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(func, "name", name->valuestring);
|
||||
if (desc && cJSON_IsString(desc)) {
|
||||
cJSON_AddStringToObject(func, "description", desc->valuestring);
|
||||
}
|
||||
if (schema) {
|
||||
cJSON_AddItemToObject(func, "parameters", cJSON_Duplicate(schema, 1));
|
||||
}
|
||||
|
||||
cJSON *wrap = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(wrap, "type", "function");
|
||||
cJSON_AddItemToObject(wrap, "function", func);
|
||||
cJSON_AddItemToArray(out, wrap);
|
||||
}
|
||||
cJSON_Delete(arr);
|
||||
return out;
|
||||
}
|
||||
|
||||
static cJSON *convert_messages_openai(const char *system_prompt, cJSON *messages)
|
||||
{
|
||||
cJSON *out = cJSON_CreateArray();
|
||||
if (system_prompt && system_prompt[0]) {
|
||||
cJSON *sys = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(sys, "role", "system");
|
||||
cJSON_AddStringToObject(sys, "content", system_prompt);
|
||||
cJSON_AddItemToArray(out, sys);
|
||||
}
|
||||
|
||||
if (!messages || !cJSON_IsArray(messages)) return out;
|
||||
|
||||
cJSON *msg;
|
||||
cJSON_ArrayForEach(msg, messages) {
|
||||
cJSON *role = cJSON_GetObjectItem(msg, "role");
|
||||
cJSON *content = cJSON_GetObjectItem(msg, "content");
|
||||
if (!role || !cJSON_IsString(role)) continue;
|
||||
|
||||
if (content && cJSON_IsString(content)) {
|
||||
cJSON *m = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(m, "role", role->valuestring);
|
||||
cJSON_AddStringToObject(m, "content", content->valuestring);
|
||||
cJSON_AddItemToArray(out, m);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!content || !cJSON_IsArray(content)) continue;
|
||||
|
||||
if (strcmp(role->valuestring, "assistant") == 0) {
|
||||
cJSON *m = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(m, "role", "assistant");
|
||||
|
||||
/* collect text */
|
||||
char *text_buf = NULL;
|
||||
size_t off = 0;
|
||||
cJSON *block;
|
||||
cJSON *tool_calls = NULL;
|
||||
cJSON_ArrayForEach(block, content) {
|
||||
cJSON *btype = cJSON_GetObjectItem(block, "type");
|
||||
if (btype && cJSON_IsString(btype) && strcmp(btype->valuestring, "text") == 0) {
|
||||
cJSON *text = cJSON_GetObjectItem(block, "text");
|
||||
if (text && cJSON_IsString(text)) {
|
||||
size_t tlen = strlen(text->valuestring);
|
||||
char *tmp = realloc(text_buf, off + tlen + 1);
|
||||
if (tmp) {
|
||||
text_buf = tmp;
|
||||
memcpy(text_buf + off, text->valuestring, tlen);
|
||||
off += tlen;
|
||||
text_buf[off] = '\0';
|
||||
}
|
||||
}
|
||||
} else if (btype && cJSON_IsString(btype) && strcmp(btype->valuestring, "tool_use") == 0) {
|
||||
if (!tool_calls) tool_calls = cJSON_CreateArray();
|
||||
cJSON *id = cJSON_GetObjectItem(block, "id");
|
||||
cJSON *name = cJSON_GetObjectItem(block, "name");
|
||||
cJSON *input = cJSON_GetObjectItem(block, "input");
|
||||
if (!name || !cJSON_IsString(name)) continue;
|
||||
|
||||
cJSON *tc = cJSON_CreateObject();
|
||||
if (id && cJSON_IsString(id)) {
|
||||
cJSON_AddStringToObject(tc, "id", id->valuestring);
|
||||
}
|
||||
cJSON_AddStringToObject(tc, "type", "function");
|
||||
cJSON *func = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(func, "name", name->valuestring);
|
||||
if (input) {
|
||||
char *args = cJSON_PrintUnformatted(input);
|
||||
if (args) {
|
||||
cJSON_AddStringToObject(func, "arguments", args);
|
||||
free(args);
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(tc, "function", func);
|
||||
cJSON_AddItemToArray(tool_calls, tc);
|
||||
}
|
||||
}
|
||||
if (text_buf) {
|
||||
cJSON_AddStringToObject(m, "content", text_buf);
|
||||
} else {
|
||||
cJSON_AddStringToObject(m, "content", "");
|
||||
}
|
||||
if (tool_calls) {
|
||||
cJSON_AddItemToObject(m, "tool_calls", tool_calls);
|
||||
}
|
||||
cJSON_AddItemToArray(out, m);
|
||||
free(text_buf);
|
||||
} else if (strcmp(role->valuestring, "user") == 0) {
|
||||
/* tool_result blocks become role=tool */
|
||||
cJSON *block;
|
||||
bool has_user_text = false;
|
||||
char *text_buf = NULL;
|
||||
size_t off = 0;
|
||||
cJSON_ArrayForEach(block, content) {
|
||||
cJSON *btype = cJSON_GetObjectItem(block, "type");
|
||||
if (btype && cJSON_IsString(btype) && strcmp(btype->valuestring, "tool_result") == 0) {
|
||||
cJSON *tool_id = cJSON_GetObjectItem(block, "tool_use_id");
|
||||
cJSON *tcontent = cJSON_GetObjectItem(block, "content");
|
||||
if (!tool_id || !cJSON_IsString(tool_id)) continue;
|
||||
cJSON *tm = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(tm, "role", "tool");
|
||||
cJSON_AddStringToObject(tm, "tool_call_id", tool_id->valuestring);
|
||||
if (tcontent && cJSON_IsString(tcontent)) {
|
||||
cJSON_AddStringToObject(tm, "content", tcontent->valuestring);
|
||||
} else {
|
||||
cJSON_AddStringToObject(tm, "content", "");
|
||||
}
|
||||
cJSON_AddItemToArray(out, tm);
|
||||
} else if (btype && cJSON_IsString(btype) && strcmp(btype->valuestring, "text") == 0) {
|
||||
cJSON *text = cJSON_GetObjectItem(block, "text");
|
||||
if (text && cJSON_IsString(text)) {
|
||||
size_t tlen = strlen(text->valuestring);
|
||||
char *tmp = realloc(text_buf, off + tlen + 1);
|
||||
if (tmp) {
|
||||
text_buf = tmp;
|
||||
memcpy(text_buf + off, text->valuestring, tlen);
|
||||
off += tlen;
|
||||
text_buf[off] = '\0';
|
||||
}
|
||||
has_user_text = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (has_user_text) {
|
||||
cJSON *um = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(um, "role", "user");
|
||||
cJSON_AddStringToObject(um, "content", text_buf);
|
||||
cJSON_AddItemToArray(out, um);
|
||||
}
|
||||
free(text_buf);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ── Public: simple chat (backward compat) ────────────────────── */
|
||||
|
||||
esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
||||
@@ -234,8 +479,21 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
||||
cJSON *body = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(body, "model", s_model);
|
||||
cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS);
|
||||
cJSON_AddStringToObject(body, "system", system_prompt);
|
||||
|
||||
if (provider_is_openai()) {
|
||||
cJSON *messages = cJSON_Parse(messages_json);
|
||||
if (!messages) {
|
||||
messages = cJSON_CreateArray();
|
||||
cJSON *msg = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(msg, "role", "user");
|
||||
cJSON_AddStringToObject(msg, "content", messages_json);
|
||||
cJSON_AddItemToArray(messages, msg);
|
||||
}
|
||||
cJSON *openai_msgs = convert_messages_openai(system_prompt, messages);
|
||||
cJSON_Delete(messages);
|
||||
cJSON_AddItemToObject(body, "messages", openai_msgs);
|
||||
} else {
|
||||
cJSON_AddStringToObject(body, "system", system_prompt);
|
||||
cJSON *messages = cJSON_Parse(messages_json);
|
||||
if (messages) {
|
||||
cJSON_AddItemToObject(body, "messages", messages);
|
||||
@@ -247,6 +505,7 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
||||
cJSON_AddItemToArray(arr, msg);
|
||||
cJSON_AddItemToObject(body, "messages", arr);
|
||||
}
|
||||
}
|
||||
|
||||
char *post_data = cJSON_PrintUnformatted(body);
|
||||
cJSON_Delete(body);
|
||||
@@ -255,8 +514,8 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Calling Claude API (model: %s, body: %d bytes)",
|
||||
s_model, (int)strlen(post_data));
|
||||
ESP_LOGI(TAG, "Calling LLM API (provider: %s, model: %s, body: %d bytes)",
|
||||
s_provider, s_model, (int)strlen(post_data));
|
||||
|
||||
resp_buf_t rb;
|
||||
if (resp_buf_init(&rb, MIMI_LLM_STREAM_BUF_SIZE) != ESP_OK) {
|
||||
@@ -294,13 +553,17 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
extract_text(root, response_buf, buf_size);
|
||||
if (provider_is_openai()) {
|
||||
extract_text_openai(root, response_buf, buf_size);
|
||||
} else {
|
||||
extract_text_anthropic(root, response_buf, buf_size);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
|
||||
if (response_buf[0] == '\0') {
|
||||
snprintf(response_buf, buf_size, "No response from Claude API");
|
||||
snprintf(response_buf, buf_size, "No response from LLM API");
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Claude response: %d bytes", (int)strlen(response_buf));
|
||||
ESP_LOGI(TAG, "LLM response: %d bytes", (int)strlen(response_buf));
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
@@ -334,6 +597,19 @@ esp_err_t llm_chat_tools(const char *system_prompt,
|
||||
cJSON *body = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(body, "model", s_model);
|
||||
cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS);
|
||||
|
||||
if (provider_is_openai()) {
|
||||
cJSON *openai_msgs = convert_messages_openai(system_prompt, messages);
|
||||
cJSON_AddItemToObject(body, "messages", openai_msgs);
|
||||
|
||||
if (tools_json) {
|
||||
cJSON *tools = convert_tools_openai(tools_json);
|
||||
if (tools) {
|
||||
cJSON_AddItemToObject(body, "tools", tools);
|
||||
cJSON_AddStringToObject(body, "tool_choice", "auto");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cJSON_AddStringToObject(body, "system", system_prompt);
|
||||
|
||||
/* Deep-copy messages so caller keeps ownership */
|
||||
@@ -347,13 +623,14 @@ esp_err_t llm_chat_tools(const char *system_prompt,
|
||||
cJSON_AddItemToObject(body, "tools", tools);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char *post_data = cJSON_PrintUnformatted(body);
|
||||
cJSON_Delete(body);
|
||||
if (!post_data) return ESP_ERR_NO_MEM;
|
||||
|
||||
ESP_LOGI(TAG, "Calling Claude API with tools (model: %s, body: %d bytes)",
|
||||
s_model, (int)strlen(post_data));
|
||||
ESP_LOGI(TAG, "Calling LLM API with tools (provider: %s, model: %s, body: %d bytes)",
|
||||
s_provider, s_model, (int)strlen(post_data));
|
||||
|
||||
/* HTTP call */
|
||||
resp_buf_t rb;
|
||||
@@ -387,6 +664,60 @@ esp_err_t llm_chat_tools(const char *system_prompt,
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (provider_is_openai()) {
|
||||
cJSON *choices = cJSON_GetObjectItem(root, "choices");
|
||||
cJSON *choice0 = choices && cJSON_IsArray(choices) ? cJSON_GetArrayItem(choices, 0) : NULL;
|
||||
if (choice0) {
|
||||
cJSON *finish = cJSON_GetObjectItem(choice0, "finish_reason");
|
||||
if (finish && cJSON_IsString(finish)) {
|
||||
resp->tool_use = (strcmp(finish->valuestring, "tool_calls") == 0);
|
||||
}
|
||||
|
||||
cJSON *message = cJSON_GetObjectItem(choice0, "message");
|
||||
if (message) {
|
||||
cJSON *content = cJSON_GetObjectItem(message, "content");
|
||||
if (content && cJSON_IsString(content)) {
|
||||
size_t tlen = strlen(content->valuestring);
|
||||
resp->text = calloc(1, tlen + 1);
|
||||
if (resp->text) {
|
||||
memcpy(resp->text, content->valuestring, tlen);
|
||||
resp->text_len = tlen;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON *tool_calls = cJSON_GetObjectItem(message, "tool_calls");
|
||||
if (tool_calls && cJSON_IsArray(tool_calls)) {
|
||||
cJSON *tc;
|
||||
cJSON_ArrayForEach(tc, tool_calls) {
|
||||
if (resp->call_count >= MIMI_MAX_TOOL_CALLS) break;
|
||||
llm_tool_call_t *call = &resp->calls[resp->call_count];
|
||||
cJSON *id = cJSON_GetObjectItem(tc, "id");
|
||||
cJSON *func = cJSON_GetObjectItem(tc, "function");
|
||||
if (id && cJSON_IsString(id)) {
|
||||
strncpy(call->id, id->valuestring, sizeof(call->id) - 1);
|
||||
}
|
||||
if (func) {
|
||||
cJSON *name = cJSON_GetObjectItem(func, "name");
|
||||
cJSON *args = cJSON_GetObjectItem(func, "arguments");
|
||||
if (name && cJSON_IsString(name)) {
|
||||
strncpy(call->name, name->valuestring, sizeof(call->name) - 1);
|
||||
}
|
||||
if (args && cJSON_IsString(args)) {
|
||||
call->input = strdup(args->valuestring);
|
||||
if (call->input) {
|
||||
call->input_len = strlen(call->input);
|
||||
}
|
||||
}
|
||||
}
|
||||
resp->call_count++;
|
||||
}
|
||||
if (resp->call_count > 0) {
|
||||
resp->tool_use = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* stop_reason */
|
||||
cJSON *stop_reason = cJSON_GetObjectItem(root, "stop_reason");
|
||||
if (stop_reason && cJSON_IsString(stop_reason)) {
|
||||
@@ -456,6 +787,7 @@ esp_err_t llm_chat_tools(const char *system_prompt,
|
||||
resp->call_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
@@ -476,7 +808,7 @@ esp_err_t llm_set_api_key(const char *api_key)
|
||||
ESP_ERROR_CHECK(nvs_commit(nvs));
|
||||
nvs_close(nvs);
|
||||
|
||||
strncpy(s_api_key, api_key, sizeof(s_api_key) - 1);
|
||||
safe_copy(s_api_key, sizeof(s_api_key), api_key);
|
||||
ESP_LOGI(TAG, "API key saved");
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -489,7 +821,20 @@ esp_err_t llm_set_model(const char *model)
|
||||
ESP_ERROR_CHECK(nvs_commit(nvs));
|
||||
nvs_close(nvs);
|
||||
|
||||
strncpy(s_model, model, sizeof(s_model) - 1);
|
||||
safe_copy(s_model, sizeof(s_model), model);
|
||||
ESP_LOGI(TAG, "Model set to: %s", s_model);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t llm_set_provider(const char *provider)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
ESP_ERROR_CHECK(nvs_open(MIMI_NVS_LLM, NVS_READWRITE, &nvs));
|
||||
ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_PROVIDER, provider));
|
||||
ESP_ERROR_CHECK(nvs_commit(nvs));
|
||||
nvs_close(nvs);
|
||||
|
||||
safe_copy(s_provider, sizeof(s_provider), provider);
|
||||
ESP_LOGI(TAG, "Provider set to: %s", s_provider);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -13,17 +13,22 @@
|
||||
esp_err_t llm_proxy_init(void);
|
||||
|
||||
/**
|
||||
* Save the Anthropic API key to NVS.
|
||||
* Save the LLM API key to NVS.
|
||||
*/
|
||||
esp_err_t llm_set_api_key(const char *api_key);
|
||||
|
||||
/**
|
||||
* Save the LLM provider to NVS. (e.g. "anthropic", "openai")
|
||||
*/
|
||||
esp_err_t llm_set_provider(const char *provider);
|
||||
|
||||
/**
|
||||
* Save the model identifier to NVS.
|
||||
*/
|
||||
esp_err_t llm_set_model(const char *model);
|
||||
|
||||
/**
|
||||
* Send a chat completion request to Anthropic Messages API (streaming).
|
||||
* Send a chat completion request to the configured LLM API (non-streaming).
|
||||
*
|
||||
* @param system_prompt System prompt string
|
||||
* @param messages_json JSON array of messages: [{"role":"user","content":"..."},...]
|
||||
@@ -54,7 +59,7 @@ typedef struct {
|
||||
void llm_response_free(llm_response_t *resp);
|
||||
|
||||
/**
|
||||
* Send a chat completion request with tools to Anthropic Messages API (streaming).
|
||||
* Send a chat completion request with tools to the configured LLM API (non-streaming).
|
||||
*
|
||||
* @param system_prompt System prompt string
|
||||
* @param messages cJSON array of messages (caller owns)
|
||||
|
||||
@@ -117,6 +117,8 @@ void app_main(void)
|
||||
/* Start WiFi */
|
||||
esp_err_t wifi_err = wifi_manager_start();
|
||||
if (wifi_err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Scanning nearby APs on boot...");
|
||||
wifi_manager_scan_and_print();
|
||||
ESP_LOGI(TAG, "Waiting for WiFi connection...");
|
||||
if (wifi_manager_wait_connected(30000) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip());
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#ifndef MIMI_SECRET_MODEL
|
||||
#define MIMI_SECRET_MODEL ""
|
||||
#endif
|
||||
#ifndef MIMI_SECRET_MODEL_PROVIDER
|
||||
#define MIMI_SECRET_MODEL_PROVIDER "anthropic"
|
||||
#endif
|
||||
#ifndef MIMI_SECRET_PROXY_HOST
|
||||
#define MIMI_SECRET_PROXY_HOST ""
|
||||
#endif
|
||||
@@ -57,8 +60,10 @@
|
||||
|
||||
/* LLM */
|
||||
#define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-5"
|
||||
#define MIMI_LLM_PROVIDER_DEFAULT "anthropic"
|
||||
#define MIMI_LLM_MAX_TOKENS 4096
|
||||
#define MIMI_LLM_API_URL "https://api.anthropic.com/v1/messages"
|
||||
#define MIMI_OPENAI_API_URL "https://api.openai.com/v1/chat/completions"
|
||||
#define MIMI_LLM_API_VERSION "2023-06-01"
|
||||
#define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024)
|
||||
|
||||
@@ -101,6 +106,6 @@
|
||||
#define MIMI_NVS_KEY_TG_TOKEN "bot_token"
|
||||
#define MIMI_NVS_KEY_API_KEY "api_key"
|
||||
#define MIMI_NVS_KEY_MODEL "model"
|
||||
#define MIMI_NVS_KEY_PROVIDER "provider"
|
||||
#define MIMI_NVS_KEY_PROXY_HOST "host"
|
||||
#define MIMI_NVS_KEY_PROXY_PORT "port"
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
/* Anthropic API */
|
||||
#define MIMI_SECRET_API_KEY ""
|
||||
#define MIMI_SECRET_MODEL ""
|
||||
#define MIMI_SECRET_MODEL_PROVIDER "anthropic"
|
||||
|
||||
/* HTTP Proxy (leave empty or set both) */
|
||||
#define MIMI_SECRET_PROXY_HOST ""
|
||||
|
||||
@@ -16,6 +16,23 @@ static int s_retry_count = 0;
|
||||
static char s_ip_str[16] = "0.0.0.0";
|
||||
static bool s_connected = false;
|
||||
|
||||
static const char *wifi_reason_to_str(wifi_err_reason_t reason)
|
||||
{
|
||||
switch (reason) {
|
||||
case WIFI_REASON_AUTH_EXPIRE: return "AUTH_EXPIRE";
|
||||
case WIFI_REASON_AUTH_FAIL: return "AUTH_FAIL";
|
||||
case WIFI_REASON_ASSOC_EXPIRE: return "ASSOC_EXPIRE";
|
||||
case WIFI_REASON_ASSOC_FAIL: return "ASSOC_FAIL";
|
||||
case WIFI_REASON_HANDSHAKE_TIMEOUT: return "HANDSHAKE_TIMEOUT";
|
||||
case WIFI_REASON_NO_AP_FOUND: return "NO_AP_FOUND";
|
||||
case WIFI_REASON_BEACON_TIMEOUT: return "BEACON_TIMEOUT";
|
||||
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: return "4WAY_HANDSHAKE_TIMEOUT";
|
||||
case WIFI_REASON_MIC_FAILURE: return "MIC_FAILURE";
|
||||
case WIFI_REASON_CONNECTION_FAIL: return "CONNECTION_FAIL";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static void event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
@@ -23,6 +40,10 @@ static void event_handler(void *arg, esp_event_base_t event_base,
|
||||
esp_wifi_connect();
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
s_connected = false;
|
||||
wifi_event_sta_disconnected_t *disc = (wifi_event_sta_disconnected_t *)event_data;
|
||||
if (disc) {
|
||||
ESP_LOGW(TAG, "Disconnected (reason=%d:%s)", disc->reason, wifi_reason_to_str(disc->reason));
|
||||
}
|
||||
if (s_retry_count < MIMI_WIFI_MAX_RETRY) {
|
||||
/* Exponential backoff: 1s, 2s, 4s, 8s, ... capped at 30s */
|
||||
uint32_t delay_ms = MIMI_WIFI_RETRY_BASE_MS << s_retry_count;
|
||||
@@ -148,3 +169,66 @@ EventGroupHandle_t wifi_manager_get_event_group(void)
|
||||
{
|
||||
return s_wifi_event_group;
|
||||
}
|
||||
|
||||
void wifi_manager_scan_and_print(void)
|
||||
{
|
||||
wifi_scan_config_t scan_cfg = {
|
||||
.ssid = NULL,
|
||||
.bssid = NULL,
|
||||
.channel = 0,
|
||||
.show_hidden = true,
|
||||
};
|
||||
|
||||
ESP_LOGI(TAG, "Scanning nearby APs...");
|
||||
|
||||
/* Pause auto-connect to allow scan */
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true /* block */);
|
||||
if (err == ESP_ERR_WIFI_STATE) {
|
||||
/* Try a quick stop/start cycle and scan again */
|
||||
esp_wifi_stop();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
esp_wifi_start();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
err = esp_wifi_scan_start(&scan_cfg, true /* block */);
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Scan failed: %s", esp_err_to_name(err));
|
||||
esp_wifi_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ap_count = 0;
|
||||
esp_wifi_scan_get_ap_num(&ap_count);
|
||||
if (ap_count == 0) {
|
||||
ESP_LOGW(TAG, "No APs found");
|
||||
esp_wifi_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
wifi_ap_record_t *ap_list = calloc(ap_count, sizeof(wifi_ap_record_t));
|
||||
if (!ap_list) {
|
||||
ESP_LOGE(TAG, "Out of memory for AP list");
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ap_max = ap_count;
|
||||
if (esp_wifi_scan_get_ap_records(&ap_max, ap_list) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to get AP records");
|
||||
free(ap_list);
|
||||
esp_wifi_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Found %u APs:", ap_max);
|
||||
for (uint16_t i = 0; i < ap_max; i++) {
|
||||
const wifi_ap_record_t *ap = &ap_list[i];
|
||||
ESP_LOGI(TAG, " [%u] SSID=%s RSSI=%d CH=%d Auth=%d",
|
||||
i + 1, (const char *)ap->ssid, ap->rssi, ap->primary, ap->authmode);
|
||||
}
|
||||
|
||||
free(ap_list);
|
||||
esp_wifi_connect();
|
||||
}
|
||||
|
||||
@@ -44,3 +44,8 @@ esp_err_t wifi_manager_set_credentials(const char *ssid, const char *password);
|
||||
* Get the event group for WiFi state (WIFI_CONNECTED_BIT / WIFI_FAIL_BIT).
|
||||
*/
|
||||
EventGroupHandle_t wifi_manager_get_event_group(void);
|
||||
|
||||
/**
|
||||
* Scan and print nearby APs.
|
||||
*/
|
||||
void wifi_manager_scan_and_print(void);
|
||||
|
||||
Reference in New Issue
Block a user