From 8ff004f290c5e85241057bc545162d07e0a2d063 Mon Sep 17 00:00:00 2001 From: Mauro Druwel Date: Thu, 19 Feb 2026 14:52:25 +0100 Subject: [PATCH 1/5] feat: expand LLM provider support to include OpenRouter and NVIDIA NIM --- README.md | 16 ++++++++-------- docs/ARCHITECTURE.md | 27 ++++++++++++++++++--------- main/cli/serial_cli.c | 2 +- main/llm/llm_proxy.c | 30 +++++++++++++++++++++++++----- main/llm/llm_proxy.h | 2 +- main/mimi_config.h | 2 ++ main/mimi_secrets.h.example | 2 +- 7 files changed, 56 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index cead1927..e48d318c 100644 --- a/README.md +++ b/README.md @@ -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)**, **OpenRouter**, and **NVIDIA NIM (completely free)** 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 **API key** — from [Anthropic](https://console.anthropic.com), [OpenAI](https://platform.openai.com), [OpenRouter](https://openrouter.ai), or [NVIDIA NIM (free)](https://org.ngc.nvidia.com/setup/api-keys) ### Install @@ -69,7 +69,7 @@ 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", "openrouter", or "nvidia_nim" #define MIMI_SECRET_SEARCH_KEY "" // optional: Brave Search API key #define MIMI_SECRET_PROXY_HOST "" // optional: e.g. "10.0.0.1" #define MIMI_SECRET_PROXY_PORT "" // optional: e.g. "7897" @@ -108,8 +108,8 @@ 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_api_key sk-ant-api03-... # change API key (Anthropic, OpenAI, OpenRouter, or NVIDIA NIM) +mimi> set_model_provider openai # switch provider (anthropic|openai|openrouter|nvidia_nim) mimi> set_model gpt-4o # change LLM model mimi> set_proxy 127.0.0.1 7897 # set HTTP proxy mimi> clear_proxy # remove proxy @@ -148,7 +148,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 Anthropic, OpenAI, OpenRouter, and NVIDIA NIM — the LLM can call tools during a conversation and loop until the task is done (ReAct pattern). | Tool | Description | |------|-------------| @@ -178,10 +178,10 @@ 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), OpenRouter, and NVIDIA NIM (completely free), 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 +- **Tool use** — ReAct agent loop with tool calling for all providers ## For Developers diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b8b399a4..c70cecfa 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -54,12 +54,13 @@ Telegram App (User) │ └──────────────────────────────────────────┘ │ └───────────────────────────────────────────────────┘ │ - │ Anthropic Messages API (HTTPS) + │ Anthropic/OpenAI/OpenRouter API (HTTPS) │ + Brave Search API (HTTPS) ▼ - ┌───────────┐ ┌──────────────┐ - │ Claude API │ │ Brave Search │ - └───────────┘ └──────────────┘ + ┌──────────────┐ ┌──────────────┐ + │ LLM Provider │ │ Brave Search │ + │ (API Gateway)│ │ │ + └──────────────┘ └──────────────┘ ``` --- @@ -75,7 +76,7 @@ Telegram App (User) b. Build system prompt (SOUL.md + USER.md + MEMORY.md + recent notes + tool guidance) c. Build cJSON messages array (history + current message) d. ReAct loop (max 10 iterations): - i. Call Claude API via HTTPS (non-streaming, with tools array) + i. Call LLM provider API via HTTPS (non-streaming, with tools array) ii. Parse JSON response → text blocks + tool_use blocks iii. If stop_reason == "tool_use": - Execute each tool (e.g. web_search → Brave Search API) @@ -158,7 +159,7 @@ main/ | Task | Core | Priority | Stack | Description | |--------------------|------|----------|--------|--------------------------------------| | `tg_poll` | 0 | 5 | 12 KB | Telegram long polling (30s timeout) | -| `agent_loop` | 1 | 6 | 12 KB | Message processing + Claude API call | +| `agent_loop` | 1 | 6 | 12 KB | Message processing + LLM provider API call | | `outbound` | 0 | 5 | 8 KB | Route responses to Telegram / WS | | `serial_cli` | 0 | 3 | 4 KB | USB serial console REPL | | httpd (internal) | 0 | 5 | — | WebSocket server (esp_http_server) | @@ -232,7 +233,7 @@ All configuration is done exclusively through `mimi_secrets.h` at build time. Th | `MIMI_SECRET_WIFI_SSID` | WiFi SSID | | `MIMI_SECRET_WIFI_PASS` | WiFi password | | `MIMI_SECRET_TG_TOKEN` | Telegram Bot API token | -| `MIMI_SECRET_API_KEY` | Anthropic API key | +| `MIMI_SECRET_API_KEY` | LLM API key (Anthropic, OpenAI, OpenRouter, or NVIDIA NIM) | | `MIMI_SECRET_MODEL` | Model ID (default: claude-opus-4-6) | | `MIMI_SECRET_PROXY_HOST` | HTTP proxy hostname/IP (optional) | | `MIMI_SECRET_PROXY_PORT` | HTTP proxy port (optional) | @@ -278,7 +279,15 @@ Client `chat_id` is auto-assigned on connection (`ws_`) but can be overridde --- -## Claude API Integration +## LLM Provider Integration + +MimiClaw supports multiple LLM providers via a unified API proxy: +- **Anthropic** (`anthropic`) - Native Messages API +- **OpenAI** (`openai`) - Chat Completions API +- **OpenRouter** (`openrouter`) - OpenAI-compatible endpoint +- **NVIDIA NIM** (`nvidia_nim`) - OpenAI-compatible endpoint, **completely free** + +### Anthropic API Format Endpoint: `POST https://api.anthropic.com/v1/messages` @@ -388,7 +397,7 @@ The CLI provides debug and maintenance commands only. All configuration is done | `session/manager.py` | `memory/session_mgr.c` | JSONL per chat, ring buffer | | `channels/telegram.py` | `telegram/telegram_bot.c` | Raw HTTP, no python-telegram-bot | | `bus/events.py` + `queue.py`| `bus/message_bus.c` | FreeRTOS queues vs asyncio | -| `providers/litellm_provider.py` | `llm/llm_proxy.c` | Direct Anthropic API only | +| `providers/litellm_provider.py` | `llm/llm_proxy.c` | Multi-provider LLM API support | | `config/schema.py` | `mimi_config.h` + `mimi_secrets.h` | Build-time secrets only | | `cli/commands.py` | `cli/serial_cli.c` | esp_console REPL | | `agent/tools/*` | `tools/tool_registry.c` + `tool_web_search.c` | web_search via Brave API | diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index d2315c4f..6e369a7c 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -623,7 +623,7 @@ esp_err_t serial_cli_init(void) esp_console_cmd_register(&model_cmd); /* set_model_provider */ - provider_args.provider = arg_str1(NULL, NULL, "", "Model provider (anthropic|openai)"); + provider_args.provider = arg_str1(NULL, NULL, "", "Model provider (anthropic|openai|openrouter|nvidia_nim)"); provider_args.end = arg_end(1); esp_console_cmd_t provider_cmd = { .command = "set_model_provider", diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index dba29610..250929c5 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -139,19 +139,39 @@ static bool provider_is_openai(void) return strcmp(s_provider, "openai") == 0; } +static bool provider_is_openrouter(void) +{ + return strcmp(s_provider, "openrouter") == 0; +} + +static bool provider_is_nvidia_nim(void) +{ + return strcmp(s_provider, "nvidia_nim") == 0; +} + +/** Return the full API URL for the active provider. */ static const char *llm_api_url(void) { - return provider_is_openai() ? MIMI_OPENAI_API_URL : MIMI_LLM_API_URL; + if (provider_is_openai()) return MIMI_OPENAI_API_URL; + if (provider_is_openrouter()) return MIMI_OPENROUTER_API_URL; + if (provider_is_nvidia_nim()) return MIMI_NVIDIA_NIM_API_URL; + return MIMI_LLM_API_URL; } +/** Return the HTTP Host header value for the active provider. */ static const char *llm_api_host(void) { - return provider_is_openai() ? "api.openai.com" : "api.anthropic.com"; + if (provider_is_openai()) return "api.openai.com"; + if (provider_is_openrouter()) return "openrouter.ai"; + if (provider_is_nvidia_nim()) return "integrate.api.nvidia.com"; + return "api.anthropic.com"; } +/** Return the HTTP request path for the active provider. */ static const char *llm_api_path(void) { - return provider_is_openai() ? "/v1/chat/completions" : "/v1/messages"; + if (provider_is_openai() || provider_is_openrouter() || provider_is_nvidia_nim()) return "/v1/chat/completions"; + return "/v1/messages"; } /* ── Init ─────────────────────────────────────────────────────── */ @@ -217,7 +237,7 @@ 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 (provider_is_openai() || provider_is_openrouter() || provider_is_nvidia_nim()) { if (s_api_key[0]) { char auth[LLM_API_KEY_MAX_LEN + 16]; snprintf(auth, sizeof(auth), "Bearer %s", s_api_key); @@ -245,7 +265,7 @@ 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 (provider_is_openai() || provider_is_openrouter() || provider_is_nvidia_nim()) { hlen = snprintf(header, sizeof(header), "POST %s HTTP/1.1\r\n" "Host: %s\r\n" diff --git a/main/llm/llm_proxy.h b/main/llm/llm_proxy.h index 03c3967d..cd184379 100644 --- a/main/llm/llm_proxy.h +++ b/main/llm/llm_proxy.h @@ -18,7 +18,7 @@ esp_err_t llm_proxy_init(void); esp_err_t llm_set_api_key(const char *api_key); /** - * Save the LLM provider to NVS. (e.g. "anthropic", "openai") + * Save the LLM provider to NVS. (e.g. "anthropic", "openai", "openrouter", "nvidia_nim") */ esp_err_t llm_set_provider(const char *provider); diff --git a/main/mimi_config.h b/main/mimi_config.h index 47a35973..b033cceb 100644 --- a/main/mimi_config.h +++ b/main/mimi_config.h @@ -67,6 +67,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_OPENROUTER_API_URL "https://openrouter.ai/api/v1/chat/completions" +#define MIMI_NVIDIA_NIM_API_URL "https://integrate.api.nvidia.com/v1/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 diff --git a/main/mimi_secrets.h.example b/main/mimi_secrets.h.example index bf76defb..9f321d02 100644 --- a/main/mimi_secrets.h.example +++ b/main/mimi_secrets.h.example @@ -17,7 +17,7 @@ /* Telegram Bot */ #define MIMI_SECRET_TG_TOKEN "" -/* Anthropic API */ +/* LLM API (anthropic, openai, openrouter, or nvidia_nim) #define MIMI_SECRET_API_KEY "" #define MIMI_SECRET_MODEL "" #define MIMI_SECRET_MODEL_PROVIDER "anthropic" From eb545ab4acdd2eef35589713ad87764f88c9926a Mon Sep 17 00:00:00 2001 From: Mauro Druwel <46003176+MauroDruwel@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:56:42 +0000 Subject: [PATCH 2/5] Fix pr comments --- README.md | 6 +++--- docs/ARCHITECTURE.md | 4 ++-- main/cli/serial_cli.c | 2 +- main/llm/llm_proxy.c | 33 ++++++++++++++++++++------------- main/llm/llm_proxy.h | 2 +- main/mimi_secrets.h.example | 2 +- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e48d318c..69e3065c 100644 --- a/README.md +++ b/README.md @@ -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 **Anthropic (Claude)**, **OpenAI (GPT)**, **OpenRouter**, and **NVIDIA NIM (completely free)** 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)**, **OpenRouter**, and **[NVIDIA NIM](https://www.nvidia.com/en-us/ai/#nim) (free for development for NVIDIA Developer Program members; production requires NVIDIA AI Enterprise licensing)** as providers, switchable at runtime. Everything runs on a single $5 chip with all your data stored locally on flash. ## Quick Start @@ -69,7 +69,7 @@ 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", "openai", "openrouter", or "nvidia_nim" +#define MIMI_SECRET_MODEL_PROVIDER "anthropic" // "anthropic", "openai", "openrouter", or "nvidia" #define MIMI_SECRET_SEARCH_KEY "" // optional: Brave Search API key #define MIMI_SECRET_PROXY_HOST "" // optional: e.g. "10.0.0.1" #define MIMI_SECRET_PROXY_PORT "" // optional: e.g. "7897" @@ -109,7 +109,7 @@ 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, OpenAI, OpenRouter, or NVIDIA NIM) -mimi> set_model_provider openai # switch provider (anthropic|openai|openrouter|nvidia_nim) +mimi> set_model_provider openai # switch provider (anthropic|openai|openrouter|nvidia) mimi> set_model gpt-4o # change LLM model mimi> set_proxy 127.0.0.1 7897 # set HTTP proxy mimi> clear_proxy # remove proxy diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c70cecfa..b8302654 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -54,7 +54,7 @@ Telegram App (User) │ └──────────────────────────────────────────┘ │ └───────────────────────────────────────────────────┘ │ - │ Anthropic/OpenAI/OpenRouter API (HTTPS) + │ Anthropic/OpenAI/OpenRouter/NVIDIA NIM API (HTTPS) │ + Brave Search API (HTTPS) ▼ ┌──────────────┐ ┌──────────────┐ @@ -285,7 +285,7 @@ MimiClaw supports multiple LLM providers via a unified API proxy: - **Anthropic** (`anthropic`) - Native Messages API - **OpenAI** (`openai`) - Chat Completions API - **OpenRouter** (`openrouter`) - OpenAI-compatible endpoint -- **NVIDIA NIM** (`nvidia_nim`) - OpenAI-compatible endpoint, **completely free** +- **NVIDIA NIM** (`nvidia`) - OpenAI-compatible endpoint (free for development/prototyping for NVIDIA Developer Program members; production requires NVIDIA AI Enterprise licensing) ### Anthropic API Format diff --git a/main/cli/serial_cli.c b/main/cli/serial_cli.c index 6e369a7c..3108b4e2 100644 --- a/main/cli/serial_cli.c +++ b/main/cli/serial_cli.c @@ -623,7 +623,7 @@ esp_err_t serial_cli_init(void) esp_console_cmd_register(&model_cmd); /* set_model_provider */ - provider_args.provider = arg_str1(NULL, NULL, "", "Model provider (anthropic|openai|openrouter|nvidia_nim)"); + provider_args.provider = arg_str1(NULL, NULL, "", "Model provider (anthropic|openai|openrouter|nvidia)"); provider_args.end = arg_end(1); esp_console_cmd_t provider_cmd = { .command = "set_model_provider", diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index 250929c5..b7189590 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -144,9 +144,9 @@ static bool provider_is_openrouter(void) return strcmp(s_provider, "openrouter") == 0; } -static bool provider_is_nvidia_nim(void) +static bool provider_is_nvidia(void) { - return strcmp(s_provider, "nvidia_nim") == 0; + return strcmp(s_provider, "nvidia") == 0; } /** Return the full API URL for the active provider. */ @@ -154,7 +154,7 @@ static const char *llm_api_url(void) { if (provider_is_openai()) return MIMI_OPENAI_API_URL; if (provider_is_openrouter()) return MIMI_OPENROUTER_API_URL; - if (provider_is_nvidia_nim()) return MIMI_NVIDIA_NIM_API_URL; + if (provider_is_nvidia()) return MIMI_NVIDIA_NIM_API_URL; return MIMI_LLM_API_URL; } @@ -163,17 +163,24 @@ static const char *llm_api_host(void) { if (provider_is_openai()) return "api.openai.com"; if (provider_is_openrouter()) return "openrouter.ai"; - if (provider_is_nvidia_nim()) return "integrate.api.nvidia.com"; + if (provider_is_nvidia()) return "integrate.api.nvidia.com"; return "api.anthropic.com"; } /** Return the HTTP request path for the active provider. */ static const char *llm_api_path(void) { - if (provider_is_openai() || provider_is_openrouter() || provider_is_nvidia_nim()) return "/v1/chat/completions"; + if (provider_is_openrouter()) return "/api/v1/chat/completions"; + if (provider_is_openai() || provider_is_nvidia()) return "/v1/chat/completions"; return "/v1/messages"; } +/** Return true for OpenAI-compatible providers (OpenAI, OpenRouter, NVIDIA NIM). */ +static bool provider_is_openai_compat(void) +{ + return provider_is_openai() || provider_is_openrouter() || provider_is_nvidia(); +} + /* ── Init ─────────────────────────────────────────────────────── */ esp_err_t llm_proxy_init(void) @@ -237,7 +244,7 @@ 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() || provider_is_openrouter() || provider_is_nvidia_nim()) { + if (provider_is_openai() || provider_is_openrouter() || provider_is_nvidia()) { if (s_api_key[0]) { char auth[LLM_API_KEY_MAX_LEN + 16]; snprintf(auth, sizeof(auth), "Bearer %s", s_api_key); @@ -265,7 +272,7 @@ 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() || provider_is_openrouter() || provider_is_nvidia_nim()) { + if (provider_is_openai() || provider_is_openrouter() || provider_is_nvidia()) { hlen = snprintf(header, sizeof(header), "POST %s HTTP/1.1\r\n" "Host: %s\r\n" @@ -552,13 +559,13 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json, /* Build request body (non-streaming) */ cJSON *body = cJSON_CreateObject(); cJSON_AddStringToObject(body, "model", s_model); - if (provider_is_openai()) { + if (provider_is_openai_compat()) { 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 (provider_is_openai_compat()) { cJSON *messages = cJSON_Parse(messages_json); if (!messages) { messages = cJSON_CreateArray(); @@ -635,7 +642,7 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json, return ESP_FAIL; } - if (provider_is_openai()) { + if (provider_is_openai_compat()) { extract_text_openai(root, response_buf, buf_size); } else { extract_text_anthropic(root, response_buf, buf_size); @@ -678,13 +685,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 (provider_is_openai_compat()) { 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 (provider_is_openai_compat()) { cJSON *openai_msgs = convert_messages_openai(system_prompt, messages); cJSON_AddItemToObject(body, "messages", openai_msgs); @@ -754,7 +761,7 @@ esp_err_t llm_chat_tools(const char *system_prompt, return ESP_FAIL; } - if (provider_is_openai()) { + if (provider_is_openai_compat()) { cJSON *choices = cJSON_GetObjectItem(root, "choices"); cJSON *choice0 = choices && cJSON_IsArray(choices) ? cJSON_GetArrayItem(choices, 0) : NULL; if (choice0) { diff --git a/main/llm/llm_proxy.h b/main/llm/llm_proxy.h index cd184379..3fd183e9 100644 --- a/main/llm/llm_proxy.h +++ b/main/llm/llm_proxy.h @@ -18,7 +18,7 @@ esp_err_t llm_proxy_init(void); esp_err_t llm_set_api_key(const char *api_key); /** - * Save the LLM provider to NVS. (e.g. "anthropic", "openai", "openrouter", "nvidia_nim") + * Save the LLM provider to NVS. (e.g. "anthropic", "openai", "openrouter", "nvidia") */ esp_err_t llm_set_provider(const char *provider); diff --git a/main/mimi_secrets.h.example b/main/mimi_secrets.h.example index 9f321d02..d60d49e6 100644 --- a/main/mimi_secrets.h.example +++ b/main/mimi_secrets.h.example @@ -17,7 +17,7 @@ /* Telegram Bot */ #define MIMI_SECRET_TG_TOKEN "" -/* LLM API (anthropic, openai, openrouter, or nvidia_nim) +/* LLM API (anthropic, openai, openrouter, or nvidia) */ #define MIMI_SECRET_API_KEY "" #define MIMI_SECRET_MODEL "" #define MIMI_SECRET_MODEL_PROVIDER "anthropic" From 9c9d72043b964694d84d123433f7ebe5666aa3fc Mon Sep 17 00:00:00 2001 From: Mauro Druwel <46003176+MauroDruwel@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:08:11 +0000 Subject: [PATCH 3/5] Update readme with pr comments --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69e3065c..199b8da3 100644 --- a/README.md +++ b/README.md @@ -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 **API key** — from [Anthropic](https://console.anthropic.com), [OpenAI](https://platform.openai.com), [OpenRouter](https://openrouter.ai), or [NVIDIA NIM (free)](https://org.ngc.nvidia.com/setup/api-keys) +- An **API key** — from [Anthropic](https://console.anthropic.com), [OpenAI](https://platform.openai.com), [OpenRouter](https://openrouter.ai), or [NVIDIA NIM](https://org.ngc.nvidia.com/setup/api-keys) (free for development for NVIDIA Developer Program members; production requires NVIDIA AI Enterprise licensing) ### Install @@ -178,7 +178,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 Anthropic (Claude), OpenAI (GPT), OpenRouter, and NVIDIA NIM (completely free), switchable at runtime +- **Multi-provider** — supports Anthropic (Claude), OpenAI (GPT), OpenRouter, and NVIDIA NIM (free for development for NVIDIA Developer Program members; production requires NVIDIA AI Enterprise licensing), 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 all providers From f57343b373da01edecf13963da3381928b0cf29a Mon Sep 17 00:00:00 2001 From: Mauro Druwel Date: Sun, 15 Mar 2026 10:37:37 +0100 Subject: [PATCH 4/5] docs: update README and ARCHITECTURE for NVIDIA NIM feat: add TODO in llm_proxy.c for deriving host from configured API URL --- README.md | 6 +++--- docs/ARCHITECTURE.md | 2 +- main/llm/llm_proxy.c | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ec468181..c1fb379e 100644 --- a/README.md +++ b/README.md @@ -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 **Anthropic (Claude)**, **OpenAI (GPT)**, **OpenRouter**, and **[NVIDIA NIM](https://www.nvidia.com/en-us/ai/#nim) (free for development for NVIDIA Developer Program members; production requires NVIDIA AI Enterprise licensing)** 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)**, **OpenRouter**, and **NVIDIA NIM** 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 **API key** — from [Anthropic](https://console.anthropic.com), [OpenAI](https://platform.openai.com), [OpenRouter](https://openrouter.ai), or [NVIDIA NIM](https://org.ngc.nvidia.com/setup/api-keys) (free for development for NVIDIA Developer Program members; production requires NVIDIA AI Enterprise licensing) +- An **API key** — from [Anthropic](https://console.anthropic.com), [OpenAI](https://platform.openai.com), [OpenRouter](https://openrouter.ai), or [NVIDIA NIM](https://org.ngc.nvidia.com/setup/api-keys) ### Install @@ -237,7 +237,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 Anthropic (Claude), OpenAI (GPT), OpenRouter, and NVIDIA NIM (free for development for NVIDIA Developer Program members; production requires NVIDIA AI Enterprise licensing), switchable at runtime +- **Multi-provider** — supports Anthropic (Claude), OpenAI (GPT), OpenRouter, and NVIDIA NIM, 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 all providers diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b8302654..3387741d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -285,7 +285,7 @@ MimiClaw supports multiple LLM providers via a unified API proxy: - **Anthropic** (`anthropic`) - Native Messages API - **OpenAI** (`openai`) - Chat Completions API - **OpenRouter** (`openrouter`) - OpenAI-compatible endpoint -- **NVIDIA NIM** (`nvidia`) - OpenAI-compatible endpoint (free for development/prototyping for NVIDIA Developer Program members; production requires NVIDIA AI Enterprise licensing) +- **NVIDIA NIM** (`nvidia`) - OpenAI-compatible endpoint ### Anthropic API Format diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index b7189590..3181e34d 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -161,6 +161,7 @@ static const char *llm_api_url(void) /** Return the HTTP Host header value for the active provider. */ static const char *llm_api_host(void) { + /* TODO(PR#40): derive host from configured API URL once custom provider URLs are supported. */ if (provider_is_openai()) return "api.openai.com"; if (provider_is_openrouter()) return "openrouter.ai"; if (provider_is_nvidia()) return "integrate.api.nvidia.com"; From c882fee463f268e2d393200a5313d7da92db92a1 Mon Sep 17 00:00:00 2001 From: Mauro Druwel Date: Sun, 15 Mar 2026 11:08:52 +0100 Subject: [PATCH 5/5] coderabbit suggestions --- main/llm/llm_proxy.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/llm/llm_proxy.c b/main/llm/llm_proxy.c index 2bd77db0..cd486d7b 100644 --- a/main/llm/llm_proxy.c +++ b/main/llm/llm_proxy.c @@ -293,7 +293,7 @@ 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() || provider_is_openrouter() || provider_is_nvidia()) { + if (provider_is_openai_compat()) { if (s_api_key[0]) { char auth[LLM_API_KEY_MAX_LEN + 16]; snprintf(auth, sizeof(auth), "Bearer %s", s_api_key); @@ -321,7 +321,7 @@ 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() || provider_is_openrouter() || provider_is_nvidia()) { + if (provider_is_openai_compat()) { hlen = snprintf(header, sizeof(header), "POST %s HTTP/1.1\r\n" "Host: %s\r\n"