#include "llm_provider.h" #include "mimi_config.h" #include "nvs.h" #include #include #include #include "esp_log.h" #include "esp_err.h" #include "esp_http_client.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); }