feat: add Kimi Code and web APIs
This commit is contained in:
+47
-3
@@ -22,6 +22,25 @@
|
||||
#define ERRBODY_MAX 4096
|
||||
#define LLM_MAX_RETRIES 3
|
||||
|
||||
static int api_host_has(const char *base, const char *needle) {
|
||||
return base && needle && strstr(base, needle) != NULL;
|
||||
}
|
||||
|
||||
static int is_kimi_api(const char *base) {
|
||||
return api_host_has(base, "api.moonshot.ai") ||
|
||||
api_host_has(base, "api.moonshot.cn") ||
|
||||
api_host_has(base, "api.kimi.ai") ||
|
||||
api_host_has(base, "api.kimi.com");
|
||||
}
|
||||
|
||||
static int is_kimi_k2(const char *model) {
|
||||
return model && strncmp(model, "kimi-k2", 7) == 0;
|
||||
}
|
||||
|
||||
static int is_kimi_k27(const char *model) {
|
||||
return model && strncmp(model, "kimi-k2.7-code", 14) == 0;
|
||||
}
|
||||
|
||||
static int ca_file_ok(const char *path) {
|
||||
if (!path || !path[0])
|
||||
return 0;
|
||||
@@ -452,8 +471,31 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
return NULL;
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(root, "model", req->model);
|
||||
if (req->reasoning_effort && req->reasoning_effort[0])
|
||||
const int kimi = is_kimi_api(req->api_base);
|
||||
const int kimi_k2 = kimi && is_kimi_k2(req->model);
|
||||
if (req->reasoning_effort && req->reasoning_effort[0] && !kimi_k2)
|
||||
cJSON_AddStringToObject(root, "reasoning_effort", req->reasoning_effort);
|
||||
if (kimi_k2) {
|
||||
/* K2.6/K2.7 do not understand K3's top-level reasoning_effort. They
|
||||
* expose thinking as an object and require preserved reasoning to be
|
||||
* sent back on the next turn. K2.7-code is always enabled/kept. */
|
||||
cJSON *thinking = cJSON_AddObjectToObject(root, "thinking");
|
||||
cJSON_AddStringToObject(thinking, "type", "enabled");
|
||||
if (req->send_reasoning || is_kimi_k27(req->model))
|
||||
cJSON_AddStringToObject(thinking, "keep", "all");
|
||||
}
|
||||
if (kimi && (!getenv("CST_NO_CACHE_HINTS") ||
|
||||
!getenv("CST_NO_CACHE_HINTS")[0] ||
|
||||
getenv("CST_NO_CACHE_HINTS")[0] == '0')) {
|
||||
/* Kimi's implicit cache defaults to 5m. Agent sessions commonly pause
|
||||
* longer than that, so opt into the 1h tier; users can select 5m/1h. */
|
||||
const char *ttl = getenv("CST_KIMI_CACHE_TTL");
|
||||
if (!ttl || (strcmp(ttl, "5m") != 0 && strcmp(ttl, "1h") != 0))
|
||||
ttl = "1h";
|
||||
cJSON *cache = cJSON_AddObjectToObject(root, "prompt_cache_options");
|
||||
cJSON_AddStringToObject(cache, "mode", "implicit");
|
||||
cJSON_AddStringToObject(cache, "ttl", ttl);
|
||||
}
|
||||
cJSON_AddTrueToObject(root, "stream");
|
||||
const char *nousage = getenv("CST_NO_STREAM_USAGE");
|
||||
if (!nousage || !nousage[0] || nousage[0] == '0') {
|
||||
@@ -586,8 +628,10 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
if (req->send_reasoning && m->reasoning && m->reasoning[0])
|
||||
cJSON_AddItemToObject(o, "reasoning_content",
|
||||
clean_string(m->reasoning, 0));
|
||||
if (m->prefix)
|
||||
cJSON_AddTrueToObject(o, "prefix");
|
||||
if (m->prefix) {
|
||||
/* Kimi calls the OpenAI-style assistant prefill flag `partial`. */
|
||||
cJSON_AddTrueToObject(o, kimi ? "partial" : "prefix");
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToArray(msgs, o);
|
||||
if (role == CST_ROLE_TOOL && n_att)
|
||||
|
||||
@@ -876,6 +876,11 @@ const char *cst_effort_wire(const char *provider, const char *model,
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (strcmp(mode, "passthrough") == 0)
|
||||
return effort;
|
||||
if (strcmp(mode, "kimi") == 0) {
|
||||
/* Kimi K3 accepts low/high/max; the older K2 thinking models use the
|
||||
* separate `thinking` object and only need a valid effort selection. */
|
||||
return strcmp(effort, "medium") == 0 ? "low" : effort;
|
||||
}
|
||||
if (strcmp(mode, "openai") == 0) {
|
||||
cst_model_info_t mi;
|
||||
cst_model_info(provider, model, &mi);
|
||||
|
||||
@@ -15,6 +15,23 @@
|
||||
"prefix_completion": true,
|
||||
"effort": "passthrough"
|
||||
},
|
||||
"kimi": {
|
||||
"api_base": "https://api.moonshot.ai/v1",
|
||||
"key_env": "MOONSHOT_API_KEY",
|
||||
"default_model": "kimi-k3",
|
||||
"models": ["kimi-k3", "kimi-k2.6", "kimi-k2.7-code"],
|
||||
"reasoning_passback": true,
|
||||
"effort": "kimi"
|
||||
},
|
||||
"kimi-code": {
|
||||
"api_base": "https://api.kimi.ai/coding/v1",
|
||||
"key_env": "KIMI_API_KEY",
|
||||
"default_model": "k3",
|
||||
"models": ["k3", "k3-256k", "kimi-for-coding",
|
||||
"kimi-for-coding-highspeed"],
|
||||
"reasoning_passback": true,
|
||||
"effort": "kimi"
|
||||
},
|
||||
"openai": {
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
"key_env": "OPENAI_API_KEY",
|
||||
@@ -65,6 +82,16 @@
|
||||
{"match": "glm", "context": 128000},
|
||||
{"match": "glm-*v*", "vision": true},
|
||||
{"match": "kimi", "context": 131072},
|
||||
{"match": "kimi-k3", "context": 1048576, "vision": true,
|
||||
"reasoning": true},
|
||||
{"match": "kimi-k2.6", "context": 262144, "vision": true,
|
||||
"reasoning": true},
|
||||
{"match": "kimi-k2.7-code", "context": 262144, "reasoning": true},
|
||||
{"match": "k3", "context": 1048576, "vision": true, "reasoning": true},
|
||||
{"match": "k3-256k", "context": 262144, "reasoning": true},
|
||||
{"match": "kimi-for-coding", "context": 1048576, "reasoning": true},
|
||||
{"match": "kimi-for-coding-highspeed", "context": 262144,
|
||||
"reasoning": true},
|
||||
{"match": "pixtral", "vision": true},
|
||||
{"match": "llava", "vision": true},
|
||||
{"match": "*-vl*", "vision": true},
|
||||
|
||||
@@ -159,6 +159,14 @@ static const cst_tool_def_t TOOLS[] = {
|
||||
NULL,
|
||||
{{"todos", S, 1, "the whole list, one item per line"}},
|
||||
CST_TP_META, 0, cst_tool_todo_write, NULL, NO_DYN, NO_META},
|
||||
{"web_search",
|
||||
"Search the live web through the current Kimi or DeepSeek provider and "
|
||||
"return titles, URLs and snippets. Use web_fetch to read a result.",
|
||||
NULL,
|
||||
{{"query", S, 1, "search query"},
|
||||
{"count", I, 0, "number of results (default 5, max 20)"},
|
||||
{"include_content", B, 0, "include provider-returned page content"}},
|
||||
CST_TP_NET, 0, cst_tool_web_search, NULL, NO_DYN, NO_META},
|
||||
{"web_fetch",
|
||||
"Fetch an http(s) URL (docs, issues, APIs) and return its text; HTML "
|
||||
"is reduced to readable text.",
|
||||
|
||||
@@ -55,6 +55,8 @@ int cst_preview_clipboard_write(const cst_tool_ctx_t *ctx,
|
||||
const ctl_tool_call_t *c, char **out);
|
||||
int cst_tool_web_fetch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r);
|
||||
int cst_tool_web_search(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r);
|
||||
int cst_tool_download(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r);
|
||||
int cst_tool_md_to_html(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
|
||||
+209
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_agent.h"
|
||||
#include "cst_attach.h"
|
||||
#include "cst_tools.h"
|
||||
@@ -23,6 +24,211 @@ typedef struct {
|
||||
int overflow;
|
||||
} fetch_t;
|
||||
|
||||
static size_t on_body(char *p, size_t sz, size_t n, void *ud);
|
||||
|
||||
static const cst_agent_config_t *web_cfg(const cst_tool_ctx_t *ctx) {
|
||||
return ctx && ctx->agent ? cst_agent_config(ctx->agent) : NULL;
|
||||
}
|
||||
|
||||
static int provider_api(const cst_tool_ctx_t *ctx, const char **kind,
|
||||
const char **base, const char **key) {
|
||||
const cst_agent_config_t *cfg = web_cfg(ctx);
|
||||
if (!cfg || !cfg->api_key[0])
|
||||
return 0;
|
||||
if (strcmp(cfg->provider, "deepseek") == 0 ||
|
||||
(cfg->api_base[0] && strstr(cfg->api_base, "api.deepseek.com"))) {
|
||||
*kind = "deepseek";
|
||||
} else if (strcmp(cfg->provider, "kimi") == 0 ||
|
||||
(cfg->api_base[0] && strstr(cfg->api_base, "moonshot"))) {
|
||||
*kind = "kimi";
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
*base = cfg->api_base;
|
||||
*key = cfg->api_key;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int api_post_json(const char *url, const char *key, const char *body,
|
||||
cst_buf_t *out, long *http) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl)
|
||||
return -1;
|
||||
char auth[700];
|
||||
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", key);
|
||||
struct curl_slist *hdrs = NULL;
|
||||
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
|
||||
hdrs = curl_slist_append(hdrs, "Accept: application/json");
|
||||
hdrs = curl_slist_append(hdrs, auth);
|
||||
fetch_t f = {0};
|
||||
cst_curl_apply_ca(curl);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 90L);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, on_body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &f);
|
||||
CURLcode rc = curl_easy_perform(curl);
|
||||
*http = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http);
|
||||
if (f.body.buf)
|
||||
cst_buf_append(out, f.body.buf, f.body.len);
|
||||
cst_buf_free(&f.body);
|
||||
curl_slist_free_all(hdrs);
|
||||
curl_easy_cleanup(curl);
|
||||
return rc == CURLE_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
static void api_path(char *out, size_t n, const char *base, const char *path) {
|
||||
size_t len = strlen(base);
|
||||
while (len && base[len - 1] == '/')
|
||||
len--;
|
||||
snprintf(out, n, "%.*s/%s", (int)len, base, path);
|
||||
}
|
||||
|
||||
static int api_error(cst_tool_result_t *r, long http, const cst_buf_t *raw) {
|
||||
cJSON *j = cJSON_Parse(raw->buf ? raw->buf : "");
|
||||
const cJSON *e = j ? cJSON_GetObjectItemCaseSensitive(j, "error") : NULL;
|
||||
const cJSON *m = e ? cJSON_GetObjectItemCaseSensitive(e, "message") : NULL;
|
||||
int rc;
|
||||
if (cJSON_IsString(m))
|
||||
rc = tool_set_resultf(r, 1, "error: HTTP %ld: %s", http, m->valuestring);
|
||||
else
|
||||
rc = tool_set_resultf(r, 1, "error: HTTP %ld: %.400s", http,
|
||||
raw->buf ? raw->buf : "no response");
|
||||
cJSON_Delete(j);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int online_fetch(const cst_tool_ctx_t *ctx, const char *url,
|
||||
long max_out, cst_tool_result_t *r) {
|
||||
const char *kind, *base, *key;
|
||||
if (!provider_api(ctx, &kind, &base, &key))
|
||||
return 0;
|
||||
if (max_out <= 0)
|
||||
max_out = FETCH_DEFAULT_OUT;
|
||||
if (max_out > FETCH_MAX_OUT)
|
||||
max_out = FETCH_MAX_OUT;
|
||||
char endpoint[1024];
|
||||
api_path(endpoint, sizeof(endpoint), base, "tools/fetch");
|
||||
cJSON *body = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(body, "url", url);
|
||||
char *json = cJSON_PrintUnformatted(body);
|
||||
cJSON_Delete(body);
|
||||
cst_buf_t raw = {0};
|
||||
long http = 0;
|
||||
int rc = json ? api_post_json(endpoint, key, json, &raw, &http) : -1;
|
||||
free(json);
|
||||
if (rc != 0) {
|
||||
cst_buf_free(&raw);
|
||||
return tool_set_result(r, "error: online fetch request failed", 1);
|
||||
}
|
||||
if (http >= 400) {
|
||||
int e = api_error(r, http, &raw);
|
||||
cst_buf_free(&raw);
|
||||
return e;
|
||||
}
|
||||
cJSON *doc = cJSON_Parse(raw.buf ? raw.buf : "");
|
||||
if (!doc) {
|
||||
cst_buf_free(&raw);
|
||||
return tool_set_result(r, "error: online fetch returned invalid JSON", 1);
|
||||
}
|
||||
const cJSON *text = cJSON_GetObjectItemCaseSensitive(
|
||||
doc, strcmp(kind, "kimi") == 0 ? "markdown" : "content");
|
||||
const cJSON *title = cJSON_GetObjectItemCaseSensitive(doc, "title");
|
||||
if (!cJSON_IsString(text)) {
|
||||
cJSON_Delete(doc);
|
||||
cst_buf_free(&raw);
|
||||
return tool_set_result(r, "error: online fetch returned no readable content", 1);
|
||||
}
|
||||
cst_buf_t out = {0};
|
||||
cst_buf_printf(&out, "%s\n\n%s", cJSON_IsString(title) ? title->valuestring : url,
|
||||
cJSON_IsString(text) ? text->valuestring : "");
|
||||
size_t keep = out.len > (size_t)max_out ? cst_utf8_floor(out.buf, (size_t)max_out)
|
||||
: out.len;
|
||||
cst_buf_t clipped = {0};
|
||||
cst_buf_append(&clipped, out.buf ? out.buf : "", keep);
|
||||
if (keep < out.len)
|
||||
cst_buf_puts(&clipped, "\n...(truncated)\n");
|
||||
int result = tool_take_buf(r, &clipped, 0);
|
||||
cst_buf_free(&out);
|
||||
cJSON_Delete(doc);
|
||||
cst_buf_free(&raw);
|
||||
return result;
|
||||
}
|
||||
|
||||
int cst_tool_web_search(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
const char *query = tool_arg(c, "query");
|
||||
const char *kind, *base, *key;
|
||||
if (!query || !query[0])
|
||||
return tool_set_result(r, "error: missing query", 1);
|
||||
if (!provider_api(ctx, &kind, &base, &key))
|
||||
return tool_set_result(
|
||||
r, "error: web_search needs the kimi or deepseek provider", 1);
|
||||
long count = tool_arg_long(c, "count", 5);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 20) count = 20;
|
||||
int content = tool_arg_bool(c, "include_content", 0);
|
||||
cJSON *body = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(body, strcmp(kind, "kimi") == 0 ? "text_query" : "query", query);
|
||||
cJSON_AddNumberToObject(body, strcmp(kind, "kimi") == 0 ? "limit" : "count", count);
|
||||
if (strcmp(kind, "kimi") == 0)
|
||||
cJSON_AddBoolToObject(body, "include_content", content);
|
||||
else
|
||||
cJSON_AddBoolToObject(body, "need_content", content);
|
||||
char *json = cJSON_PrintUnformatted(body);
|
||||
cJSON_Delete(body);
|
||||
char endpoint[1024];
|
||||
api_path(endpoint, sizeof(endpoint), base, "tools/search");
|
||||
cst_buf_t raw = {0};
|
||||
long http = 0;
|
||||
int rc = json ? api_post_json(endpoint, key, json, &raw, &http) : -1;
|
||||
free(json);
|
||||
if (rc != 0) {
|
||||
cst_buf_free(&raw);
|
||||
return tool_set_result(r, "error: web search request failed", 1);
|
||||
}
|
||||
if (http >= 400) {
|
||||
int e = api_error(r, http, &raw);
|
||||
cst_buf_free(&raw);
|
||||
return e;
|
||||
}
|
||||
cJSON *doc = cJSON_Parse(raw.buf ? raw.buf : "");
|
||||
if (!doc) {
|
||||
cst_buf_free(&raw);
|
||||
return tool_set_result(r, "error: web search returned invalid JSON", 1);
|
||||
}
|
||||
const cJSON *arr = cJSON_GetObjectItemCaseSensitive(
|
||||
doc, strcmp(kind, "kimi") == 0 ? "search_results" : "results");
|
||||
if (!cJSON_IsArray(arr)) {
|
||||
cJSON_Delete(doc);
|
||||
cst_buf_free(&raw);
|
||||
return tool_set_result(r, "error: web search returned no result list", 1);
|
||||
}
|
||||
cst_buf_t out = {0};
|
||||
int n = cJSON_GetArraySize(arr);
|
||||
cst_buf_printf(&out, "web search (%s): %d result%s\n", kind, n, n == 1 ? "" : "s");
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cJSON *item = cJSON_GetArrayItem(arr, i);
|
||||
const cJSON *title = cJSON_GetObjectItemCaseSensitive(item, "title");
|
||||
const cJSON *url = cJSON_GetObjectItemCaseSensitive(item, "url");
|
||||
const cJSON *snippet = cJSON_GetObjectItemCaseSensitive(
|
||||
item, strcmp(kind, "kimi") == 0 ? (content ? "text" : "snippet") :
|
||||
(content ? "summary" : "snippet"));
|
||||
cst_buf_printf(&out, "\n[%d] %s\n%s\n%s\n", i + 1,
|
||||
cJSON_IsString(title) ? title->valuestring : "(untitled)",
|
||||
cJSON_IsString(url) ? url->valuestring : "",
|
||||
cJSON_IsString(snippet) ? snippet->valuestring : "");
|
||||
}
|
||||
int result = tool_take_buf(r, &out, 0);
|
||||
cJSON_Delete(doc);
|
||||
cst_buf_free(&raw);
|
||||
return result;
|
||||
}
|
||||
|
||||
static size_t on_body(char *p, size_t sz, size_t n, void *ud) {
|
||||
fetch_t *f = ud;
|
||||
size_t len = sz * n;
|
||||
@@ -206,6 +412,9 @@ int cst_tool_web_fetch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
if (strncasecmp(url, "http://", 7) != 0 &&
|
||||
strncasecmp(url, "https://", 8) != 0)
|
||||
return tool_set_result(r, "error: only http:// and https:// URLs", 1);
|
||||
const char *online_kind, *online_base, *online_key;
|
||||
if (provider_api(ctx, &online_kind, &online_base, &online_key))
|
||||
return online_fetch(ctx, url, tool_arg_long(c, "max_bytes", FETCH_DEFAULT_OUT), r);
|
||||
long max_out = tool_arg_long(c, "max_bytes", FETCH_DEFAULT_OUT);
|
||||
if (max_out <= 0)
|
||||
max_out = FETCH_DEFAULT_OUT;
|
||||
|
||||
Reference in New Issue
Block a user