feat: 增加国内大模型厂商支持(硅基流动、火山方舟)
Some checks failed
Build / idf-build (push) Has been cancelled

- 创建通用提供商架构(llm_provider.h/c)
- 支持四个提供商:Anthropic、OpenAI、SiliconFlow、Volcengine
- 添加提供商特定的API密钥和Base URL配置
- 扩展CLI命令:set_siliconflow_key/url、set_volcengine_key/url
- 更新mimi_secrets.h.example配置模板
- 更新README.md文档说明
- 每个提供商支持独立的NVS存储配置
This commit is contained in:
2026-03-31 19:37:15 +08:00
parent 46e46b0a13
commit 49d3a131b7
8 changed files with 618 additions and 20 deletions

View File

@@ -31,7 +31,7 @@ MimiClaw turns a tiny ESP32-S3 board into a personal AI assistant. Plug it into
![](assets/mimiclaw.png)
You send a message on Telegram. The ESP32-S3 picks it up over WiFi, feeds it into an agent loop — the LLM thinks, calls tools, reads memory — and sends the reply back. Supports both **Anthropic (Claude)** and **OpenAI (GPT)** as providers, switchable at runtime. Everything runs on a single $5 chip with all your data stored locally on flash.
You send a message on Telegram. The ESP32-S3 picks it up over WiFi, feeds it into an agent loop — the LLM thinks, calls tools, reads memory — and sends the reply back. Supports **Anthropic (Claude)**, **OpenAI (GPT)**, **SiliconFlow** (Chinese LLM providers), and **Volcengine** (ByteDance Doubao models) as providers, switchable at runtime. Everything runs on a single $5 chip with all your data stored locally on flash.
## Quick Start
@@ -40,7 +40,7 @@ You send a message on Telegram. The ESP32-S3 picks it up over WiFi, feeds it int
- An **ESP32-S3 dev board** with 16 MB flash and 8 MB PSRAM (e.g. Xiaozhi AI board, ~$10)
- A **USB Type-C cable**
- A **Telegram bot token** — talk to [@BotFather](https://t.me/BotFather) on Telegram to create one
- An **Anthropic API key** — from [console.anthropic.com](https://console.anthropic.com), or an **OpenAI API key** — from [platform.openai.com](https://platform.openai.com)
- An **Anthropic API key** — from [console.anthropic.com](https://console.anthropic.com), or an **OpenAI API key** — from [platform.openai.com](https://platform.openai.com), or a **SiliconFlow API key** — from [siliconflow.cn](https://siliconflow.cn), or a **Volcengine API key** — from [volcengine.com](https://volcengine.com)
### Install
@@ -128,11 +128,19 @@ Edit `main/mimi_secrets.h`:
#define MIMI_SECRET_WIFI_PASS "YourWiFiPassword"
#define MIMI_SECRET_TG_TOKEN "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
#define MIMI_SECRET_API_KEY "sk-ant-api03-xxxxx"
#define MIMI_SECRET_MODEL_PROVIDER "anthropic" // "anthropic" or "openai"
#define MIMI_SECRET_MODEL_PROVIDER "anthropic" // "anthropic", "openai", "siliconflow", or "volcengine"
#define MIMI_SECRET_SEARCH_KEY "" // optional: Brave Search API key
#define MIMI_SECRET_TAVILY_KEY "" // optional: Tavily API key (preferred)
#define MIMI_SECRET_PROXY_HOST "" // optional: e.g. "10.0.0.1"
#define MIMI_SECRET_PROXY_PORT "" // optional: e.g. "7897"
/* Optional: SiliconFlow API (OpenAI-compatible) */
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
/* Optional: Volcengine API (OpenAI-compatible, ByteDance Doubao models) */
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
```
Then build and flash:
@@ -169,12 +177,16 @@ Connect via serial to configure or debug. **Config commands** let you change set
mimi> wifi_set MySSID MyPassword # change WiFi network
mimi> set_tg_token 123456:ABC... # change Telegram bot token
mimi> set_api_key sk-ant-api03-... # change API key (Anthropic or OpenAI)
mimi> set_model_provider openai # switch provider (anthropic|openai)
mimi> set_model_provider openai # switch provider (anthropic|openai|siliconflow|volcengine)
mimi> set_model gpt-4o # change LLM model
mimi> set_proxy 127.0.0.1 7897 # set HTTP proxy
mimi> clear_proxy # remove proxy
mimi> set_search_key BSA... # set Brave Search API key
mimi> set_tavily_key tvly-... # set Tavily API key (preferred)
mimi> set_siliconflow_key sk-... # set SiliconFlow API key
mimi> set_siliconflow_url https://api.siliconflow.cn/v1 # set SiliconFlow Base URL
mimi> set_volcengine_key sk-... # set Volcengine API key
mimi> set_volcengine_url https://ark.cn-beijing.volces.com/api/v3 # set Volcengine Base URL
mimi> config_show # show all config (masked)
mimi> config_reset # clear NVS, revert to build-time defaults
```
@@ -250,7 +262,7 @@ MimiClaw stores everything as plain text files you can read and edit:
## Tools
MimiClaw supports tool calling for both Anthropic and OpenAI — the LLM can call tools during a conversation and loop until the task is done (ReAct pattern).
MimiClaw supports tool calling for all LLM providers — the LLM can call tools during a conversation and loop until the task is done (ReAct pattern). Supported providers include Anthropic (Claude), OpenAI (GPT), SiliconFlow, and Volcengine.
| Tool | Description |
|------|-------------|
@@ -280,7 +292,7 @@ This turns MimiClaw into a proactive assistant — write tasks to `HEARTBEAT.md`
- **OTA updates** — flash new firmware over WiFi, no USB needed
- **Dual-core** — network I/O and AI processing run on separate CPU cores
- **HTTP proxy** — CONNECT tunnel support for restricted networks
- **Multi-provider** — supports both Anthropic (Claude) and OpenAI (GPT), switchable at runtime
- **Multi-provider** — supports Anthropic (Claude), OpenAI (GPT), SiliconFlow, and Volcengine, switchable at runtime
- **Cron scheduler** — the AI can schedule its own recurring and one-shot tasks, persisted across reboots
- **Heartbeat** — periodically checks a task file and prompts the AI to act autonomously
- **Tool use** — ReAct agent loop with tool calling for both providers

View File

@@ -171,6 +171,80 @@ static int cmd_set_model_provider(int argc, char **argv)
return 0;
}
/* --- set_siliconflow_key command --- */
static struct {
struct arg_str *key;
struct arg_end *end;
} siliconflow_key_args;
static int cmd_set_siliconflow_key(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&siliconflow_key_args);
if (nerrors != 0) {
arg_print_errors(stderr, siliconflow_key_args.end, argv[0]);
return 1;
}
llm_set_api_key(siliconflow_key_args.key->sval[0]);
llm_provider_set_api_key("siliconflow", siliconflow_key_args.key->sval[0]);
printf("SiliconFlow API key saved.\n");
return 0;
}
/* --- set_siliconflow_url command --- */
static struct {
struct arg_str *url;
struct arg_end *end;
} siliconflow_url_args;
static int cmd_set_siliconflow_url(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&siliconflow_url_args);
if (nerrors != 0) {
arg_print_errors(stderr, siliconflow_url_args.end, argv[0]);
return 1;
}
llm_set_base_url("siliconflow", siliconflow_url_args.url->sval[0]);
printf("SiliconFlow Base URL saved.\n");
return 0;
}
/* --- set_volcengine_key command --- */
static struct {
struct arg_str *key;
struct arg_end *end;
} volcengine_key_args;
static int cmd_set_volcengine_key(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&volcengine_key_args);
if (nerrors != 0) {
arg_print_errors(stderr, volcengine_key_args.end, argv[0]);
return 1;
}
llm_set_api_key(volcengine_key_args.key->sval[0]);
llm_provider_set_api_key("volcengine", volcengine_key_args.key->sval[0]);
printf("Volcengine API key saved.\n");
return 0;
}
/* --- set_volcengine_url command --- */
static struct {
struct arg_str *url;
struct arg_end *end;
} volcengine_url_args;
static int cmd_set_volcengine_url(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&volcengine_url_args);
if (nerrors != 0) {
arg_print_errors(stderr, volcengine_url_args.end, argv[0]);
return 1;
}
llm_set_base_url("volcengine", volcengine_url_args.url->sval[0]);
printf("Volcengine Base URL saved.\n");
return 0;
}
/* --- memory_read command --- */
static int cmd_memory_read(int argc, char **argv)
{
@@ -563,6 +637,18 @@ static int cmd_config_show(int argc, char **argv)
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);
/* Provider-specific configurations */
printf(" --- Provider-specific configs ---\n");
print_config("Anthropic Key", MIMI_NVS_LLM, MIMI_NVS_KEY_ANTHROPIC_API_KEY, "", true);
print_config("Anthropic URL", MIMI_NVS_LLM, MIMI_NVS_KEY_ANTHROPIC_BASE_URL, "", false);
print_config("OpenAI Key", MIMI_NVS_LLM, MIMI_NVS_KEY_OPENAI_API_KEY, "", true);
print_config("OpenAI URL", MIMI_NVS_LLM, MIMI_NVS_KEY_OPENAI_BASE_URL, "", false);
print_config("SiliconFlow Key", MIMI_NVS_LLM, MIMI_NVS_KEY_SILICONFLOW_API_KEY, "", true);
print_config("SiliconFlow URL", MIMI_NVS_LLM, MIMI_NVS_KEY_SILICONFLOW_BASE_URL, "", false);
print_config("Volcengine Key", MIMI_NVS_LLM, MIMI_NVS_KEY_VOLCENGINE_API_KEY, "", true);
print_config("Volcengine URL", MIMI_NVS_LLM, MIMI_NVS_KEY_VOLCENGINE_BASE_URL, "", false);
print_config("Proxy Host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST, MIMI_SECRET_PROXY_HOST, false);
print_config_u16("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT);
print_config("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true);
@@ -887,7 +973,7 @@ 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.provider = arg_str1(NULL, NULL, "<provider>", "Model provider (anthropic|openai|siliconflow|volcengine)");
provider_args.end = arg_end(1);
esp_console_cmd_t provider_cmd = {
.command = "set_model_provider",
@@ -897,6 +983,50 @@ esp_err_t serial_cli_init(void)
};
esp_console_cmd_register(&provider_cmd);
/* set_siliconflow_key */
siliconflow_key_args.key = arg_str1(NULL, NULL, "<key>", "SiliconFlow API key");
siliconflow_key_args.end = arg_end(1);
esp_console_cmd_t siliconflow_key_cmd = {
.command = "set_siliconflow_key",
.help = "Set SiliconFlow API key",
.func = &cmd_set_siliconflow_key,
.argtable = &siliconflow_key_args,
};
esp_console_cmd_register(&siliconflow_key_cmd);
/* set_siliconflow_url */
siliconflow_url_args.url = arg_str1(NULL, NULL, "<url>", "SiliconFlow Base URL");
siliconflow_url_args.end = arg_end(1);
esp_console_cmd_t siliconflow_url_cmd = {
.command = "set_siliconflow_url",
.help = "Set SiliconFlow Base URL",
.func = &cmd_set_siliconflow_url,
.argtable = &siliconflow_url_args,
};
esp_console_cmd_register(&siliconflow_url_cmd);
/* set_volcengine_key */
volcengine_key_args.key = arg_str1(NULL, NULL, "<key>", "Volcengine API key");
volcengine_key_args.end = arg_end(1);
esp_console_cmd_t volcengine_key_cmd = {
.command = "set_volcengine_key",
.help = "Set Volcengine API key",
.func = &cmd_set_volcengine_key,
.argtable = &volcengine_key_args,
};
esp_console_cmd_register(&volcengine_key_cmd);
/* set_volcengine_url */
volcengine_url_args.url = arg_str1(NULL, NULL, "<url>", "Volcengine Base URL");
volcengine_url_args.end = arg_end(1);
esp_console_cmd_t volcengine_url_cmd = {
.command = "set_volcengine_url",
.help = "Set Volcengine Base URL",
.func = &cmd_set_volcengine_url,
.argtable = &volcengine_url_args,
};
esp_console_cmd_register(&volcengine_url_cmd);
/* skill_list */
esp_console_cmd_t skill_list_cmd = {
.command = "skill_list",

298
main/llm/llm_provider.c Normal file
View File

@@ -0,0 +1,298 @@
#include "llm_provider.h"
#include "mimi_config.h"
#include "nvs.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "esp_log.h"
#include "esp_err.h"
static const char *TAG = "llm_provider";
#define LLM_API_KEY_MAX_LEN 320
/* Provider registry - all supported providers */
const llm_provider_config_t llm_providers[] = {
{
.name = "anthropic",
.default_api_url = MIMI_LLM_API_URL,
.default_host = "api.anthropic.com",
.default_path = "/v1/messages",
.is_openai_compatible = false,
},
{
.name = "openai",
.default_api_url = MIMI_OPENAI_API_URL,
.default_host = "api.openai.com",
.default_path = "/v1/chat/completions",
.is_openai_compatible = true,
},
{
.name = "siliconflow",
.default_api_url = MIMI_SILICONFLOW_API_URL,
.default_host = "api.siliconflow.cn",
.default_path = "/v1/chat/completions",
.is_openai_compatible = true,
},
{
.name = "volcengine",
.default_api_url = MIMI_VOLCENGINE_API_URL,
.default_host = "ark.cn-beijing.volces.com",
.default_path = "/v1/chat/completions",
.is_openai_compatible = true,
},
};
const int llm_provider_count = sizeof(llm_providers) / sizeof(llm_providers[0]);
/* Current provider state */
static const llm_provider_config_t *s_current_provider = &llm_providers[0]; /* Default to anthropic */
static char s_api_key[LLM_API_KEY_MAX_LEN] = {0};
static char s_base_url[256] = {0};
/* Helper function to get NVS key for provider API key */
static const char *get_provider_api_key_nvs_key(const char *provider_name) {
if (strcmp(provider_name, "anthropic") == 0) return MIMI_NVS_KEY_ANTHROPIC_API_KEY;
if (strcmp(provider_name, "openai") == 0) return MIMI_NVS_KEY_OPENAI_API_KEY;
if (strcmp(provider_name, "siliconflow") == 0) return MIMI_NVS_KEY_SILICONFLOW_API_KEY;
if (strcmp(provider_name, "volcengine") == 0) return MIMI_NVS_KEY_VOLCENGINE_API_KEY;
return NULL;
}
/* Helper function to get NVS key for provider Base URL */
static const char *get_provider_base_url_nvs_key(const char *provider_name) {
if (strcmp(provider_name, "anthropic") == 0) return MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
if (strcmp(provider_name, "openai") == 0) return MIMI_NVS_KEY_OPENAI_BASE_URL;
if (strcmp(provider_name, "siliconflow") == 0) return MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
if (strcmp(provider_name, "volcengine") == 0) return MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
return NULL;
}
/* Find provider configuration by name */
const llm_provider_config_t *llm_provider_find(const char *name) {
if (!name) return NULL;
for (int i = 0; i < llm_provider_count; i++) {
if (strcmp(llm_providers[i].name, name) == 0) {
return &llm_providers[i];
}
}
return NULL;
}
/* Get current provider configuration */
const llm_provider_config_t *llm_provider_current(void) {
return s_current_provider;
}
/* Set current provider by name */
void llm_provider_set_current(const char *name) {
const llm_provider_config_t *provider = llm_provider_find(name);
if (provider) {
s_current_provider = provider;
/* Load provider-specific configuration */
llm_provider_init();
ESP_LOGI(TAG, "Current provider set to: %s", name);
} else {
ESP_LOGW(TAG, "Unknown provider: %s", name);
}
}
/* Get current provider name */
const char *llm_provider_current_name(void) {
return s_current_provider ? s_current_provider->name : "unknown";
}
/* Check if current provider is OpenAI-compatible */
bool llm_provider_is_openai_compatible(void) {
return s_current_provider ? s_current_provider->is_openai_compatible : false;
}
/* Get API URL for current provider (with dynamic Base URL support) */
const char *llm_provider_api_url(void) {
if (s_base_url[0] != '\0') {
/* Use configured Base URL */
static char full_url[512];
snprintf(full_url, sizeof(full_url), "%s%s", s_base_url, s_current_provider->default_path);
return full_url;
}
/* Use default API URL */
return s_current_provider ? s_current_provider->default_api_url : "";
}
/* Get hostname for current provider */
const char *llm_provider_host(void) {
if (s_base_url[0] != '\0') {
/* Extract hostname from Base URL */
static char hostname[256];
const char *start = s_base_url;
if (strncmp(start, "https://", 8) == 0) start += 8;
else if (strncmp(start, "http://", 7) == 0) start += 7;
const char *end = strchr(start, '/');
if (end) {
size_t len = end - start;
if (len < sizeof(hostname)) {
memcpy(hostname, start, len);
hostname[len] = '\0';
return hostname;
}
}
/* No path, copy whole string */
strncpy(hostname, start, sizeof(hostname) - 1);
hostname[sizeof(hostname) - 1] = '\0';
return hostname;
}
/* Use default host */
return s_current_provider ? s_current_provider->default_host : "";
}
/* Get API path for current provider */
const char *llm_provider_path(void) {
return s_current_provider ? s_current_provider->default_path : "";
}
/* Provider-specific API key management */
void llm_provider_set_api_key(const char *provider_name, const char *api_key) {
if (!provider_name || !api_key) return;
const char *nvs_key = get_provider_api_key_nvs_key(provider_name);
if (!nvs_key) {
ESP_LOGW(TAG, "No NVS key for provider: %s", provider_name);
return;
}
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READWRITE, &nvs) == ESP_OK) {
nvs_set_str(nvs, nvs_key, api_key);
nvs_commit(nvs);
nvs_close(nvs);
ESP_LOGI(TAG, "API key saved for provider: %s", provider_name);
}
/* If this is the current provider, update in-memory key */
if (strcmp(provider_name, s_current_provider->name) == 0) {
strncpy(s_api_key, api_key, sizeof(s_api_key) - 1);
s_api_key[sizeof(s_api_key) - 1] = '\0';
}
}
/* Provider-specific Base URL management */
void llm_provider_set_base_url(const char *provider_name, const char *base_url) {
if (!provider_name || !base_url) return;
const char *nvs_key = get_provider_base_url_nvs_key(provider_name);
if (!nvs_key) {
ESP_LOGW(TAG, "No NVS key for provider: %s", provider_name);
return;
}
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READWRITE, &nvs) == ESP_OK) {
nvs_set_str(nvs, nvs_key, base_url);
nvs_commit(nvs);
nvs_close(nvs);
ESP_LOGI(TAG, "Base URL saved for provider: %s", provider_name);
}
/* If this is the current provider, update in-memory Base URL */
if (strcmp(provider_name, s_current_provider->name) == 0) {
strncpy(s_base_url, base_url, sizeof(s_base_url) - 1);
s_base_url[sizeof(s_base_url) - 1] = '\0';
}
}
/* Get API key for a provider */
const char *llm_provider_get_api_key(const char *provider_name) {
if (!provider_name) return NULL;
/* If this is the current provider, return in-memory key */
if (strcmp(provider_name, s_current_provider->name) == 0) {
return s_api_key;
}
/* Otherwise, load from NVS */
static char key_buffer[LLM_API_KEY_MAX_LEN] = {0};
const char *nvs_key = get_provider_api_key_nvs_key(provider_name);
if (!nvs_key) return NULL;
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(key_buffer);
if (nvs_get_str(nvs, nvs_key, key_buffer, &len) == ESP_OK && key_buffer[0]) {
nvs_close(nvs);
return key_buffer;
}
nvs_close(nvs);
}
return NULL;
}
/* Get Base URL for a provider */
const char *llm_provider_get_base_url(const char *provider_name) {
if (!provider_name) return NULL;
/* If this is the current provider, return in-memory Base URL */
if (strcmp(provider_name, s_current_provider->name) == 0) {
return s_base_url[0] ? s_base_url : NULL;
}
/* Otherwise, load from NVS */
static char url_buffer[256] = {0};
const char *nvs_key = get_provider_base_url_nvs_key(provider_name);
if (!nvs_key) return NULL;
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(url_buffer);
if (nvs_get_str(nvs, nvs_key, url_buffer, &len) == ESP_OK && url_buffer[0]) {
nvs_close(nvs);
return url_buffer;
}
nvs_close(nvs);
}
return NULL;
}
/* Initialize provider system (load from NVS) */
void llm_provider_init(void) {
/* Load API key for current provider */
const char *api_key = llm_provider_get_api_key(s_current_provider->name);
if (api_key) {
strncpy(s_api_key, api_key, sizeof(s_api_key) - 1);
s_api_key[sizeof(s_api_key) - 1] = '\0';
} else {
s_api_key[0] = '\0';
}
/* Load Base URL for current provider */
const char *base_url = llm_provider_get_base_url(s_current_provider->name);
if (base_url) {
strncpy(s_base_url, base_url, sizeof(s_base_url) - 1);
s_base_url[sizeof(s_base_url) - 1] = '\0';
} else {
s_base_url[0] = '\0';
}
ESP_LOGI(TAG, "Provider initialized: %s (API key: %s, Base URL: %s)",
s_current_provider->name,
s_api_key[0] ? "set" : "not set",
s_base_url[0] ? s_base_url : "default");
}
/* Save provider configuration to NVS */
void llm_provider_save_config(const char *provider_name) {
/* This function is intentionally left empty -
individual set functions already save to NVS */
}
/* Common authentication header setup for OpenAI-compatible providers */
void llm_provider_set_auth_headers(esp_http_client_handle_t client, const char *api_key) {
if (!client || !api_key) return;
/* All OpenAI-compatible providers use Bearer token authentication */
char auth[LLM_API_KEY_MAX_LEN + 16];
snprintf(auth, sizeof(auth), "Bearer %s", api_key);
esp_http_client_set_header(client, "Authorization", auth);
}

