// SPDX-License-Identifier: ISC#include <errno.h>#include <stdbool.h>#include <stddef.h>#include <stdio.h>#include <stdlib.h>#include <string.h> #include <curl/curl.h>#include <uv.h> #include "clm/http_async.h"#include "clm/log.h"#include "clm/cleanup.h"#include "useful.h"#include "version.h"#include "banned.h" /* Base User-Agent: product token, version, and a homepage comment. Kept * minimal (no OS/arch/stack leakage) by design; an optional per-request * "(tool: <name>)" comment is appended for tool/plugin attribution. */#define CLM_HOMEPAGE "https://github.com/mischief/clm"#define CLM_UA_BASE "clm/" CLM_VERSION " (+" CLM_HOMEPAGE ")" /* HTTP request state. */enum clm_http_request_state { CLM_HTTP_PENDING, CLM_HTTP_RUNNING, CLM_HTTP_DONE, CLM_HTTP_ERROR,}; /* Async HTTP request response buffer. */struct http_buf { char *data; size_t len;}; /* Per-socket poll context, allocated by the socket callback. */struct clm_http_socket { uv_poll_t poll; curl_socket_t sockfd; struct clm_http_request *req;}; /* Async HTTP request context (opaque to callers; see clm/http_async.h). */struct clm_http_request { uv_loop_t *uv; /* Curl handles */ CURLM *multi_handle; CURL *easy_handle; struct curl_slist *headers; /* Response buffer */ struct http_buf response_buf; int events_pending; /* UV timer handle */ uv_timer_t timer_handle; int timer_initialized; /* Callbacks and user data */ clm_http_success_cb success_cb; clm_http_error_cb error_cb; clm_http_data_cb data_cb; void *user; /* State */ enum clm_http_request_state state; int error_code; char error_msg[256]; int closing; int handles_to_close;}; static size_thttp_write_callback(void *contents, size_t size, size_t nmemb, void *userp){ size_t realsize = size * nmemb; struct clm_http_request *req = userp; struct http_buf *buf = &req->response_buf; char *ptr; ptr = realloc(buf->data, buf->len + realsize + 1); if (ptr == NULL) return 0; buf->data = ptr; memcpy(buf->data + buf->len, contents, realsize); buf->len += realsize; buf->data[buf->len] = '\0'; /* Forward chunks to a streaming consumer, but only for a 2xx response so * it never sees an error body. */ if (req->data_cb != NULL) { long code = 0; curl_easy_getinfo(req->easy_handle, CURLINFO_RESPONSE_CODE, &code); if (code >= 200 && code < 300) req->data_cb(contents, realsize, req->user); } return realsize;} static void http_handle_closed(uv_handle_t *handle);static void http_request_teardown(struct clm_http_request *req);static int http_timer_callback(CURLM *multi, long timeout_ms, void *userp);static void http_timer_expired(uv_timer_t *handle); /* * Drain curl's message queue after a socket_action. If a transfer finished, * record its outcome on req and return true so the caller tears down. Both the * socket-poll and timer paths must call this: a transfer can complete off the * timer (e.g. TLS/handshake progress or a fast response driven by a 0ms * timeout), and if only the poll path reaps CURLMSG_DONE the timer keeps * re-arming at 0ms and the loop spins at 100% CPU. */static boolhttp_reap_done(struct clm_http_request *req){ int msgs_left; CURLMsg *msg; while ((msg = curl_multi_info_read(req->multi_handle, &msgs_left)) != NULL) { if (msg->msg != CURLMSG_DONE) continue; CURLcode curl_err = msg->data.result; if (curl_err == CURLE_OK) { req->state = CLM_HTTP_DONE; clm_debug("CURLMSG_DONE, CURLE_OK"); } else { req->state = CLM_HTTP_ERROR; req->error_code = (int)curl_err; snprintf(req->error_msg, sizeof(req->error_msg), "curl error: %s", curl_easy_strerror(curl_err)); clm_debug("CURLMSG_DONE, curl_err=%d", curl_err); } return true; } return false;} static voidhttp_poll_callback(uv_poll_t *handle, int status, int events){ struct clm_http_socket *ctx = handle->data; struct clm_http_request *req = ctx->req; int curl_action = 0; clm_debug("status=%d, events=%d, fd=%d", status, events, ctx->sockfd); if (status < 0) { req->state = CLM_HTTP_ERROR; req->error_code = -status; snprintf(req->error_msg, sizeof(req->error_msg), "poll error: %s", uv_err_name(-status)); clm_debug("poll error, status=%d", status); goto done; } if (events & UV_READABLE) curl_action |= CURL_POLL_IN; if (events & UV_WRITABLE) curl_action |= CURL_POLL_OUT; clm_debug("curl_action=%d", curl_action); curl_multi_socket_action(req->multi_handle, ctx->sockfd, curl_action, &req->events_pending); clm_debug("curl_multi_socket_action completed"); if (http_reap_done(req)) goto done; return; done: clm_debug("calling http_request_teardown"); http_request_teardown(req);} static voidon_socket_closed(uv_handle_t *handle){ struct clm_http_socket *ctx = handle->data; free(ctx);} static inthttp_socket_callback(CURL *easy, curl_socket_t s, int action, void *userp, void *socketp){ struct clm_http_request *req = userp; struct clm_http_socket *ctx = socketp; clm_debug("action=%d, s=%d", action, s); if (action == CURL_POLL_REMOVE) { if (ctx != NULL) { uv_poll_stop(&ctx->poll); uv_close((uv_handle_t *)&ctx->poll, on_socket_closed); curl_multi_assign(req->multi_handle, s, NULL); } return 0; } if (ctx == NULL) { ctx = calloc(1, sizeof(*ctx)); if (ctx == NULL) return 0; ctx->sockfd = s; ctx->req = req; ctx->poll.data = ctx; uv_poll_init_socket(req->uv, &ctx->poll, s); curl_multi_assign(req->multi_handle, s, ctx); clm_debug("uv_poll_init_socket success, sockfd=%d", s); } int events = 0; if (action & CURL_POLL_IN) events |= UV_READABLE; if (action & CURL_POLL_OUT) events |= UV_WRITABLE; clm_debug("events=%d", events); if (!uv_is_closing((uv_handle_t *)&ctx->poll)) uv_poll_start(&ctx->poll, events, http_poll_callback); return 0;} intclm_http_async_post(uv_loop_t *loop, const char *url, const char *api_key, const char *json_body, struct curl_slist *extra_headers, clm_http_success_cb success_cb, clm_http_error_cb error_cb, clm_http_data_cb data_cb, const char *client_suffix, void *user, struct clm_http_request **out_req){ struct clm_http_request *req; char *auth_header; size_t auth_header_len; clm_debug("starting"); if (out_req != NULL) *out_req = NULL; ASSERT_RETURN(loop != NULL, -EINVAL); ASSERT_RETURN(url != NULL, -EINVAL); /* json_body == NULL performs a GET (used for the health check). */ ASSERT_RETURN(success_cb != NULL, -EINVAL); ASSERT_RETURN(error_cb != NULL, -EINVAL); req = calloc(1, sizeof(*req)); if (req == NULL) { clm_debug("calloc failed"); return -ENOMEM; } clm_debug("req allocated"); req->uv = loop; req->success_cb = success_cb; req->error_cb = error_cb; req->data_cb = data_cb; req->user = user; req->state = CLM_HTTP_PENDING; req->response_buf.data = NULL; req->response_buf.len = 0; req->multi_handle = curl_multi_init(); if (req->multi_handle == NULL) { clm_debug("curl_multi_init failed"); free(req); return -ENOMEM; } clm_debug("curl_multi_init success"); req->easy_handle = curl_easy_init(); if (req->easy_handle == NULL) { clm_debug("curl_easy_init failed"); curl_multi_cleanup(req->multi_handle); free(req); return -ENOMEM; } clm_debug("curl_easy_init success"); if (api_key != NULL && api_key[0] != '\0') { auth_header_len = sizeof("Authorization: Bearer ") + strlen(api_key); auth_header = malloc(auth_header_len); if (auth_header == NULL) { clm_debug("auth_header alloc failed"); curl_easy_cleanup(req->easy_handle); curl_multi_cleanup(req->multi_handle); free(req); return -ENOMEM; } (void)snprintf(auth_header, auth_header_len, "Authorization: Bearer %s", api_key); req->headers = curl_slist_append(NULL, "Content-Type: application/json"); req->headers = curl_slist_append(req->headers, auth_header); free(auth_header); } else { req->headers = NULL; } /* Append any caller-provided extra headers. */ if (extra_headers != NULL) { struct curl_slist *h; for (h = extra_headers; h != NULL; h = h->next) req->headers = curl_slist_append(req->headers, h->data); curl_slist_free_all(extra_headers); } curl_easy_setopt(req->easy_handle, CURLOPT_URL, url); if (json_body != NULL) { curl_easy_setopt(req->easy_handle, CURLOPT_POST, 1L); curl_easy_setopt(req->easy_handle, CURLOPT_POSTFIELDS, json_body); } else { curl_easy_setopt(req->easy_handle, CURLOPT_HTTPGET, 1L); } curl_easy_setopt(req->easy_handle, CURLOPT_WRITEFUNCTION, http_write_callback); curl_easy_setopt(req->easy_handle, CURLOPT_WRITEDATA, req); if (req->headers != NULL) curl_easy_setopt(req->easy_handle, CURLOPT_HTTPHEADER, req->headers); /* Minimal, deliberate User-Agent; append a tool/plugin comment if given. */ if (client_suffix != NULL && client_suffix[0] != '\0') { char ua[256]; (void)snprintf(ua, sizeof(ua), "%s (tool: %s)", CLM_UA_BASE, client_suffix); curl_easy_setopt(req->easy_handle, CURLOPT_USERAGENT, ua); } else { curl_easy_setopt(req->easy_handle, CURLOPT_USERAGENT, CLM_UA_BASE); } curl_easy_setopt(req->easy_handle, CURLOPT_TIMEOUT, 0L); /* No total timeout, but abort if the connection goes completely dead. * 300s (not 120s) because a large prompt against a slow/local model * can legitimately sit silent for minutes during prefill before the * server emits its first byte -- this bit real compaction calls * (whose prompt is the whole conversation history, the largest single * request in a session) and ordinary turn calls once context grew * large: curl aborted mid-prefill on a perfectly healthy connection, * discarding 100+ seconds of already-done prefill and forcing a cold * restart on the next call. Still catches a truly dead connection, * just after a longer wait. */ curl_easy_setopt(req->easy_handle, CURLOPT_LOW_SPEED_LIMIT, 1L); curl_easy_setopt(req->easy_handle, CURLOPT_LOW_SPEED_TIME, 300L); curl_easy_setopt(req->easy_handle, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(req->easy_handle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); curl_easy_setopt(req->easy_handle, CURLOPT_PRIVATE, req); curl_multi_setopt(req->multi_handle, CURLMOPT_SOCKETFUNCTION, http_socket_callback); curl_multi_setopt(req->multi_handle, CURLMOPT_SOCKETDATA, req); curl_multi_setopt(req->multi_handle, CURLMOPT_TIMERFUNCTION, http_timer_callback); curl_multi_setopt(req->multi_handle, CURLMOPT_TIMERDATA, req); curl_multi_add_handle(req->multi_handle, req->easy_handle); req->state = CLM_HTTP_RUNNING; clm_debug("curl_multi_add_handle success"); /* Hand the caller a cancellable handle before we start driving it. The * transfer only completes on a later loop iteration, so this is safe. */ if (out_req != NULL) *out_req = req; /* Trigger initial socket action. A refused/failed connect can complete * synchronously right here instead of later off the poll/timer * callbacks -- observed on illumos, where a loopback connect to a * closed port fails inline rather than returning EINPROGRESS the way * it does on Linux. Reap it here too, else the CURLMSG_DONE just sits * queued forever: no poll/timer callback is ever going to run for a * socket that never got added, so success_cb/error_cb never fire and * the caller's event loop spins with nothing to wait on. */ curl_multi_socket_action(req->multi_handle, CURL_SOCKET_TIMEOUT, 0, &req->events_pending); clm_debug("curl_multi_socket_action completed"); if (http_reap_done(req)) http_request_teardown(req); return 0;} voidclm_http_async_cancel(struct clm_http_request *req){ if (req == NULL || req->closing) return; /* Report the abort through the error path, then tear down. */ req->state = CLM_HTTP_ERROR; req->error_code = -ECANCELED; (void)snprintf(req->error_msg, sizeof(req->error_msg), "cancelled"); http_request_teardown(req);} static voidhttp_request_complete(struct clm_http_request *req){ if (req->state == CLM_HTTP_DONE) { struct clm_http_response resp = {0}; long status_code = 200; curl_easy_getinfo(req->easy_handle, CURLINFO_RESPONSE_CODE, &status_code); resp.status_code = (int)status_code; resp.body = req->response_buf.data; req->response_buf.data = NULL; req->success_cb(&resp, req->user); } else { req->error_cb(req->error_code, req->error_msg, req->user); }} static voidhttp_handle_closed(uv_handle_t *handle){ struct clm_http_request *req = handle->data; req->handles_to_close--; if (req->handles_to_close == 0) { http_request_complete(req); clm_http_request_free(req); }} static voidhttp_request_teardown(struct clm_http_request *req){ clm_debug("closing=%d", req->closing); if (req->closing) return; req->closing = 1; /* Remove the easy handle from multi; this triggers CURL_POLL_REMOVE * callbacks for any open sockets, which close their poll handles. */ if (req->easy_handle != NULL && req->multi_handle != NULL) curl_multi_remove_handle(req->multi_handle, req->easy_handle); req->handles_to_close = 0; if (req->timer_initialized) req->handles_to_close++; if (req->handles_to_close == 0) { clm_debug("no handles to close, completing"); http_request_complete(req); clm_http_request_free(req); return; } if (req->timer_initialized) { clm_debug("uv_close timer_handle"); uv_close((uv_handle_t *)&req->timer_handle, http_handle_closed); }} static inthttp_timer_callback(CURLM *multi, long timeout_ms, void *userp){ struct clm_http_request *req = userp; clm_debug("timeout_ms=%ld", timeout_ms); if (timeout_ms < 0) { if (req->timer_initialized) { uv_timer_stop(&req->timer_handle); clm_debug("uv_timer_stop completed"); } return 0; } if (!req->timer_initialized) { uv_timer_init(req->uv, &req->timer_handle); req->timer_handle.data = req; req->timer_initialized = 1; clm_debug("uv_timer_init success"); } uv_timer_start(&req->timer_handle, http_timer_expired, (uint64_t)timeout_ms, 0); clm_debug("uv_timer_start completed with timeout=%lu", (unsigned long)timeout_ms); return 0;} static voidhttp_timer_expired(uv_timer_t *handle){ struct clm_http_request *req = handle->data; int running; clm_debug("calling curl_multi_socket_action"); curl_multi_socket_action(req->multi_handle, CURL_SOCKET_TIMEOUT, 0, &running); clm_debug("curl_multi_socket_action completed, running=%d", running); /* * A transfer can finish on the timer path. Reap it here too, else a * curl-requested 0ms timeout re-arms forever and the loop spins. */ if (http_reap_done(req)) http_request_teardown(req);} voidclm_http_request_free(struct clm_http_request *req){ if (req == NULL) return; if (req->easy_handle != NULL) {Lines 1–500 of 524. Load more
// SPDX-License-Identifier: ISC #include <errno.h> #include <stdbool.h> #include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <curl/curl.h> #include <uv.h> #include "clm/http_async.h" #include "clm/log.h" #include "clm/cleanup.h" #include "useful.h" #include "version.h" #include "banned.h" /* Base User-Agent: product token, version, and a homepage comment. Kept * minimal (no OS/arch/stack leakage) by design; an optional per-request * "(tool: <name>)" comment is appended for tool/plugin attribution. */ #define CLM_HOMEPAGE "https://github.com/mischief/clm" #define CLM_UA_BASE "clm/" CLM_VERSION " (+" CLM_HOMEPAGE ")" /* HTTP request state. */ enum clm_http_request_state { CLM_HTTP_PENDING, CLM_HTTP_RUNNING, CLM_HTTP_DONE, CLM_HTTP_ERROR, }; /* Async HTTP request response buffer. */ struct http_buf { char *data; size_t len; }; /* Per-socket poll context, allocated by the socket callback. */ struct clm_http_socket { uv_poll_t poll; curl_socket_t sockfd; struct clm_http_request *req; }; /* Async HTTP request context (opaque to callers; see clm/http_async.h). */ struct clm_http_request { uv_loop_t *uv; /* Curl handles */ CURLM *multi_handle; CURL *easy_handle; struct curl_slist *headers; /* Response buffer */ struct http_buf response_buf; int events_pending; /* UV timer handle */ uv_timer_t timer_handle; int timer_initialized; /* Callbacks and user data */ clm_http_success_cb success_cb; clm_http_error_cb error_cb; clm_http_data_cb data_cb; void *user; /* State */ enum clm_http_request_state state; int error_code; char error_msg[256]; int closing; int handles_to_close; }; static size_t http_write_callback(void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize = size * nmemb; struct clm_http_request *req = userp; struct http_buf *buf = &req->response_buf; char *ptr; ptr = realloc(buf->data, buf->len + realsize + 1); if (ptr == NULL) return 0; buf->data = ptr; memcpy(buf->data + buf->len, contents, realsize); buf->len += realsize; buf->data[buf->len] = '\0'; /* Forward chunks to a streaming consumer, but only for a 2xx response so * it never sees an error body. */ if (req->data_cb != NULL) { long code = 0; curl_easy_getinfo(req->easy_handle, CURLINFO_RESPONSE_CODE, &code); if (code >= 200 && code < 300) req->data_cb(contents, realsize, req->user); } return realsize; } static void http_handle_closed(uv_handle_t *handle); static void http_request_teardown(struct clm_http_request *req); static int http_timer_callback(CURLM *multi, long timeout_ms, void *userp); static void http_timer_expired(uv_timer_t *handle); /* * Drain curl's message queue after a socket_action. If a transfer finished, * record its outcome on req and return true so the caller tears down. Both the * socket-poll and timer paths must call this: a transfer can complete off the * timer (e.g. TLS/handshake progress or a fast response driven by a 0ms * timeout), and if only the poll path reaps CURLMSG_DONE the timer keeps * re-arming at 0ms and the loop spins at 100% CPU. */ static bool http_reap_done(struct clm_http_request *req) { int msgs_left; CURLMsg *msg; while ((msg = curl_multi_info_read(req->multi_handle, &msgs_left)) != NULL) { if (msg->msg != CURLMSG_DONE) continue; CURLcode curl_err = msg->data.result; if (curl_err == CURLE_OK) { req->state = CLM_HTTP_DONE; clm_debug("CURLMSG_DONE, CURLE_OK"); } else { req->state = CLM_HTTP_ERROR; req->error_code = (int)curl_err; snprintf(req->error_msg, sizeof(req->error_msg), "curl error: %s", curl_easy_strerror(curl_err)); clm_debug("CURLMSG_DONE, curl_err=%d", curl_err); } return true; } return false; } static void http_poll_callback(uv_poll_t *handle, int status, int events) { struct clm_http_socket *ctx = handle->data; struct clm_http_request *req = ctx->req; int curl_action = 0; clm_debug("status=%d, events=%d, fd=%d", status, events, ctx->sockfd); if (status < 0) { req->state = CLM_HTTP_ERROR; req->error_code = -status; snprintf(req->error_msg, sizeof(req->error_msg), "poll error: %s", uv_err_name(-status)); clm_debug("poll error, status=%d", status); goto done; } if (events & UV_READABLE) curl_action |= CURL_POLL_IN; if (events & UV_WRITABLE) curl_action |= CURL_POLL_OUT; clm_debug("curl_action=%d", curl_action); curl_multi_socket_action(req->multi_handle, ctx->sockfd, curl_action, &req->events_pending); clm_debug("curl_multi_socket_action completed"); if (http_reap_done(req)) goto done; return; done: clm_debug("calling http_request_teardown"); http_request_teardown(req); } static void on_socket_closed(uv_handle_t *handle) { struct clm_http_socket *ctx = handle->data; free(ctx); } static int http_socket_callback(CURL *easy, curl_socket_t s, int action, void *userp, void *socketp) { struct clm_http_request *req = userp; struct clm_http_socket *ctx = socketp; clm_debug("action=%d, s=%d", action, s); if (action == CURL_POLL_REMOVE) { if (ctx != NULL) { uv_poll_stop(&ctx->poll); uv_close((uv_handle_t *)&ctx->poll, on_socket_closed); curl_multi_assign(req->multi_handle, s, NULL); } return 0; } if (ctx == NULL) { ctx = calloc(1, sizeof(*ctx)); if (ctx == NULL) return 0; ctx->sockfd = s; ctx->req = req; ctx->poll.data = ctx; uv_poll_init_socket(req->uv, &ctx->poll, s); curl_multi_assign(req->multi_handle, s, ctx); clm_debug("uv_poll_init_socket success, sockfd=%d", s); } int events = 0; if (action & CURL_POLL_IN) events |= UV_READABLE; if (action & CURL_POLL_OUT) events |= UV_WRITABLE; clm_debug("events=%d", events); if (!uv_is_closing((uv_handle_t *)&ctx->poll)) uv_poll_start(&ctx->poll, events, http_poll_callback); return 0; } int clm_http_async_post(uv_loop_t *loop, const char *url, const char *api_key, const char *json_body, struct curl_slist *extra_headers, clm_http_success_cb success_cb, clm_http_error_cb error_cb, clm_http_data_cb data_cb, const char *client_suffix, void *user, struct clm_http_request **out_req) { struct clm_http_request *req; char *auth_header; size_t auth_header_len; clm_debug("starting"); if (out_req != NULL) *out_req = NULL; ASSERT_RETURN(loop != NULL, -EINVAL); ASSERT_RETURN(url != NULL, -EINVAL); /* json_body == NULL performs a GET (used for the health check). */ ASSERT_RETURN(success_cb != NULL, -EINVAL); ASSERT_RETURN(error_cb != NULL, -EINVAL); req = calloc(1, sizeof(*req)); if (req == NULL) { clm_debug("calloc failed"); return -ENOMEM; } clm_debug("req allocated"); req->uv = loop; req->success_cb = success_cb; req->error_cb = error_cb; req->data_cb = data_cb; req->user = user; req->state = CLM_HTTP_PENDING; req->response_buf.data = NULL; req->response_buf.len = 0; req->multi_handle = curl_multi_init(); if (req->multi_handle == NULL) { clm_debug("curl_multi_init failed"); free(req); return -ENOMEM; } clm_debug("curl_multi_init success"); req->easy_handle = curl_easy_init(); if (req->easy_handle == NULL) { clm_debug("curl_easy_init failed"); curl_multi_cleanup(req->multi_handle); free(req); return -ENOMEM; } clm_debug("curl_easy_init success"); if (api_key != NULL && api_key[0] != '\0') { auth_header_len = sizeof("Authorization: Bearer ") + strlen(api_key); auth_header = malloc(auth_header_len); if (auth_header == NULL) { clm_debug("auth_header alloc failed"); curl_easy_cleanup(req->easy_handle); curl_multi_cleanup(req->multi_handle); free(req); return -ENOMEM; } (void)snprintf(auth_header, auth_header_len, "Authorization: Bearer %s", api_key); req->headers = curl_slist_append(NULL, "Content-Type: application/json"); req->headers = curl_slist_append(req->headers, auth_header); free(auth_header); } else { req->headers = NULL; } /* Append any caller-provided extra headers. */ if (extra_headers != NULL) { struct curl_slist *h; for (h = extra_headers; h != NULL; h = h->next) req->headers = curl_slist_append(req->headers, h->data); curl_slist_free_all(extra_headers); } curl_easy_setopt(req->easy_handle, CURLOPT_URL, url); if (json_body != NULL) { curl_easy_setopt(req->easy_handle, CURLOPT_POST, 1L); curl_easy_setopt(req->easy_handle, CURLOPT_POSTFIELDS, json_body); } else { curl_easy_setopt(req->easy_handle, CURLOPT_HTTPGET, 1L); } curl_easy_setopt(req->easy_handle, CURLOPT_WRITEFUNCTION, http_write_callback); curl_easy_setopt(req->easy_handle, CURLOPT_WRITEDATA, req); if (req->headers != NULL) curl_easy_setopt(req->easy_handle, CURLOPT_HTTPHEADER, req->headers); /* Minimal, deliberate User-Agent; append a tool/plugin comment if given. */ if (client_suffix != NULL && client_suffix[0] != '\0') { char ua[256]; (void)snprintf(ua, sizeof(ua), "%s (tool: %s)", CLM_UA_BASE, client_suffix); curl_easy_setopt(req->easy_handle, CURLOPT_USERAGENT, ua); } else { curl_easy_setopt(req->easy_handle, CURLOPT_USERAGENT, CLM_UA_BASE); } curl_easy_setopt(req->easy_handle, CURLOPT_TIMEOUT, 0L); /* No total timeout, but abort if the connection goes completely dead. * 300s (not 120s) because a large prompt against a slow/local model * can legitimately sit silent for minutes during prefill before the * server emits its first byte -- this bit real compaction calls * (whose prompt is the whole conversation history, the largest single * request in a session) and ordinary turn calls once context grew * large: curl aborted mid-prefill on a perfectly healthy connection, * discarding 100+ seconds of already-done prefill and forcing a cold * restart on the next call. Still catches a truly dead connection, * just after a longer wait. */ curl_easy_setopt(req->easy_handle, CURLOPT_LOW_SPEED_LIMIT, 1L); curl_easy_setopt(req->easy_handle, CURLOPT_LOW_SPEED_TIME, 300L); curl_easy_setopt(req->easy_handle, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(req->easy_handle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); curl_easy_setopt(req->easy_handle, CURLOPT_PRIVATE, req); curl_multi_setopt(req->multi_handle, CURLMOPT_SOCKETFUNCTION, http_socket_callback); curl_multi_setopt(req->multi_handle, CURLMOPT_SOCKETDATA, req); curl_multi_setopt(req->multi_handle, CURLMOPT_TIMERFUNCTION, http_timer_callback); curl_multi_setopt(req->multi_handle, CURLMOPT_TIMERDATA, req); curl_multi_add_handle(req->multi_handle, req->easy_handle); req->state = CLM_HTTP_RUNNING; clm_debug("curl_multi_add_handle success"); /* Hand the caller a cancellable handle before we start driving it. The * transfer only completes on a later loop iteration, so this is safe. */ if (out_req != NULL) *out_req = req; /* Trigger initial socket action. A refused/failed connect can complete * synchronously right here instead of later off the poll/timer * callbacks -- observed on illumos, where a loopback connect to a * closed port fails inline rather than returning EINPROGRESS the way * it does on Linux. Reap it here too, else the CURLMSG_DONE just sits * queued forever: no poll/timer callback is ever going to run for a * socket that never got added, so success_cb/error_cb never fire and * the caller's event loop spins with nothing to wait on. */ curl_multi_socket_action(req->multi_handle, CURL_SOCKET_TIMEOUT, 0, &req->events_pending); clm_debug("curl_multi_socket_action completed"); if (http_reap_done(req)) http_request_teardown(req); return 0; } void clm_http_async_cancel(struct clm_http_request *req) { if (req == NULL || req->closing) return; /* Report the abort through the error path, then tear down. */ req->state = CLM_HTTP_ERROR; req->error_code = -ECANCELED; (void)snprintf(req->error_msg, sizeof(req->error_msg), "cancelled"); http_request_teardown(req); } static void http_request_complete(struct clm_http_request *req) { if (req->state == CLM_HTTP_DONE) { struct clm_http_response resp = {0}; long status_code = 200; curl_easy_getinfo(req->easy_handle, CURLINFO_RESPONSE_CODE, &status_code); resp.status_code = (int)status_code; resp.body = req->response_buf.data; req->response_buf.data = NULL; req->success_cb(&resp, req->user); } else { req->error_cb(req->error_code, req->error_msg, req->user); } } static void http_handle_closed(uv_handle_t *handle) { struct clm_http_request *req = handle->data; req->handles_to_close--; if (req->handles_to_close == 0) { http_request_complete(req); clm_http_request_free(req); } } static void http_request_teardown(struct clm_http_request *req) { clm_debug("closing=%d", req->closing); if (req->closing) return; req->closing = 1; /* Remove the easy handle from multi; this triggers CURL_POLL_REMOVE * callbacks for any open sockets, which close their poll handles. */ if (req->easy_handle != NULL && req->multi_handle != NULL) curl_multi_remove_handle(req->multi_handle, req->easy_handle); req->handles_to_close = 0; if (req->timer_initialized) req->handles_to_close++; if (req->handles_to_close == 0) { clm_debug("no handles to close, completing"); http_request_complete(req); clm_http_request_free(req); return; } if (req->timer_initialized) { clm_debug("uv_close timer_handle"); uv_close((uv_handle_t *)&req->timer_handle, http_handle_closed); } } static int http_timer_callback(CURLM *multi, long timeout_ms, void *userp) { struct clm_http_request *req = userp; clm_debug("timeout_ms=%ld", timeout_ms); if (timeout_ms < 0) { if (req->timer_initialized) { uv_timer_stop(&req->timer_handle); clm_debug("uv_timer_stop completed"); } return 0; } if (!req->timer_initialized) { uv_timer_init(req->uv, &req->timer_handle); req->timer_handle.data = req; req->timer_initialized = 1; clm_debug("uv_timer_init success"); } uv_timer_start(&req->timer_handle, http_timer_expired, (uint64_t)timeout_ms, 0); clm_debug("uv_timer_start completed with timeout=%lu", (unsigned long)timeout_ms); return 0; } static void http_timer_expired(uv_timer_t *handle) { struct clm_http_request *req = handle->data; int running; clm_debug("calling curl_multi_socket_action"); curl_multi_socket_action(req->multi_handle, CURL_SOCKET_TIMEOUT, 0, &running); clm_debug("curl_multi_socket_action completed, running=%d", running); /* * A transfer can finish on the timer path. Reap it here too, else a * curl-requested 0ms timeout re-arms forever and the loop spins. */ if (http_reap_done(req)) http_request_teardown(req); } void clm_http_request_free(struct clm_http_request *req) { if (req == NULL) return; if (req->easy_handle != NULL) {