* Copyright (C) 2026 Xiaomi Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "channels/cmd_llm.h"
#include "infra/config_store.h"
#include "infra/url_parse.h"
#include "llm/llm_proxy.h"
#include "llm/llm_router.h"
#include "infra/http_proxy.h"
#include "infra/vela_tls.h"
#include "agent_compat.h"
#include "agent_config.h"
#include <cJSON.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
const char* name;
const char* host;
const char* path;
const char* model;
int cost_tier;
} router_preset_t;
static const router_preset_t g_router_presets[] = {
{ "deepseek", "api.deepseek.com", "/v1/chat/completions", "deepseek-chat", 1 },
{ "kimi", "api.moonshot.cn", "/v1/chat/completions", "kimi-k2.5", 2 },
{ "qwen", "dashscope.aliyuncs.com", "/compatible-mode/v1/chat/completions",
"qwen-turbo", 1 },
{ "glm", "open.bigmodel.cn", "/api/paas/v4/chat/completions", "glm-4-flash", 1 },
{ "openai", "api.openai.com", "/v1/chat/completions", "gpt-4o", 3 },
{ "claude", "api.anthropic.com", "/v1/messages", "claude-sonnet-4-20250514", 3 },
{ "mimo", "api.xiaomimimo.com", "/v1/chat/completions", "mimo-v2-flash", 1 },
{ "openrouter", "openrouter.ai", "/api/v1/chat/completions",
"openrouter/hunter-alpha", 2 },
{ NULL, NULL, NULL, NULL, 0 }
};
void cmd_set_llm(int argc, char** argv)
{
if (argc < 2) {
printf("Usage: set_llm <preset> [api_key]\n"
" set_llm <url> <model> [api_key]\n"
" set_llm <host> <model> [api_key]\n"
"\n"
"Presets:\n"
" kimi - api.moonshot.cn (kimi-k2.5)\n"
" qwen - dashscope.aliyuncs.com (qwen-turbo)\n"
" deepseek - api.deepseek.com (deepseek-chat)\n"
" glm - open.bigmodel.cn (glm-4-flash)\n"
" openai - api.openai.com (gpt-4o)\n"
" openrouter - openrouter.ai (openrouter/hunter-alpha)\n"
" mimo - api.xiaomimimo.com (MiMo-v2-Flash)\n"
"\n"
"URL format (for custom endpoints):\n"
" set_llm http://host:port/path model key\n"
" set_llm http://<your-endpoint>/v1 model-name sk-xxx\n"
"\n"
"Note: set_llm writes to router slot 0 and applies immediately.\n");
return;
}
const char* arg1 = argv[1];
const char* host = NULL;
const char* port = "443";
const char* path = "/v1/chat/completions";
const char* model = NULL;
const char* api_key = NULL;
int cost_tier = 1;
if (strncmp(arg1, "http://", 7) == 0 || strncmp(arg1, "https://", 8) == 0) {
parsed_url_t parsed;
if (url_parse(arg1, &parsed) != 0) {
printf("Invalid URL: %s\n", arg1);
return;
}
host = parsed.host;
port = parsed.port;
if (parsed.path[0] && parsed.path[1]) {
path = parsed.path;
} else {
path = "/v1/chat/completions";
}
char full_path[256];
if (strcmp(path, "/v1") == 0 || strcmp(path, "/v1/") == 0) {
snprintf(full_path, sizeof(full_path), "/v1/chat/completions");
path = full_path;
}
if (argc >= 3)
model = argv[2];
if (argc >= 4)
api_key = argv[3];
}
else {
const router_preset_t* preset = NULL;
for (int i = 0; g_router_presets[i].name; i++) {
if (strcmp(g_router_presets[i].name, arg1) == 0) {
preset = &g_router_presets[i];
break;
}
}
if (preset) {
host = preset->host;
path = preset->path;
model = preset->model;
cost_tier = preset->cost_tier;
port = "443";
if (argc >= 3)
api_key = argv[2];
} else {
host = arg1;
port = "443";
if (argc >= 3)
model = argv[2];
if (argc >= 4)
api_key = argv[3];
}
}
llm_backend_t backend = { 0 };
strncpy(backend.host, host, sizeof(backend.host) - 1);
backend.host[sizeof(backend.host) - 1] = '\0';
strncpy(backend.path, path, sizeof(backend.path) - 1);
backend.path[sizeof(backend.path) - 1] = '\0';
strncpy(backend.port, port, sizeof(backend.port) - 1);
backend.port[sizeof(backend.port) - 1] = '\0';
if (model) {
strncpy(backend.model, model, sizeof(backend.model) - 1);
backend.model[sizeof(backend.model) - 1] = '\0';
}
if (api_key) {
strncpy(backend.api_key, api_key, sizeof(backend.api_key) - 1);
backend.api_key[sizeof(backend.api_key) - 1] = '\0';
} else {
llm_backend_t old;
if (llm_router_get_backend(0, &old) == 0 && old.api_key[0]) {
strncpy(backend.api_key, old.api_key, sizeof(backend.api_key) - 1);
backend.api_key[sizeof(backend.api_key) - 1] = '\0';
}
}
backend.priority = 0;
backend.cost_tier = cost_tier;
backend.enabled = true;
llm_router_set_backend(0, &backend);
llm_router_apply(0);
printf("LLM backend: %s:%s%s (model: %s) [router slot 0]\n",
host, port, path, model ? model : "(unchanged)");
if (api_key) {
printf("API key saved.\n");
}
}
void cmd_set_vision_llm(int argc, char** argv)
{
if (argc < 2) {
printf("Usage: set_vision_llm <preset> [api_key]\n"
" set_vision_llm <host> <model> [api_key]\n"
" set_vision_llm clear\n"
"\n"
"Set an independent vision model. If not configured,\n"
"vision calls inherit the main LLM config.\n"
"\n"
"Presets:\n"
" mimo - api.xiaomimimo.com (mimo-v2-omni)\n"
" openai - api.openai.com (gpt-4o)\n"
" qwen - dashscope.aliyuncs.com (qwen-vl-max)\n"
" glm - open.bigmodel.cn (glm-4v-flash)\n"
"\n"
"Examples:\n"
" set_vision_llm mimo <api_key>\n"
" set_vision_llm clear\n");
return;
}
const char* arg1 = argv[1];
if (strcmp(arg1, "clear") == 0) {
llm_set_vision_model(NULL, NULL, NULL);
printf("Vision LLM config cleared (using main LLM).\n");
return;
}
const char* host = NULL;
const char* model = NULL;
const char* api_key = NULL;
static const struct {
const char* name;
const char* host;
const char* model;
} vision_presets[] = {
{ "mimo", "api.xiaomimimo.com", "mimo-v2-omni" },
{ "openai", "api.openai.com", "gpt-4o" },
{ "qwen", "dashscope.aliyuncs.com", "qwen-vl-max" },
{ "glm", "open.bigmodel.cn", "glm-4v-flash" },
{ NULL, NULL, NULL }
};
bool found = false;
for (int i = 0; vision_presets[i].name; i++) {
if (strcmp(vision_presets[i].name, arg1) == 0) {
host = vision_presets[i].host;
model = vision_presets[i].model;
if (argc >= 3)
api_key = argv[2];
found = true;
break;
}
}
if (!found) {
host = arg1;
if (argc >= 3)
model = argv[2];
if (argc >= 4)
api_key = argv[3];
}
llm_set_vision_model(host, model, api_key);
printf("Vision LLM: %s (model: %s)\n", host, model ? model : "(inherit)");
if (api_key)
printf("Vision API key saved.\n");
}
#define LIST_MODELS_BUF_SIZE (256 * 1024)
#define LIST_MODELS_PATH "/api/v1/models"
static int list_models_fetch(const char* api_key, char** out, size_t* out_len)
{
char* buf = malloc(LIST_MODELS_BUF_SIZE);
if (!buf) {
printf("Error: OOM allocating response buffer\n");
return ERROR;
}
vela_header_t hdrs[3];
char auth[160];
snprintf(auth, sizeof(auth), "Bearer %s", api_key);
hdrs[0].name = "Authorization";
hdrs[0].value = auth;
hdrs[1].name = "Accept";
hdrs[1].value = "application/json";
hdrs[2].name = NULL;
hdrs[2].value = NULL;
size_t body_len;
int status = vela_https_request(
AGENT_LLM_OPENROUTER_HOST, "443", "GET",
LIST_MODELS_PATH, hdrs, NULL, 0,
buf, LIST_MODELS_BUF_SIZE, &body_len);
if (status < 200 || status >= 300) {
printf("Error: HTTP %d from models API\n", status);
free(buf);
return ERROR;
}
*out = buf;
*out_len = body_len;
return OK;
}
static int list_models_fetch_proxy(const char* api_key,
char** out, size_t* out_len)
{
proxy_conn_t* conn = proxy_conn_open(
AGENT_LLM_OPENROUTER_HOST, 443, 30000);
if (!conn) {
printf("Error: proxy connection failed\n");
return ERROR;
}
char hdr[512];
int hlen = snprintf(hdr, sizeof(hdr),
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Authorization: Bearer %s\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n\r\n",
LIST_MODELS_PATH, AGENT_LLM_OPENROUTER_HOST, api_key);
if (proxy_conn_write(conn, hdr, hlen) < 0) {
proxy_conn_close(conn);
printf("Error: write failed\n");
return ERROR;
}
char* buf = malloc(LIST_MODELS_BUF_SIZE);
if (!buf) {
proxy_conn_close(conn);
printf("Error: OOM\n");
return ERROR;
}
size_t total = 0;
char tmp[4096];
while (total < LIST_MODELS_BUF_SIZE - 1) {
int n = proxy_conn_read(conn, tmp, sizeof(tmp), 60000);
if (n <= 0) {
break;
}
size_t avail = LIST_MODELS_BUF_SIZE - 1 - total;
size_t copy = (size_t)n < avail ? (size_t)n : avail;
memcpy(buf + total, tmp, copy);
total += copy;
}
proxy_conn_close(conn);
buf[total] = '\0';
char* body = strstr(buf, "\r\n\r\n");
if (body) {
body += 4;
size_t blen = total - (size_t)(body - buf);
memmove(buf, body, blen);
buf[blen] = '\0';
*out_len = blen;
} else {
*out_len = total;
}
*out = buf;
return OK;
}
static bool model_is_free(cJSON* item)
{
cJSON* pricing = cJSON_GetObjectItem(item, "pricing");
if (!pricing) {
return false;
}
const cJSON* prompt = cJSON_GetObjectItem(pricing, "prompt");
const cJSON* completion = cJSON_GetObjectItem(pricing, "completion");
if (!prompt || !completion) {
return false;
}
const char* p = prompt->valuestring;
const char* c = completion->valuestring;
if (!p || !c) {
return false;
}
return (atof(p) == 0.0 && atof(c) == 0.0);
}
static void list_models_print(cJSON* data, bool free_only,
const char* keyword)
{
int count = 0;
int total = cJSON_GetArraySize(data);
for (int i = 0; i < total; i++) {
cJSON* item = cJSON_GetArrayItem(data, i);
cJSON* id = cJSON_GetObjectItem(item, "id");
if (!id || !id->valuestring) {
continue;
}
bool is_free = model_is_free(item);
if (free_only && !is_free) {
continue;
}
if (keyword && !strstr(id->valuestring, keyword)) {
continue;
}
printf(" %-45s %s\n", id->valuestring,
is_free ? "[FREE]" : "[PAID]");
count++;
}
printf("\n%d model(s) shown (total: %d)\n", count, total);
}
void cmd_list_models(int argc, char** argv)
{
char host[128];
char api_key[128];
claw_config_get(AGENT_CFG_KEY_LLM_HOST, host, sizeof(host));
claw_config_get(AGENT_CFG_KEY_API_KEY, api_key, sizeof(api_key));
if (!strstr(host, "openrouter.ai")) {
printf("list_models only works with openrouter.ai backend.\n"
"Use: set_llm openrouter\n");
return;
}
if (!api_key[0]) {
printf("No API key set. Use: set_llm <preset> <key>\n");
return;
}
bool free_only = false;
const char* keyword = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--free") == 0) {
free_only = true;
} else {
keyword = argv[i];
}
}
printf("Fetching models from %s ...\n", AGENT_LLM_OPENROUTER_HOST);
char* buf;
size_t buf_len;
int ret;
if (http_proxy_is_enabled()) {
ret = list_models_fetch_proxy(api_key, &buf, &buf_len);
} else {
ret = list_models_fetch(api_key, &buf, &buf_len);
}
if (ret != OK) {
return;
}
cJSON* root = cJSON_Parse(buf);
free(buf);
buf = NULL;
if (!root) {
printf("Error: failed to parse JSON response\n");
return;
}
cJSON* data = cJSON_GetObjectItem(root, "data");
if (!data || !cJSON_IsArray(data)) {
printf("Error: unexpected response format\n");
cJSON_Delete(root);
return;
}
list_models_print(data, free_only, keyword);
cJSON_Delete(root);
}
void cmd_router_status(void)
{
char* json = llm_router_status_json();
if (json) {
printf("=== LLM Router Status ===\n%s\n=========================\n", json);
free(json);
} else {
printf("Failed to get router status.\n");
}
}
void cmd_router_profile(int argc, char** argv)
{
if (argc < 2) {
llm_route_profile_t p = llm_router_get_profile();
const char* name = "auto";
if (p == LLM_ROUTE_ECO)
name = "eco";
else if (p == LLM_ROUTE_PREMIUM)
name = "premium";
printf("Current profile: %s\n", name);
printf("Usage: router_profile <eco|auto|premium>\n");
return;
}
llm_route_profile_t profile = LLM_ROUTE_AUTO;
const char* profile_name = "auto";
if (strcmp(argv[1], "eco") == 0) {
profile = LLM_ROUTE_ECO;
profile_name = "eco";
} else if (strcmp(argv[1], "premium") == 0) {
profile = LLM_ROUTE_PREMIUM;
profile_name = "premium";
} else if (strcmp(argv[1], "auto") != 0) {
printf("Unknown profile: %s (valid: eco, auto, premium)\n", argv[1]);
return;
}
llm_router_set_profile(profile);
printf("Router profile set to: %s\n", profile_name);
}
void cmd_router_set(int argc, char** argv)
{
if (argc < 3) {
printf("Usage: router_set <preset> <api_key>\n\n"
"Presets:\n"
" deepseek - DeepSeek (cheap, fast)\n"
" kimi - Moonshot Kimi (mid-tier)\n"
" qwen - Alibaba Qwen (cheap)\n"
" glm - Zhipu GLM-4 (cheap)\n"
" openai - OpenAI GPT-4o (premium)\n"
" claude - Anthropic Claude (premium)\n"
" mimo - Xiaomi MiMo (cheap)\n\n"
"Example:\n"
" router_set deepseek sk-xxx\n"
" router_set openai sk-xxx\n");
return;
}
const char* preset_name = argv[1];
const char* api_key = argv[2];
const router_preset_t* preset = NULL;
for (int i = 0; g_router_presets[i].name; i++) {
if (strcmp(g_router_presets[i].name, preset_name) == 0) {
preset = &g_router_presets[i];
break;
}
}
if (!preset) {
printf("Unknown preset: %s\n", preset_name);
return;
}
int slot = -1;
for (int i = 0; i < LLM_ROUTER_MAX_BACKENDS; i++) {
llm_backend_t b;
if (llm_router_get_backend(i, &b) != 0 || !b.enabled) {
slot = i;
break;
}
if (strcmp(b.host, preset->host) == 0) {
slot = i;
break;
}
}
if (slot < 0) {
printf("Error: All 4 slots full. Use router_clear first.\n");
return;
}
llm_backend_t backend = { 0 };
strncpy(backend.host, preset->host, sizeof(backend.host) - 1);
strncpy(backend.path, preset->path, sizeof(backend.path) - 1);
strncpy(backend.port, "443", sizeof(backend.port) - 1);
strncpy(backend.model, preset->model, sizeof(backend.model) - 1);
strncpy(backend.api_key, api_key, sizeof(backend.api_key) - 1);
backend.priority = slot;
backend.cost_tier = preset->cost_tier;
backend.enabled = true;
if (llm_router_set_backend(slot, &backend) == 0) {
printf("Added [%d]: %s (%s, tier=%d)\n",
slot, preset_name, preset->model, preset->cost_tier);
} else {
printf("Failed to add backend.\n");
}
}
void cmd_router_clear(int argc, char** argv)
{
if (argc >= 2) {
const char* arg = argv[1];
if (arg[0] < '0' || arg[0] > '9') {
printf("Error: index must be a number (0-%d)\n",
LLM_ROUTER_MAX_BACKENDS - 1);
return;
}
int idx = atoi(arg);
if (idx >= 0 && idx < LLM_ROUTER_MAX_BACKENDS) {
llm_backend_t empty = { 0 };
llm_router_set_backend(idx, &empty);
printf("Slot %d cleared.\n", idx);
} else {
printf("Error: index must be 0-%d\n", LLM_ROUTER_MAX_BACKENDS - 1);
}
} else {
for (int i = 0; i < LLM_ROUTER_MAX_BACKENDS; i++) {
llm_backend_t empty = { 0 };
llm_router_set_backend(i, &empty);
}
printf("All router backends cleared.\n");
}
}
void cmd_router_model(int argc, char** argv)
{
if (argc < 3) {
printf("Usage: router_model <index> <model_name>\n"
"Example: router_model 1 gpt-4o-mini\n");
return;
}
int idx = atoi(argv[1]);
if (idx < 0 || idx >= LLM_ROUTER_MAX_BACKENDS) {
printf("Error: index must be 0-%d\n", LLM_ROUTER_MAX_BACKENDS - 1);
return;
}
llm_backend_t b;
if (llm_router_get_backend(idx, &b) != 0 || !b.enabled) {
printf("Error: slot %d is empty\n", idx);
return;
}
strncpy(b.model, argv[2], sizeof(b.model) - 1);
b.model[sizeof(b.model) - 1] = '\0';
if (llm_router_set_backend(idx, &b) == 0) {
printf("Slot %d model changed to: %s\n", idx, argv[2]);
} else {
printf("Failed to update model.\n");
}
}