68
main/llm/llm_provider.h Normal file
View File

@@ -0,0 +1,68 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Forward declaration for esp_http_client_handle_t */
typedef struct esp_http_client* esp_http_client_handle_t;
/* Provider configuration structure */
typedef struct {
const char *name; /* Provider name, e.g., "anthropic", "openai", "siliconflow", "volcengine" */
const char *default_api_url; /* Default API URL */
const char *default_host; /* Default hostname */
const char *default_path; /* Default API path */
bool is_openai_compatible; /* Whether this provider uses OpenAI-compatible API */
} llm_provider_config_t;
/* Provider registry - all supported providers */
extern const llm_provider_config_t llm_providers[];
extern const int llm_provider_count;
/* Find provider configuration by name */
const llm_provider_config_t *llm_provider_find(const char *name);
/* Get current provider configuration */
const llm_provider_config_t *llm_provider_current(void);
/* Set current provider by name */
void llm_provider_set_current(const char *name);
/* Get current provider name */
const char *llm_provider_current_name(void);
/* Check if current provider is OpenAI-compatible */
bool llm_provider_is_openai_compatible(void);
/* Get API URL for current provider (with dynamic Base URL support) */
const char *llm_provider_api_url(void);
/* Get hostname for current provider */
const char *llm_provider_host(void);
/* Get API path for current provider */
const char *llm_provider_path(void);
/* Provider-specific API key and Base URL management */
void llm_provider_set_api_key(const char *provider_name, const char *api_key);
void llm_provider_set_base_url(const char *provider_name, const char *base_url);
const char *llm_provider_get_api_key(const char *provider_name);
const char *llm_provider_get_base_url(const char *provider_name);
/* Initialize provider system (load from NVS) */
void llm_provider_init(void);
/* Save provider configuration to NVS */
void llm_provider_save_config(const char *provider_name);
/* Common authentication header setup for OpenAI-compatible providers */
void llm_provider_set_auth_headers(esp_http_client_handle_t client, const char *api_key);
#ifdef __cplusplus
}
#endif

