v1.0 - launch: full nav on gallery, chat widget auto-open, comparison logos, checkout fixes
This commit is contained in:
+157
-59
@@ -1895,12 +1895,28 @@ el_val_t json_get(el_val_t jsonv, el_val_t keyv) {
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n') p++;
|
||||
if (*p == '"') {
|
||||
p++;
|
||||
const char* start = p;
|
||||
while (*p && !(*p == '"' && *(p-1) != '\\')) p++;
|
||||
size_t len = (size_t)(p - start);
|
||||
char* out = el_strbuf(len);
|
||||
memcpy(out, start, len);
|
||||
out[len] = '\0';
|
||||
/* Unescape the JSON string value into a clean buffer. */
|
||||
size_t cap = strlen(p) + 1;
|
||||
char* out = el_strbuf(cap);
|
||||
char* w = out;
|
||||
while (*p && *p != '"') {
|
||||
if (*p == '\\' && *(p+1)) {
|
||||
p++;
|
||||
switch (*p) {
|
||||
case '"': *w++ = '"'; break;
|
||||
case '\\': *w++ = '\\'; break;
|
||||
case '/': *w++ = '/'; break;
|
||||
case 'n': *w++ = '\n'; break;
|
||||
case 'r': *w++ = '\r'; break;
|
||||
case 't': *w++ = '\t'; break;
|
||||
default: *w++ = *p; break;
|
||||
}
|
||||
} else {
|
||||
*w++ = *p;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
*w = '\0';
|
||||
return el_wrap_str(out);
|
||||
}
|
||||
const char* start = p;
|
||||
@@ -4534,30 +4550,143 @@ static const char* llm_resolve_model(const char* m) {
|
||||
return m;
|
||||
}
|
||||
|
||||
/* Make an Anthropic /v1/messages request with the given JSON body. Returns
|
||||
* the assistant's first text content as an owned string, or a JSON error
|
||||
* fragment on transport failure. */
|
||||
static el_val_t llm_request(const char* json_body) {
|
||||
const char* api_key = getenv("ANTHROPIC_API_KEY");
|
||||
if (!api_key || !*api_key) {
|
||||
return http_error_json("ANTHROPIC_API_KEY not set");
|
||||
/*
|
||||
* ── Configurable LLM provider chain ──────────────────────────────────────────
|
||||
*
|
||||
* Providers are configured via indexed env vars. The runtime tries each in
|
||||
* order (0, 1, 2, ...) and returns the first successful non-empty response.
|
||||
*
|
||||
* Per provider (N = 0, 1, 2, ...):
|
||||
* NEURON_LLM_N_URL — endpoint URL (base URL; /v1/chat/completions appended
|
||||
* if format is "openai" and not already in URL)
|
||||
* NEURON_LLM_N_KEY — API key
|
||||
* NEURON_LLM_N_FORMAT — "openai" (default) or "anthropic"
|
||||
* NEURON_LLM_N_MODEL — model name override (optional)
|
||||
*
|
||||
* Example — Neuron inference primary, Anthropic fallback:
|
||||
* NEURON_LLM_0_URL=https://soma.../v1/chat/completions
|
||||
* NEURON_LLM_0_KEY=svc-key
|
||||
* NEURON_LLM_0_FORMAT=openai
|
||||
* NEURON_LLM_0_MODEL=neuron
|
||||
* NEURON_LLM_1_URL=https://api.anthropic.com/v1/messages
|
||||
* NEURON_LLM_1_KEY=sk-ant-...
|
||||
* NEURON_LLM_1_FORMAT=anthropic
|
||||
*
|
||||
* If no NEURON_LLM_0_URL is set, falls back to legacy ANTHROPIC_API_KEY.
|
||||
*/
|
||||
|
||||
#define LLM_MAX_PROVIDERS 16
|
||||
|
||||
/* forward declarations */
|
||||
static el_val_t llm_extract_text(el_val_t resp_val);
|
||||
static el_val_t llm_extract_text_openai(el_val_t resp_val);
|
||||
|
||||
static el_val_t llm_extract_text_openai(el_val_t resp_val) {
|
||||
const char* resp = EL_CSTR(resp_val);
|
||||
if (!resp || !*resp) return el_wrap_str(el_strdup(""));
|
||||
if (resp[0] == '{' && strstr(resp, "\"error\"")) return el_wrap_str(el_strdup(""));
|
||||
const char* choices = json_find_key(resp, "choices");
|
||||
if (!choices || *choices != '[') return el_wrap_str(el_strdup(""));
|
||||
choices++;
|
||||
while (*choices == ' ' || *choices == '\t') choices++;
|
||||
if (*choices != '{') return el_wrap_str(el_strdup(""));
|
||||
const char* end = json_skip_value(choices);
|
||||
size_t n = (size_t)(end - choices);
|
||||
char* obj = malloc(n + 1); memcpy(obj, choices, n); obj[n] = '\0';
|
||||
const char* msg = json_find_key(obj, "message");
|
||||
if (!msg || *msg != '{') { free(obj); return el_wrap_str(el_strdup("")); }
|
||||
const char* msg_end = json_skip_value(msg);
|
||||
size_t mn = (size_t)(msg_end - msg);
|
||||
char* msg_obj = malloc(mn + 1); memcpy(msg_obj, msg, mn); msg_obj[mn] = '\0';
|
||||
const char* content = json_find_key(msg_obj, "content");
|
||||
el_val_t result = el_wrap_str(el_strdup(""));
|
||||
if (content && *content == '"') {
|
||||
JsonParser jp = { .p = content, .end = content + strlen(content), .err = 0 };
|
||||
char* text = jp_parse_string_raw(&jp);
|
||||
if (!jp.err && text) result = el_wrap_str(text);
|
||||
}
|
||||
free(msg_obj); free(obj);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Send a request to one provider. Returns the raw response string.
|
||||
* format: 0 = openai, 1 = anthropic */
|
||||
static el_val_t llm_provider_request(const char* url, const char* key,
|
||||
int format, const char* model,
|
||||
const char* system_str,
|
||||
const char* user_str) {
|
||||
char* esc_sys = system_str && *system_str ? json_escape_alloc(system_str) : NULL;
|
||||
char* esc_user = json_escape_alloc(user_str ? user_str : "");
|
||||
JsonBuf b; jb_init(&b);
|
||||
struct curl_slist* h = NULL;
|
||||
h = curl_slist_append(h, "Content-Type: application/json");
|
||||
{
|
||||
size_t n = strlen(api_key) + 16;
|
||||
char* line = malloc(n);
|
||||
snprintf(line, n, "x-api-key: %s", api_key);
|
||||
h = curl_slist_append(h, line);
|
||||
free(line);
|
||||
|
||||
if (format == 0) { /* OpenAI */
|
||||
char full_url[1024];
|
||||
if (strstr(url, "/chat/completions") || strstr(url, "/messages")) {
|
||||
snprintf(full_url, sizeof(full_url), "%s", url);
|
||||
} else {
|
||||
snprintf(full_url, sizeof(full_url), "%s/v1/chat/completions", url);
|
||||
}
|
||||
{ size_t n = strlen(key)+24; char* l=malloc(n); snprintf(l,n,"Authorization: Bearer %s",key); h=curl_slist_append(h,l); free(l); }
|
||||
jb_putc(&b, '{');
|
||||
jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : "neuron");
|
||||
jb_puts(&b, ",\"max_tokens\":4096,\"messages\":[");
|
||||
if (esc_sys && *esc_sys) { jb_puts(&b,"{\"role\":\"system\",\"content\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\"},"); }
|
||||
jb_puts(&b, "{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}");
|
||||
el_val_t resp = http_do("POST", full_url, b.buf, h);
|
||||
curl_slist_free_all(h); free(b.buf);
|
||||
if (esc_sys) free(esc_sys); free(esc_user);
|
||||
return llm_extract_text_openai(resp);
|
||||
} else { /* Anthropic */
|
||||
{ size_t n = strlen(key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",key); h=curl_slist_append(h,l); free(l); }
|
||||
{ size_t n = strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); }
|
||||
jb_putc(&b, '{');
|
||||
jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, model ? model : LLM_DEFAULT_MODEL);
|
||||
jb_puts(&b, ",\"max_tokens\":4096");
|
||||
if (esc_sys && *esc_sys) { jb_puts(&b,",\"system\":\""); jb_puts(&b,esc_sys); jb_puts(&b,"\""); }
|
||||
jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\""); jb_puts(&b, esc_user); jb_puts(&b, "\"}]}");
|
||||
el_val_t resp = http_do("POST", url, b.buf, h);
|
||||
curl_slist_free_all(h); free(b.buf);
|
||||
if (esc_sys) free(esc_sys); free(esc_user);
|
||||
return llm_extract_text(resp);
|
||||
}
|
||||
{
|
||||
size_t n = strlen(LLM_VERSION) + 32;
|
||||
char* line = malloc(n);
|
||||
snprintf(line, n, "anthropic-version: %s", LLM_VERSION);
|
||||
h = curl_slist_append(h, line);
|
||||
free(line);
|
||||
}
|
||||
|
||||
static el_val_t llm_chain_call(const char* system_str, const char* user_str) {
|
||||
char url_key[64], key_key[64], fmt_key[64], model_key[64];
|
||||
for (int i = 0; i < LLM_MAX_PROVIDERS; i++) {
|
||||
snprintf(url_key, sizeof(url_key), "NEURON_LLM_%d_URL", i);
|
||||
snprintf(key_key, sizeof(key_key), "NEURON_LLM_%d_KEY", i);
|
||||
snprintf(fmt_key, sizeof(fmt_key), "NEURON_LLM_%d_FORMAT", i);
|
||||
snprintf(model_key, sizeof(model_key), "NEURON_LLM_%d_MODEL", i);
|
||||
const char* url = getenv(url_key);
|
||||
const char* key = getenv(key_key);
|
||||
if (!url || !*url || !key || !*key) break; /* end of chain */
|
||||
const char* fmt_s = getenv(fmt_key);
|
||||
int fmt = (fmt_s && strcmp(fmt_s, "anthropic") == 0) ? 1 : 0;
|
||||
const char* model = getenv(model_key);
|
||||
fprintf(stderr, "[llm] trying provider %d (%s)\n", i, url);
|
||||
el_val_t result = llm_provider_request(url, key, fmt, model, system_str, user_str);
|
||||
const char* t = EL_CSTR(result);
|
||||
if (t && *t && t[0] != '{') return result; /* success */
|
||||
fprintf(stderr, "[llm] provider %d failed or empty, trying next\n", i);
|
||||
}
|
||||
/* Legacy fallback: ANTHROPIC_API_KEY */
|
||||
const char* api_key = getenv("ANTHROPIC_API_KEY");
|
||||
if (!api_key || !*api_key) return http_error_json("no LLM providers configured");
|
||||
fprintf(stderr, "[llm] using legacy ANTHROPIC_API_KEY fallback\n");
|
||||
return llm_provider_request(LLM_API_URL, api_key, 1, NULL, system_str, user_str);
|
||||
}
|
||||
|
||||
/* Legacy llm_request — kept for backward compat with agentic loop internals */
|
||||
static el_val_t llm_request(const char* json_body) {
|
||||
const char* api_key = getenv("ANTHROPIC_API_KEY");
|
||||
if (!api_key || !*api_key) return http_error_json("ANTHROPIC_API_KEY not set");
|
||||
struct curl_slist* h = NULL;
|
||||
h = curl_slist_append(h, "Content-Type: application/json");
|
||||
{ size_t n=strlen(api_key)+16; char* l=malloc(n); snprintf(l,n,"x-api-key: %s",api_key); h=curl_slist_append(h,l); free(l); }
|
||||
{ size_t n=strlen(LLM_VERSION)+32; char* l=malloc(n); snprintf(l,n,"anthropic-version: %s",LLM_VERSION); h=curl_slist_append(h,l); free(l); }
|
||||
el_val_t resp = http_do("POST", LLM_API_URL, json_body, h);
|
||||
curl_slist_free_all(h);
|
||||
return resp;
|
||||
@@ -4611,45 +4740,14 @@ static el_val_t llm_extract_text(el_val_t resp_val) {
|
||||
}
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt) {
|
||||
const char* m = llm_resolve_model(EL_CSTR(model));
|
||||
const char* u = EL_CSTR(prompt);
|
||||
if (!u) u = "";
|
||||
char* esc_user = json_escape_alloc(u);
|
||||
JsonBuf b; jb_init(&b);
|
||||
jb_putc(&b, '{');
|
||||
jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m);
|
||||
jb_puts(&b, ",\"max_tokens\":4096");
|
||||
jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\"");
|
||||
jb_puts(&b, esc_user);
|
||||
jb_puts(&b, "\"}]}");
|
||||
free(esc_user);
|
||||
el_val_t resp = llm_request(b.buf);
|
||||
free(b.buf);
|
||||
return llm_extract_text(resp);
|
||||
const char* u = EL_CSTR(prompt); if (!u) u = "";
|
||||
return llm_chain_call(NULL, u);
|
||||
}
|
||||
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt) {
|
||||
const char* m = llm_resolve_model(EL_CSTR(model));
|
||||
const char* s = EL_CSTR(system_prompt); if (!s) s = "";
|
||||
const char* u = EL_CSTR(user_prompt); if (!u) u = "";
|
||||
char* esc_sys = json_escape_alloc(s);
|
||||
char* esc_user = json_escape_alloc(u);
|
||||
JsonBuf b; jb_init(&b);
|
||||
jb_putc(&b, '{');
|
||||
jb_puts(&b, "\"model\":"); jb_emit_escaped(&b, m);
|
||||
jb_puts(&b, ",\"max_tokens\":4096");
|
||||
if (*s) {
|
||||
jb_puts(&b, ",\"system\":\"");
|
||||
jb_puts(&b, esc_sys);
|
||||
jb_puts(&b, "\"");
|
||||
}
|
||||
jb_puts(&b, ",\"messages\":[{\"role\":\"user\",\"content\":\"");
|
||||
jb_puts(&b, esc_user);
|
||||
jb_puts(&b, "\"}]}");
|
||||
free(esc_sys); free(esc_user);
|
||||
el_val_t resp = llm_request(b.buf);
|
||||
free(b.buf);
|
||||
return llm_extract_text(resp);
|
||||
return llm_chain_call(s, u);
|
||||
}
|
||||
|
||||
/* ── Tool registry for llm_call_agentic ─────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user