629 lines
23 KiB
C
629 lines
23 KiB
C
/* llm.c - OpenAI 兼容接口: 请求构造、SSE 流式解析、tool_calls 增量拼接 */
|
|
#include "llm.h"
|
|
#include "config.h" /* THINKING_AUTO / _ON / _OFF */
|
|
#include "http.h"
|
|
#include "i18n.h"
|
|
#include "util.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* URL / 请求体 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
char *llm_endpoint_url(const char *base_url) {
|
|
const char *base =
|
|
(base_url && *base_url) ? base_url : "https://api.openai.com/v1";
|
|
size_t n = strlen(base);
|
|
while (n > 0 && base[n - 1] == '/')
|
|
n--;
|
|
/* 允许用户直接给完整的 completions 地址 */
|
|
if (str_ends_with(base_url, "/chat/completions"))
|
|
return xstrndup(base, n);
|
|
return xasprintf("%.*s/chat/completions", (int)n, base);
|
|
}
|
|
|
|
JsonValue *llm_build_body(const LlmConfig *cfg, const JsonValue *messages,
|
|
const JsonValue *tools, int stream) {
|
|
JsonValue *body = json_new_object();
|
|
json_object_set_string(body, "model", cfg->model);
|
|
json_object_set(body, "messages", json_clone(messages));
|
|
if (cfg->temperature >= 0)
|
|
json_object_set_number(body, "temperature", cfg->temperature);
|
|
if (cfg->max_tokens > 0)
|
|
json_object_set_number(body, "max_tokens", (double)cfg->max_tokens);
|
|
/* 思考模式开关. AUTO 时整个字段都不发, 让服务端用自己的默认值
|
|
* (DeepSeek 默认是开启). 注意这些是请求体的顶层字段, 不是
|
|
* OpenAI SDK 里的 extra_body 包装. */
|
|
if (cfg->thinking != THINKING_AUTO) {
|
|
JsonValue *t = json_new_object();
|
|
json_object_set_string(
|
|
t, "type", cfg->thinking == THINKING_ON ? "enabled" : "disabled");
|
|
json_object_set(body, "thinking", t);
|
|
}
|
|
if (cfg->reasoning_effort && *cfg->reasoning_effort)
|
|
json_object_set_string(body, "reasoning_effort", cfg->reasoning_effort);
|
|
if (tools && json_array_size(tools) > 0) {
|
|
json_object_set(body, "tools", json_clone(tools));
|
|
json_object_set_string(body, "tool_choice", "auto");
|
|
}
|
|
json_object_set_bool(body, "stream", stream ? 1 : 0);
|
|
return body;
|
|
}
|
|
|
|
/* 单行 SSE 上限. 正常的 data: 行只有几十字节;
|
|
* 出现超长"行"说明对端根本不是 SSE (例如忽略 stream 直接返回整包 JSON,
|
|
* 而 JSON 里没有换行), 必须丢弃, 否则内存会被撑爆. */
|
|
#define SSE_MAX_LINE (256u * 1024)
|
|
/* 单条回复累积内容上限 */
|
|
#define SSE_MAX_CONTENT (32u * 1024 * 1024)
|
|
/* 单个工具调用参数上限. 正常人写不出 8MB 的参数,
|
|
* 超过基本可以断定是对端异常或恶意, 必须截住 */
|
|
#define SSE_MAX_TOOL_ARGS (8u * 1024 * 1024)
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 错误响应的可读化 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* 很多服务端把 message 写成 \uXXXX 转义, 直接打印很难看.
|
|
* 这里尝试解析出 error.message, 失败则回退到原始文本. */
|
|
static char *extract_error_message(const char *body) {
|
|
if (!body || !*body)
|
|
return NULL;
|
|
JsonValue *root = json_parse(body, NULL, 0);
|
|
if (!root)
|
|
return NULL;
|
|
char *msg = NULL;
|
|
JsonValue *err = json_object_get(root, "error");
|
|
if (err && err->type == JSON_OBJECT) {
|
|
const char *m = json_get_string(err, "message");
|
|
if (m && *m)
|
|
msg = xstrdup(m);
|
|
} else if (err && err->type == JSON_STRING) {
|
|
msg = xstrdup(err->u.string);
|
|
}
|
|
if (!msg) {
|
|
const char *m = json_get_string(root, "message");
|
|
if (m && *m)
|
|
msg = xstrdup(m);
|
|
}
|
|
json_free(root);
|
|
return msg;
|
|
}
|
|
|
|
/* 401/403 时补一句提示. 服务端的原文经常把人引向错误的方向:
|
|
* OpenRouter 对"格式不对的 key"回的是 "Missing Authentication header",
|
|
* 听起来像是请求里没带认证头, 实际上带了, 只是 key 不属于这家. */
|
|
static void append_auth_hint(char *errbuf, size_t errlen, const LlmConfig *cfg,
|
|
long status) {
|
|
if (status != 401 && status != 403)
|
|
return;
|
|
|
|
const char *key = cfg->api_key ? cfg->api_key : "";
|
|
size_t n = strlen(key);
|
|
char masked[80];
|
|
if (n == 0) {
|
|
snprintf(masked, sizeof masked, "%s", t("llm.mask.empty"));
|
|
} else {
|
|
/* 只露前几个字符: 够认出是哪一把, 又不至于把密钥打进日志 */
|
|
char head[8];
|
|
snprintf(head, sizeof head, "%.*s", n <= 10 ? 4 : 7, key);
|
|
snprintf(masked, sizeof masked, "%s",
|
|
tf("llm.mask.partial", "head", head, "n",
|
|
i18n_num((long long)n), NULL));
|
|
}
|
|
|
|
size_t cur = strlen(errbuf);
|
|
if (cur + 4 >= errlen)
|
|
return;
|
|
snprintf(errbuf + cur, errlen - cur, "%s",
|
|
tf("llm.err.auth_hint", "masked", masked, "url",
|
|
cfg->base_url && *cfg->base_url ? cfg->base_url
|
|
: t("llm.val.no_base_url"),
|
|
NULL));
|
|
}
|
|
|
|
static void set_error_with_detail(char *errbuf, size_t errlen,
|
|
const char *prefix, const char *body) {
|
|
snprintf(errbuf, errlen, "%s", prefix);
|
|
if (!body || !*body)
|
|
return;
|
|
size_t cur = strlen(errbuf);
|
|
if (cur + 4 >= errlen)
|
|
return;
|
|
char *msg = extract_error_message(body);
|
|
if (msg) {
|
|
snprintf(errbuf + cur, errlen - cur, " — %s", msg);
|
|
free(msg);
|
|
} else {
|
|
snprintf(errbuf + cur, errlen - cur, " — %.400s", body);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 流式累积器 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
typedef struct {
|
|
char *id;
|
|
char *name;
|
|
StrBuf args;
|
|
int started;
|
|
} ToolAcc;
|
|
|
|
typedef struct {
|
|
StrBuf raw; /* 原始字节, 用于 "服务端不支持流式" 时的回退 */
|
|
StrBuf content;
|
|
StrBuf reasoning; /* reasoning_content 累积 (思考过程, 不显示) */
|
|
ToolAcc *accs;
|
|
size_t acc_count;
|
|
size_t acc_cap;
|
|
char *finish_reason;
|
|
LlmDeltaFn on_delta;
|
|
void *ud;
|
|
LlmThinkFn on_think;
|
|
void *think_ud;
|
|
int got_event;
|
|
int overflow; /* 内容超过上限 */
|
|
int raw_truncated;
|
|
char *error; /* 服务端返回的 error.message */
|
|
JsonValue *usage;
|
|
StrBuf linebuf;
|
|
} SseCtx;
|
|
|
|
static ToolAcc *acc_at(SseCtx *c, size_t index) {
|
|
if (index >= c->acc_cap) {
|
|
size_t cap = c->acc_cap ? c->acc_cap : 4;
|
|
while (cap <= index)
|
|
cap *= 2;
|
|
c->accs = xrealloc(c->accs, cap * sizeof(ToolAcc));
|
|
memset(c->accs + c->acc_cap, 0, (cap - c->acc_cap) * sizeof(ToolAcc));
|
|
c->acc_cap = cap;
|
|
}
|
|
ToolAcc *a = &c->accs[index];
|
|
if (!a->started) {
|
|
a->started = 1;
|
|
sb_init(&a->args);
|
|
if (index + 1 > c->acc_count)
|
|
c->acc_count = index + 1;
|
|
}
|
|
return a;
|
|
}
|
|
|
|
static void sse_ctx_free(SseCtx *c) {
|
|
sb_free(&c->raw);
|
|
sb_free(&c->content);
|
|
sb_free(&c->reasoning);
|
|
sb_free(&c->linebuf);
|
|
for (size_t i = 0; i < c->acc_count; i++) {
|
|
if (!c->accs[i].started)
|
|
continue;
|
|
free(c->accs[i].id);
|
|
free(c->accs[i].name);
|
|
sb_free(&c->accs[i].args);
|
|
}
|
|
free(c->accs);
|
|
free(c->finish_reason);
|
|
free(c->error);
|
|
json_free(c->usage);
|
|
}
|
|
|
|
/* 处理一个 SSE data 负载 (不含 "data:" 前缀) */
|
|
static void handle_payload(SseCtx *c, const char *payload, size_t len) {
|
|
if (len == 0)
|
|
return;
|
|
if (len == 6 && memcmp(payload, "[DONE]", 6) == 0)
|
|
return;
|
|
|
|
char *js = xstrndup(payload, len);
|
|
char err[256];
|
|
JsonValue *root = json_parse(js, err, sizeof err);
|
|
free(js);
|
|
if (!root)
|
|
return;
|
|
c->got_event = 1;
|
|
|
|
JsonValue *errv = json_object_get(root, "error");
|
|
if (errv && errv->type == JSON_OBJECT) {
|
|
const char *m = json_get_string(errv, "message");
|
|
if (m && !c->error)
|
|
c->error = xstrdup(m);
|
|
json_free(root);
|
|
return;
|
|
}
|
|
JsonValue *usage = json_object_get(root, "usage");
|
|
if (usage && usage->type == JSON_OBJECT && !c->usage)
|
|
c->usage = json_clone(usage);
|
|
|
|
JsonValue *choices = json_object_get(root, "choices");
|
|
JsonValue *ch0 = json_array_get(choices, 0);
|
|
if (!ch0) {
|
|
json_free(root);
|
|
return;
|
|
}
|
|
|
|
const char *fr = json_get_string(ch0, "finish_reason");
|
|
if (fr && *fr && !c->finish_reason)
|
|
c->finish_reason = xstrdup(fr);
|
|
|
|
/* OpenAI 流式用 delta, 少数实现直接给 message */
|
|
JsonValue *d = json_object_get(ch0, "delta");
|
|
if (!d)
|
|
d = json_object_get(ch0, "message");
|
|
if (!d || d->type != JSON_OBJECT) {
|
|
json_free(root);
|
|
return;
|
|
}
|
|
|
|
const char *piece = json_get_string(d, "content");
|
|
if (piece && *piece) {
|
|
if (c->content.len + strlen(piece) > SSE_MAX_CONTENT) {
|
|
c->overflow = 1;
|
|
} else {
|
|
sb_puts(&c->content, piece);
|
|
if (c->on_delta)
|
|
c->on_delta(c->ud, piece);
|
|
}
|
|
}
|
|
|
|
/* 思考过程. DeepSeek 用 reasoning_content, 有些 OpenAI 兼容实现用
|
|
* reasoning. 只累积、不回调 —— 用户明确要求不显示, 但它必须随
|
|
* assistant 消息一起回传: 请求带 tools 时服务端要求这么做,
|
|
* 漏了会在下一轮直接 400. */
|
|
const char *think = json_get_string(d, "reasoning_content");
|
|
if (!think)
|
|
think = json_get_string(d, "reasoning");
|
|
if (think && *think) {
|
|
if (c->reasoning.len + strlen(think) > SSE_MAX_CONTENT) {
|
|
c->overflow = 1;
|
|
} else {
|
|
sb_puts(&c->reasoning, think);
|
|
/* 不往正文回调里塞 —— 思维链不该被渲染. 单独走一路,
|
|
* 让上层知道"模型开始推理了". */
|
|
if (c->on_think)
|
|
c->on_think(c->think_ud, think);
|
|
}
|
|
}
|
|
|
|
JsonValue *tcs = json_object_get(d, "tool_calls");
|
|
if (tcs && tcs->type == JSON_ARRAY) {
|
|
for (size_t i = 0; i < json_array_size(tcs); i++) {
|
|
JsonValue *tc = json_array_get(tcs, i);
|
|
if (!tc)
|
|
continue;
|
|
size_t idx = (size_t)json_get_number(tc, "index", (double)i);
|
|
ToolAcc *a = acc_at(c, idx);
|
|
const char *id = json_get_string(tc, "id");
|
|
if (id && *id && !a->id)
|
|
a->id = xstrdup(id);
|
|
JsonValue *fn = json_object_get(tc, "function");
|
|
if (fn && fn->type == JSON_OBJECT) {
|
|
const char *name = json_get_string(fn, "name");
|
|
if (name && *name) {
|
|
if (!a->name)
|
|
a->name = xstrdup(name);
|
|
else if (strcmp(a->name, name) != 0) {
|
|
/* 有些服务端会拆着发名字, 追加处理 */
|
|
char *joined = xasprintf("%s%s", a->name, name);
|
|
free(a->name);
|
|
a->name = joined;
|
|
}
|
|
}
|
|
const char *args = json_get_string(fn, "arguments");
|
|
if (args) {
|
|
if (a->args.len + strlen(args) > SSE_MAX_TOOL_ARGS)
|
|
c->overflow = 1;
|
|
else
|
|
sb_puts(&a->args, args);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
json_free(root);
|
|
}
|
|
|
|
/* 按行切分, 逐条处理 "data:" */
|
|
static void feed(SseCtx *c, const char *data, size_t len) {
|
|
sb_append(&c->linebuf, data, len);
|
|
char *buf = c->linebuf.data;
|
|
size_t start = 0;
|
|
for (size_t i = 0; i < c->linebuf.len; i++) {
|
|
if (buf[i] != '\n')
|
|
continue;
|
|
size_t line_len = i - start;
|
|
if (line_len > 0 && buf[start + line_len - 1] == '\r')
|
|
line_len--;
|
|
const char *line = buf + start;
|
|
if (line_len > 5 && memcmp(line, "data:", 5) == 0) {
|
|
const char *p = line + 5;
|
|
size_t n = line_len - 5;
|
|
while (n > 0 && (*p == ' ' || *p == '\t')) {
|
|
p++;
|
|
n--;
|
|
}
|
|
handle_payload(c, p, n);
|
|
}
|
|
start = i + 1;
|
|
}
|
|
if (start > 0) {
|
|
memmove(buf, buf + start, c->linebuf.len - start);
|
|
c->linebuf.len -= start;
|
|
buf[c->linebuf.len] = '\0';
|
|
}
|
|
|
|
/* 残行过长: 丢弃, 保证内存有界 */
|
|
if (c->linebuf.len > SSE_MAX_LINE)
|
|
sb_clear(&c->linebuf);
|
|
}
|
|
|
|
static size_t sse_write_cb(char *ptr, size_t size, size_t nmemb, void *ud) {
|
|
SseCtx *c = ud;
|
|
size_t n = size * nmemb;
|
|
if (c->overflow)
|
|
return 0; /* 超限, 中止传输 */
|
|
|
|
if (c->raw.len < 65536) { /* 仅保留开头用于回退判断 */
|
|
size_t take = n < 65536 - c->raw.len ? n : 65536 - c->raw.len;
|
|
sb_append(&c->raw, ptr, take);
|
|
if (take < n)
|
|
c->raw_truncated = 1;
|
|
} else {
|
|
c->raw_truncated = 1;
|
|
}
|
|
feed(c, ptr, n);
|
|
if (c->overflow)
|
|
return 0;
|
|
return n;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 从整包 JSON 响应里抽出 assistant 消息 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static LlmResponse *from_full_response(JsonValue *root, size_t raw_len) {
|
|
(void)raw_len;
|
|
JsonValue *errv = json_object_get(root, "error");
|
|
if (errv && errv->type == JSON_OBJECT)
|
|
return NULL;
|
|
|
|
JsonValue *ch0 = json_array_get(json_object_get(root, "choices"), 0);
|
|
if (!ch0)
|
|
return NULL;
|
|
JsonValue *msg = json_object_get(ch0, "message");
|
|
if (!msg || msg->type != JSON_OBJECT)
|
|
return NULL;
|
|
|
|
LlmResponse *r = xcalloc(1, sizeof(LlmResponse));
|
|
r->message = json_clone(msg);
|
|
JsonValue *u = json_object_get(root, "usage");
|
|
if (u && u->type == JSON_OBJECT)
|
|
r->usage = json_clone(u);
|
|
const char *fr = json_get_string(ch0, "finish_reason");
|
|
if (fr)
|
|
r->finish_reason = xstrdup(fr);
|
|
return r;
|
|
}
|
|
|
|
/* 把累积结构整理成 LlmResponse */
|
|
static LlmResponse *from_sse(SseCtx *c) {
|
|
LlmResponse *r = xcalloc(1, sizeof(LlmResponse));
|
|
r->message = json_new_object();
|
|
json_object_set_string(r->message, "role", "assistant");
|
|
|
|
int has_tools = 0;
|
|
for (size_t i = 0; i < c->acc_count; i++)
|
|
if (c->accs[i].started) {
|
|
has_tools = 1;
|
|
break;
|
|
}
|
|
|
|
if (c->content.len > 0 || !has_tools)
|
|
json_object_set_string(r->message, "content",
|
|
c->content.data ? c->content.data : "");
|
|
else
|
|
json_object_set_null(r->message, "content");
|
|
|
|
/* 思考过程跟着 assistant 消息一起进历史. 用户看不到它, 但它必须原样回传:
|
|
* 只要请求里带 tools, 服务端就要求把历史里的 reasoning_content 全带上,
|
|
* 否则下一轮直接 400. */
|
|
if (c->reasoning.len > 0)
|
|
json_object_set_string(r->message, "reasoning_content",
|
|
c->reasoning.data ? c->reasoning.data : "");
|
|
|
|
if (has_tools) {
|
|
JsonValue *arr = json_new_array();
|
|
for (size_t i = 0; i < c->acc_count; i++) {
|
|
ToolAcc *a = &c->accs[i];
|
|
if (!a->started)
|
|
continue;
|
|
JsonValue *tc = json_new_object();
|
|
if (a->id) {
|
|
json_object_set_string(tc, "id", a->id);
|
|
} else {
|
|
char *gen = xasprintf("call_%zu", i);
|
|
json_object_set_string(tc, "id", gen);
|
|
free(gen);
|
|
}
|
|
json_object_set_string(tc, "type", "function");
|
|
JsonValue *fn = json_new_object();
|
|
json_object_set_string(fn, "name", a->name ? a->name : "");
|
|
json_object_set_string(fn, "arguments",
|
|
a->args.data ? a->args.data : "{}");
|
|
json_object_set(tc, "function", fn);
|
|
json_array_push(arr, tc);
|
|
}
|
|
json_object_set(r->message, "tool_calls", arr);
|
|
}
|
|
|
|
if (c->finish_reason)
|
|
r->finish_reason = xstrdup(c->finish_reason);
|
|
if (c->usage)
|
|
r->usage = json_clone(c->usage);
|
|
return r;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 主入口 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
LlmResponse *llm_chat(const LlmConfig *cfg, const JsonValue *messages,
|
|
const JsonValue *tools, LlmDeltaFn on_delta, void *ud,
|
|
LlmThinkFn on_think, void *think_ud, char *errbuf,
|
|
size_t errlen) {
|
|
static char errsink[1];
|
|
if (!errbuf || !errlen) {
|
|
errbuf = errsink;
|
|
errlen = 1;
|
|
}
|
|
errbuf[0] = '\0';
|
|
if (!cfg->api_key || !*cfg->api_key) {
|
|
snprintf(errbuf, errlen, "%s", t("llm.err.no_api_key"));
|
|
return NULL;
|
|
}
|
|
char *url = llm_endpoint_url(cfg->base_url);
|
|
JsonValue *body = llm_build_body(cfg, messages, tools, cfg->stream);
|
|
char *payload = json_dump(body);
|
|
json_free(body);
|
|
|
|
if (cfg->verbose)
|
|
fprintf(stderr, "[ligent] POST %s\n[ligent] body: %.2000s\n", url,
|
|
payload);
|
|
|
|
LlmResponse *result = NULL;
|
|
char http_err[512];
|
|
long status = 0;
|
|
|
|
if (cfg->stream) {
|
|
SseCtx ctx;
|
|
memset(&ctx, 0, sizeof ctx);
|
|
sb_init(&ctx.raw);
|
|
sb_init(&ctx.content);
|
|
sb_init(&ctx.reasoning);
|
|
sb_init(&ctx.linebuf);
|
|
ctx.on_delta = on_delta;
|
|
ctx.ud = ud;
|
|
ctx.on_think = on_think;
|
|
ctx.think_ud = think_ud;
|
|
|
|
char *errbody = NULL;
|
|
size_t delivered = 0;
|
|
HttpRetry rt = HTTP_RETRY_DEFAULT;
|
|
rt.max_retries = cfg->retries;
|
|
int rc = http_post_stream(url, cfg->api_key, payload, cfg->timeout,
|
|
cfg->verbose, &rt, sse_write_cb, &ctx,
|
|
&errbody, &status, NULL, &delivered, http_err,
|
|
sizeof http_err);
|
|
if (g_ligent_interrupt) {
|
|
result = xcalloc(1, sizeof(LlmResponse));
|
|
result->interrupted = 1;
|
|
result->message = json_new_object();
|
|
json_object_set_string(result->message, "role", "assistant");
|
|
json_object_set_string(result->message, "content",
|
|
ctx.content.data ? ctx.content.data : "");
|
|
if (ctx.reasoning.len > 0)
|
|
json_object_set_string(result->message, "reasoning_content",
|
|
ctx.reasoning.data ? ctx.reasoning.data
|
|
: "");
|
|
} else if (ctx.overflow) {
|
|
snprintf(errbuf, errlen, "%s",
|
|
tf("llm.err.overflow", "content",
|
|
i18n_num((long long)SSE_MAX_CONTENT), "args",
|
|
i18n_num((long long)SSE_MAX_TOOL_ARGS), NULL));
|
|
} else if (rc != 0 && (ctx.content.len > 0 || ctx.got_event)) {
|
|
/* 传输断了, 但已经收到并显示了部分内容.
|
|
* 这些内容用户已经看到, 丢掉会让上下文和屏幕对不上;
|
|
* 所以保留成一条"不完整"的消息, 交给上层提示. */
|
|
result = from_sse(&ctx);
|
|
result->incomplete = 1;
|
|
result->incomplete_reason = xstrdup(http_err);
|
|
} else if (rc != 0) {
|
|
if (errbody && *errbody) {
|
|
set_error_with_detail(errbuf, errlen, http_err, errbody);
|
|
} else if (ctx.error) {
|
|
size_t cur = (size_t)snprintf(errbuf, errlen, "%s", http_err);
|
|
if (cur + 4 < errlen)
|
|
snprintf(errbuf + cur, errlen - cur, " — %s", ctx.error);
|
|
} else {
|
|
snprintf(errbuf, errlen, "%s", http_err);
|
|
}
|
|
append_auth_hint(errbuf, errlen, cfg, status);
|
|
} else if (ctx.error) {
|
|
snprintf(errbuf, errlen, "%s",
|
|
tf("llm.err.server", "error", ctx.error, NULL));
|
|
} else if (ctx.got_event) {
|
|
result = from_sse(&ctx);
|
|
} else {
|
|
/* 服务端忽略了 stream 参数, 直接返回整包 JSON */
|
|
char *raw = xstrndup(ctx.raw.data ? ctx.raw.data : "", ctx.raw.len);
|
|
JsonValue *root = json_parse_ui(raw, http_err, sizeof http_err);
|
|
if (root) {
|
|
result = from_full_response(root, 0);
|
|
json_free(root);
|
|
}
|
|
free(raw);
|
|
if (!result && !errbuf[0]) {
|
|
if (status >= 400)
|
|
snprintf(errbuf, errlen, "HTTP %ld", status);
|
|
else
|
|
snprintf(errbuf, errlen, "%s",
|
|
t("llm.err.unparsable_response"));
|
|
}
|
|
}
|
|
free(errbody);
|
|
sse_ctx_free(&ctx);
|
|
} else {
|
|
char *resp = NULL;
|
|
HttpRetry rt = HTTP_RETRY_DEFAULT;
|
|
rt.max_retries = cfg->retries;
|
|
int rc =
|
|
http_post(url, cfg->api_key, payload, cfg->timeout, cfg->verbose,
|
|
&rt, &resp, &status, NULL, http_err, sizeof http_err);
|
|
if (rc != 0) {
|
|
if (resp && *resp)
|
|
set_error_with_detail(errbuf, errlen, http_err, resp);
|
|
else
|
|
snprintf(errbuf, errlen, "%s", http_err);
|
|
append_auth_hint(errbuf, errlen, cfg, status);
|
|
} else {
|
|
JsonValue *root =
|
|
json_parse_ui(resp ? resp : "", http_err, sizeof http_err);
|
|
if (root) {
|
|
JsonValue *errv = json_object_get(root, "error");
|
|
if (errv && errv->type == JSON_OBJECT) {
|
|
const char *m = json_get_string(errv, "message");
|
|
snprintf(errbuf, errlen, "%s",
|
|
tf("llm.err.server", "error",
|
|
m ? m : t("llm.val.unknown"), NULL));
|
|
} else {
|
|
result = from_full_response(root, 0);
|
|
if (!result)
|
|
snprintf(errbuf, errlen, "%s", t("llm.err.no_choices"));
|
|
}
|
|
json_free(root);
|
|
} else {
|
|
char head[201];
|
|
snprintf(head, sizeof head, "%.200s", http_err);
|
|
snprintf(errbuf, errlen, "%s",
|
|
tf("llm.err.bad_json", "error", head, NULL));
|
|
}
|
|
}
|
|
free(resp);
|
|
}
|
|
|
|
free(payload);
|
|
free(url);
|
|
return result;
|
|
}
|
|
|
|
void llm_response_free(LlmResponse *r) {
|
|
if (!r)
|
|
return;
|
|
json_free(r->message);
|
|
json_free(r->usage);
|
|
free(r->finish_reason);
|
|
free(r->incomplete_reason);
|
|
free(r);
|
|
}
|