View File

@@ -1,4 +1,5 @@
#include "llm_proxy.h"
#include "llm_provider.h"
#include "mimi_config.h"
#include "proxy/http_proxy.h"
@@ -184,28 +185,31 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt)
static bool provider_is_openai(void)
{
return strcmp(s_provider, "openai") == 0;
return llm_provider_is_openai_compatible();
}
static const char *llm_api_url(void)
{
return provider_is_openai() ? MIMI_OPENAI_API_URL : MIMI_LLM_API_URL;
return llm_provider_api_url();
}
static const char *llm_api_host(void)
{
return provider_is_openai() ? "api.openai.com" : "api.anthropic.com";
return llm_provider_host();
}
static const char *llm_api_path(void)
{
return provider_is_openai() ? "/v1/chat/completions" : "/v1/messages";
return llm_provider_path();
}
/* ── Init ─────────────────────────────────────────────────────── */
esp_err_t llm_proxy_init(void)
{
/* Initialize provider system */
llm_provider_init();
/* Start with build-time defaults */
if (MIMI_SECRET_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_API_KEY);
@@ -215,6 +219,8 @@ esp_err_t llm_proxy_init(void)
}
if (MIMI_SECRET_MODEL_PROVIDER[0] != '\0') {
safe_copy(s_provider, sizeof(s_provider), MIMI_SECRET_MODEL_PROVIDER);
/* Set current provider based on build-time default */
llm_provider_set_current(s_provider);
}
/* NVS overrides take highest priority (set via CLI) */
@@ -234,10 +240,18 @@ esp_err_t llm_proxy_init(void)
len = sizeof(provider_tmp);
if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider_tmp, &len) == ESP_OK && provider_tmp[0]) {
safe_copy(s_provider, sizeof(s_provider), provider_tmp);
/* Set current provider based on NVS override */
llm_provider_set_current(s_provider);
}
nvs_close(nvs);
}
/* Load provider-specific API key if available */
const char *provider_api_key = llm_provider_get_api_key(s_provider);
if (provider_api_key && provider_api_key[0]) {
safe_copy(s_api_key, sizeof(s_api_key), provider_api_key);
}
if (s_api_key[0]) {
ESP_LOGI(TAG, "LLM proxy initialized (provider: %s, model: %s)", s_provider, s_model);
} else {
@@ -265,13 +279,15 @@ 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()) {
/* Use provider-specific authentication */
if (llm_provider_is_openai_compatible()) {
/* OpenAI-compatible providers use Bearer token authentication */
if (s_api_key[0]) {
char auth[LLM_API_KEY_MAX_LEN + 16];
snprintf(auth, sizeof(auth), "Bearer %s", s_api_key);
esp_http_client_set_header(client, "Authorization", auth);
llm_provider_set_auth_headers(client, s_api_key);
}
} else {
/* Anthropic uses x-api-key authentication */
esp_http_client_set_header(client, "x-api-key", s_api_key);
esp_http_client_set_header(client, "anthropic-version", MIMI_LLM_API_VERSION);
}
@@ -293,7 +309,9 @@ static esp_err_t llm_http_via_proxy(const char *post_data, resp_buf_t *rb, int *
int body_len = strlen(post_data);
char header[1024];
int hlen = 0;
if (provider_is_openai()) {
if (llm_provider_is_openai_compatible()) {
/* OpenAI-compatible providers use Bearer token authentication */
hlen = snprintf(header, sizeof(header),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
@@ -303,6 +321,7 @@ static esp_err_t llm_http_via_proxy(const char *post_data, resp_buf_t *rb, int *
"Connection: close\r\n\r\n",
llm_api_path(), llm_api_host(), s_api_key, body_len);
} else {
/* Anthropic uses x-api-key authentication */
hlen = snprintf(header, sizeof(header),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
@@ -559,13 +578,13 @@ esp_err_t llm_chat_tools(const char *system_prompt,
/* Build request body (non-streaming) */
cJSON *body = cJSON_CreateObject();
cJSON_AddStringToObject(body, "model", s_model);
if (provider_is_openai()) {
if (llm_provider_is_openai_compatible()) {
cJSON_AddNumberToObject(body, "max_completion_tokens", MIMI_LLM_MAX_TOKENS);
} else {
cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS);
}
if (provider_is_openai()) {
if (llm_provider_is_openai_compatible()) {
cJSON *openai_msgs = convert_messages_openai(system_prompt, messages);
cJSON_AddItemToObject(body, "messages", openai_msgs);
@@ -635,7 +654,7 @@ esp_err_t llm_chat_tools(const char *system_prompt,
return ESP_FAIL;
}
if (provider_is_openai()) {
if (llm_provider_is_openai_compatible()) {
cJSON *choices = cJSON_GetObjectItem(root, "choices");
cJSON *choice0 = choices && cJSON_IsArray(choices) ? cJSON_GetArrayItem(choices, 0) : NULL;
if (choice0) {
@@ -780,7 +799,11 @@ esp_err_t llm_set_api_key(const char *api_key)
nvs_close(nvs);
safe_copy(s_api_key, sizeof(s_api_key), api_key);
ESP_LOGI(TAG, "API key saved");
/* Also save to provider-specific NVS key */
llm_provider_set_api_key(s_provider, api_key);
ESP_LOGI(TAG, "API key saved for provider: %s", s_provider);
return ESP_OK;
}
@@ -806,6 +829,43 @@ esp_err_t llm_set_provider(const char *provider)
nvs_close(nvs);
safe_copy(s_provider, sizeof(s_provider), provider);
/* Update current provider in the provider system */
llm_provider_set_current(provider);
/* Load provider-specific API key if available */
const char *provider_api_key = llm_provider_get_api_key(provider);
if (provider_api_key && provider_api_key[0]) {
safe_copy(s_api_key, sizeof(s_api_key), provider_api_key);
} else {
s_api_key[0] = '\0';
}
ESP_LOGI(TAG, "Provider set to: %s", s_provider);
return ESP_OK;
}
esp_err_t llm_set_base_url(const char *provider, const char *base_url)
{
if (!provider || !base_url) return ESP_ERR_INVALID_ARG;
/* Save to provider-specific NVS key */
llm_provider_set_base_url(provider, base_url);
/* If this is the current provider, update in-memory Base URL */
if (strcmp(provider, s_provider) == 0) {
/* Reload provider configuration to pick up new Base URL */
llm_provider_init();
}
ESP_LOGI(TAG, "Base URL set for provider: %s", provider);
return ESP_OK;
}
const char *llm_get_base_url(const char *provider)
{
if (!provider) return NULL;
/* Get Base URL from provider system */
return llm_provider_get_base_url(provider);
}

View File

@@ -27,6 +27,16 @@ esp_err_t llm_set_provider(const char *provider);
*/
esp_err_t llm_set_model(const char *model);
/**
* Save the Base URL for a provider to NVS.
*/
esp_err_t llm_set_base_url(const char *provider, const char *base_url);
/**
* Get the Base URL for a provider from NVS.
*/
const char *llm_get_base_url(const char *provider);
/* ── Tool Use Support ──────────────────────────────────────────── */
typedef struct {

View File

@@ -88,6 +88,8 @@
#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_SILICONFLOW_API_URL "https://api.siliconflow.cn/v1/chat/completions"
#define MIMI_VOLCENGINE_API_URL "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
#define MIMI_LLM_API_VERSION "2023-06-01"
#define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024)
#define MIMI_LLM_LOG_VERBOSE_PAYLOAD 0
@@ -154,6 +156,16 @@
#define MIMI_NVS_KEY_PROXY_PORT "port"
#define MIMI_NVS_KEY_PROXY_TYPE "proxy_type"
/* Provider-specific NVS Keys */
#define MIMI_NVS_KEY_ANTHROPIC_API_KEY "anthropic_api_key"
#define MIMI_NVS_KEY_ANTHROPIC_BASE_URL "anthropic_base_url"
#define MIMI_NVS_KEY_OPENAI_API_KEY "openai_api_key"
#define MIMI_NVS_KEY_OPENAI_BASE_URL "openai_base_url"
#define MIMI_NVS_KEY_SILICONFLOW_API_KEY "siliconflow_api_key"
#define MIMI_NVS_KEY_SILICONFLOW_BASE_URL "siliconflow_base_url"
#define MIMI_NVS_KEY_VOLCENGINE_API_KEY "volcengine_api_key"
#define MIMI_NVS_KEY_VOLCENGINE_BASE_URL "volcengine_base_url"
/* WiFi Onboarding (Captive Portal) */
#define MIMI_ONBOARD_AP_PREFIX "MimiClaw-"
#define MIMI_ONBOARD_AP_PASS "" /* open network */

View File

@@ -26,6 +26,14 @@
#define MIMI_SECRET_MODEL ""
#define MIMI_SECRET_MODEL_PROVIDER "anthropic"
/* SiliconFlow API (OpenAI-compatible) */
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
/* Volcengine API (OpenAI-compatible, ByteDance Doubao models) */
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
/* HTTP Proxy (leave empty or set both) */
#define MIMI_SECRET_PROXY_HOST ""
#define MIMI_SECRET_PROXY_PORT ""