diff options
Diffstat (limited to 'src/agent.cpp')
| -rw-r--r-- | src/agent.cpp | 435 |
1 files changed, 435 insertions, 0 deletions
diff --git a/src/agent.cpp b/src/agent.cpp new file mode 100644 index 0000000..d1357b7 --- /dev/null +++ b/src/agent.cpp @@ -0,0 +1,435 @@ +#include "agent.h" + +#include <curl/curl.h> +#include <cmath> +#include <limits> +#include <stdlib.h> +#include <string.h> + +static char *trim_slashes(char *s) { + size_t n = strlen(s); + while (n && s[n - 1] == '/') + s[--n] = '\0'; + return s; +} + +void provider_probe_free(provider_probe *p) { + free(p->detail); + p->detail = NULL; +} + +const char *agent_default_base_url(provider_kind p) { + switch (p) { + case PROV_OPENAI: + return "https://api.openai.com/v1"; + case PROV_ANTHROPIC: + return "https://api.anthropic.com"; + case PROV_OLLAMA: + return "http://localhost:11434"; + } + return "https://api.openai.com/v1"; +} + +char *agent_models_url(const config *cfg) { + char *base = xstrdup(cfg->base_url ? cfg->base_url + : agent_default_base_url(cfg->provider)); + trim_slashes(base); + xbuf u; + xbuf_init(&u); + xbuf_put(&u, base); + switch (cfg->provider) { + case PROV_OPENAI: + xbuf_push(&u, '/'); + xbuf_put(&u, "models"); + break; + case PROV_ANTHROPIC: { + /* The canonical base is api.anthropic.com; the /v1 prefix belongs to + * the path. If the caller already included /v1 in base_url, don't + * duplicate it (avoids .../v1/v1/models). */ + size_t blen = strlen(base); + if (blen >= 3 && memcmp(base + blen - 3, "/v1", 3) == 0) + xbuf_put(&u, "/models?limit=100"); + else + xbuf_put(&u, "/v1/models?limit=100"); + break; + } + case PROV_OLLAMA: { + xbuf_put(&u, "/api/show?name="); + char *enc = cfg->model ? curl_easy_escape(NULL, cfg->model, 0) : NULL; + xbuf_put(&u, enc ? enc : ""); + free(enc); + break; + } + } + char *out = xbuf_steal(&u); + free(base); + return out; +} + +void agent_auth(const config *cfg, char **header, char **value) { + if (header) + *header = NULL; + if (value) + *value = NULL; + switch (cfg->provider) { + case PROV_OPENAI: + if (cfg->api_key) { + if (header) + *header = xstrdup("Authorization"); + xbuf b; + xbuf_init(&b); + xbuf_fmt(&b, "Bearer %s", cfg->api_key); + if (value) + *value = xbuf_steal(&b); + else + xbuf_free(&b); + } + break; + case PROV_ANTHROPIC: + if (cfg->api_key) { + if (header) + *header = xstrdup("x-api-key"); + if (value) + *value = xstrdup(cfg->api_key); + } + break; + case PROV_OLLAMA: + break; + } +} + +int agent_http_get(const char *url, const char *auth_header, + const char *auth_value, long *status, char **body, + char **err) { + if (body) + *body = NULL; + if (err) + *err = NULL; + CURL *c = curl_easy_init(); + if (!c) { + if (err) + *err = xstrdup("could not initialize curl"); + return -1; + } + xbuf b; + xbuf_init(&b); + struct curl_slist *h = NULL; + if (auth_header && auth_value) { + xbuf hb; + xbuf_init(&hb); + xbuf_fmt(&hb, "%s: %s", auth_header, auth_value); + h = curl_slist_append(h, hb.data); + xbuf_free(&hb); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); + } + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_TIMEOUT, 5L); + curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); + const char *ca = llsh_ca_bundle(); + if (ca) + curl_easy_setopt(c, CURLOPT_CAINFO, ca); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, xbuf_curl_write); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &b); + CURLcode rc = curl_easy_perform(c); + curl_slist_free_all(h); + if (rc != CURLE_OK) { + char *e = xstrdup(curl_easy_strerror(rc)); + free(b.data); + curl_easy_cleanup(c); + if (err) + *err = e; + return -1; + } + long code = 0; + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code); + if (status) + *status = code; + if (body) + *body = xbuf_steal(&b); + else + free(b.data); + curl_easy_cleanup(c); + return 0; +} + +static bool str_eq(const cJSON *v, const char *s) { + return cJSON_IsString(v) && v->valuestring && + strcmp(v->valuestring, s) == 0; +} + +static unsigned long num_u64(const cJSON *v) { + if (cJSON_IsNumber(v) && std::isfinite(v->valuedouble) && + v->valuedouble >= 0 && std::floor(v->valuedouble) == v->valuedouble && + v->valuedouble < (double)std::numeric_limits<unsigned long>::max()) + return (unsigned long)v->valuedouble; + return 0; +} + +static bool openai_model_matches_entry(const cJSON *entry, const char *model) { + if (str_eq(cJSON_GetObjectItem(entry, "id"), model)) + return true; + if (str_eq(cJSON_GetObjectItem(entry, "name"), model)) + return true; + const cJSON *al = cJSON_GetObjectItem(entry, "aliases"); + if (cJSON_IsArray(al)) { + const cJSON *x = NULL; + cJSON_ArrayForEach(x, al) + if (str_eq(x, model)) + return true; + } + return false; +} + +bool openai_model_present(const char *body, const char *model) { + cJSON *v = cJSON_Parse(body); + if (!v) + return false; + bool found = false; + cJSON *arr = cJSON_GetObjectItem(v, "data"); + if (!arr) + arr = cJSON_GetObjectItem(v, "models"); + if (cJSON_IsArray(arr)) { + const cJSON *m = NULL; + cJSON_ArrayForEach(m, arr) + if (openai_model_matches_entry(m, model)) { + found = true; + break; + } + } + cJSON_Delete(v); + return found; +} + +bool anthropic_model_present(const char *body, const char *model) { + cJSON *v = cJSON_Parse(body); + if (!v) + return false; + bool found = false; + const cJSON *arr = cJSON_GetObjectItem(v, "data"); + if (cJSON_IsArray(arr)) { + const cJSON *m = NULL; + cJSON_ArrayForEach(m, arr) + if (str_eq(cJSON_GetObjectItem(m, "id"), model)) { + found = true; + break; + } + } + cJSON_Delete(v); + return found; +} + +unsigned long extract_context_limit(const cJSON *json, const config *cfg) { + if (!json) + return 0; + switch (cfg->provider) { + case PROV_OPENAI: { + unsigned long v = num_u64(cJSON_GetObjectItem(json, "context_length")); + if (v) + return v; + const cJSON *data = cJSON_GetObjectItem(json, "data"); + if (!cJSON_IsArray(data)) + return 0; + const cJSON *want = NULL; + const cJSON *m = NULL; + cJSON_ArrayForEach(m, data) + if (openai_model_matches_entry(m, cfg->model)) { + want = m; + break; + } + if (!want && data->child) + want = data->child; + if (!want) + return 0; + v = num_u64(cJSON_GetObjectItem(want, "context_length")); + if (v) + return v; + v = num_u64(cJSON_GetObjectItem(cJSON_GetObjectItem(want, "meta"), + "n_ctx")); + if (v) + return v; + return num_u64(cJSON_GetObjectItem(want, "context_window")); + } + case PROV_OLLAMA: { + unsigned long v = num_u64(cJSON_GetObjectItem( + cJSON_GetObjectItem(json, "model_details"), "context_length")); + if (v) + return v; + const cJSON *mf = cJSON_GetObjectItem(json, "modelfiles"); + if (cJSON_IsArray(mf) && mf->child) + v = num_u64(cJSON_GetObjectItem( + cJSON_GetObjectItem(mf->child, "parameters"), + "context_length")); + return v; + } + case PROV_ANTHROPIC: { + const cJSON *data = cJSON_GetObjectItem(json, "data"); + if (!cJSON_IsArray(data) || !data->child) + return 0; + const cJSON *want = NULL; + const cJSON *m = NULL; + cJSON_ArrayForEach(m, data) + if (str_eq(cJSON_GetObjectItem(m, "id"), cfg->model)) { + want = m; + break; + } + if (!want) + want = data->child; + return num_u64(cJSON_GetObjectItem(want, "max_tokens")); + } + } + return 0; +} + +unsigned long agent_fetch_context_limit(const config *cfg) { + char *url = agent_models_url(cfg); + char *h = NULL, *v = NULL; + agent_auth(cfg, &h, &v); + long status = 0; + char *body = NULL; + char *err = NULL; + int rc = agent_http_get(url, h, v, &status, &body, &err); + free(h); + free(v); + free(url); + free(err); + if (rc != 0) { + free(body); + return 0; + } + unsigned long limit = 0; + /* No status gate (parity): a non-2xx body just has to parse and carry + * the field. */ + cJSON *j = cJSON_Parse(body); + if (j) { + limit = extract_context_limit(j, cfg); + cJSON_Delete(j); + } + free(body); + (void)status; + return limit; +} + +static const char *status_phrase(long code) { + switch (code) { + case 200: return "200 OK"; + case 201: return "201 Created"; + case 204: return "204 No Content"; + case 400: return "400 Bad Request"; + case 401: return "401 Unauthorized"; + case 403: return "403 Forbidden"; + case 404: return "404 Not Found"; + case 405: return "405 Method Not Allowed"; + case 408: return "408 Request Timeout"; + case 429: return "429 Too Many Requests"; + case 500: return "500 Internal Server Error"; + case 502: return "502 Bad Gateway"; + case 503: return "503 Service Unavailable"; + case 504: return "504 Gateway Timeout"; + default: { + static char buf[32]; + snprintf(buf, sizeof buf, "%ld", code); + return buf; + } + } +} + +provider_probe agent_probe_provider(const config *cfg) { + provider_probe p = {false, NULL}; + char *base = xstrdup(cfg->base_url ? cfg->base_url + : agent_default_base_url(cfg->provider)); + trim_slashes(base); + char *url = agent_models_url(cfg); + char *h = NULL, *v = NULL; + agent_auth(cfg, &h, &v); + long status = 0; + char *body = NULL; + char *err = NULL; + if (agent_http_get(url, h, v, &status, &body, &err) != 0) { + xbuf fb; + xbuf_init(&fb); + xbuf_fmt(&fb, "could not reach %s (connection: %s); is the endpoint " + "up and reachable?", + base, err ? err : "unknown"); + free(err); + free(h); + free(v); + free(url); + free(base); + free(body); + p.detail = xbuf_steal(&fb); + return p; + } + free(h); + free(v); + free(url); + bool present = false; + switch (cfg->provider) { + case PROV_OPENAI: + present = openai_model_present(body ? body : "", cfg->model); + break; + case PROV_ANTHROPIC: + present = anthropic_model_present(body ? body : "", cfg->model); + break; + case PROV_OLLAMA: + present = (status >= 200 && status < 300); + break; + } + bool success = status >= 200 && status < 300; + bool client_err = status >= 400 && status < 500; + xbuf fb; + xbuf_init(&fb); + switch (cfg->provider) { + case PROV_OPENAI: + if (success) { + if (present) { + p.ok = true; + xbuf_fmt(&fb, "model `%s` found in the provider list at %s", + cfg->model, base); + } else { + xbuf_fmt(&fb, "model `%s` not in the provider's model list " + "at %s", + cfg->model, base); + } + } else if (client_err) { + xbuf_fmt(&fb, "provider returned %s for %s/models (check api_key)", + status_phrase(status), base); + } else { + xbuf_fmt(&fb, "provider returned %s for %s/models", + status_phrase(status), base); + } + break; + case PROV_ANTHROPIC: + if (success) { + if (present) { + p.ok = true; + xbuf_fmt(&fb, "model `%s` found in the provider list", + cfg->model); + } else { + xbuf_fmt(&fb, "model `%s` not in the provider's model list " + "at %s", + cfg->model, base); + } + } else if (client_err) { + xbuf_fmt(&fb, "provider returned %s for %s/v1/models (check " + "api_key)", + status_phrase(status), base); + } else { + xbuf_fmt(&fb, "provider returned %s for %s/v1/models", + status_phrase(status), base); + } + break; + case PROV_OLLAMA: + if (success) { + p.ok = true; + xbuf_fmt(&fb, "model `%s` found via Ollama", cfg->model); + } else { + xbuf_fmt(&fb, "Ollama returned %s at %s (model `%s`)", + status_phrase(status), base, cfg->model); + } + break; + } + free(body); + p.detail = xbuf_steal(&fb); + free(base); + return p; +} |