From fc2e01da4d1c5498a5b2535b3389ae5205d99730 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Mon, 12 Oct 2026 07:43:34 +0300 Subject: [PATCH] init upl --- Makefile | 35 +++ README.md | 89 ++++++++ hygate.h | 117 ++++++++++ jsonapi.c | 422 +++++++++++++++++++++++++++++++++++ main.c | 129 +++++++++++ server.c | 642 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ util.c | 120 ++++++++++ 7 files changed, 1554 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 hygate.h create mode 100644 jsonapi.c create mode 100644 main.c create mode 100644 server.c create mode 100644 util.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b9f1ede --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +# Makefile for hygate-c - POSIX-compatible (builds on Linux, FreeBSD, macOS) +# Third-party dependencies: libcurl and json-c (auto-detected via pkg-config). + +CC ?= cc +CFLAGS ?= -O3 -flto -pipe -std=c11 -Wall -Wextra -pthread -D_POSIX_C_SOURCE=200809L +SRCS = main.c server.c jsonapi.c util.c +HDRS = hygate.h +BINDIR = bin +BIN = $(BINDIR)/hygate +STATICBIN = $(BINDIR)/hygate.static + +# Library flags resolved at recipe time via pkg-config (portable backtick form). +# Falls back to plain -lcurl -ljson-c when pkg-config is unavailable. +LIBS = $$(pkg-config --cflags --libs libcurl json-c 2>/dev/null || echo '-lcurl -ljson-c') + +# For static builds we need the full transitive dependency list, which +# pkg-config --static provides (nghttp2, ssl, crypto, z, ...). +STATICLIBS = $$(pkg-config --static --cflags --libs libcurl json-c 2>/dev/null || echo '-lcurl -ljson-c') + +.PHONY: all static clean + +all: $(BIN) + +static: $(STATICBIN) + +$(BIN): $(SRCS) $(HDRS) + @test -d $(BINDIR) || mkdir $(BINDIR) + $(CC) $(CFLAGS) $(SRCS) -o $@ $(LIBS) + +$(STATICBIN): $(SRCS) $(HDRS) + @test -d $(BINDIR) || mkdir $(BINDIR) + $(CC) $(CFLAGS) -static $(SRCS) -o $@ $(STATICLIBS) + +clean: + rm -rf $(BINDIR) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e005511 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# hygate-c + +A small, dependency-light C port of the Hygate gateway that exposes a +**Gradio-hosted Hunyuan model (`hy3`)** behind an **OpenAI-compatible** REST API. + +It translates OpenAI `chat/completions` requests into Gradio's queue API +(`POST /gradio_api/call/chat` → `GET /gradio_api/call/chat/{event_id}` SSE +stream) and translates the Gradio stream back into OpenAI streaming and +non-streaming responses, including tool calls and reasoning content. + +## Build + +Requires `libcurl` and `json-c` (auto-detected via `pkg-config`); falls back +to `-lcurl -ljson-c` if `pkg-config` is unavailable. + +```sh +make # dynamic binary -> bin/hygate +make static # static binary -> bin/hygate.static +make clean +``` + +Build is POSIX-portable (Linux, FreeBSD, macOS) and compiles clean with +`-Wall -Wextra`. + +## Run + +```sh +./bin/hygate [options] +``` + +### Options + +| Flag | Default | Description | +|------|---------|-------------| +| `-port ` | `8080` | Listen port (binds `0.0.0.0`). | +| `-endpoint ` | `https://tencent-hy3.hf.space` | Upstream Gradio Space base URL (trailing slash trimmed). | +| `-model ` | `hy3` | Model name reported in `/v1/models` and responses. | +| `-user-agent ` / `-ua ` | built-in Firefox UA | Outgoing `User-Agent`. Empty string => default. | +| `-max-body ` | `5242880` (5 MiB) | Maximum request body size. Oversized requests are rejected with `413`. | +| `-max-conn ` | `16` | Maximum concurrent connections (worker threads). Excess connections queue in the kernel listen backlog. | + +Flag forms: `-key value` or `-key=value`. + +Example — listen on `9090`, allow 8 MiB bodies, cap at 32 concurrent connections: + +```sh +./bin/hygate -port 9090 -max-body 8388608 -max-conn 32 +``` + +## Endpoints + +| Method | Path | Notes | +|--------|------|-------| +| `GET` | `/v1/models`, `/models` | Returns the configured model (served locally, no upstream call). | +| `POST` | `/v1/chat/completions`, `/chat/completions` | OpenAI-style chat completion. Supports `stream`, `tools`/`tool_calls`, and `reasoning_content`. | +| `OPTIONS` | any of the above | CORS preflight. | + +CORS is enabled for all origins (`Access-Control-Allow-Origin: *`). + +## Request / response notes + +- OpenAI `messages` are mapped into Gradio's `[message, system, history, + "high", temperature, max_tokens, top_p, null, functions_json]` payload. +- `max_tokens` / `max_completion_tokens` are honored; defaults to `131072` + when omitted. +- Upstream calls use Fibonacci-backoff retries (up to `MAX_RETRIES = 5`, + 300 s timeout). + +## Security considerations + +- The server binds all interfaces (`0.0.0.0`) and currently enforces **no + authentication** — it is an open relay to the configured upstream. Do not + expose it to untrusted networks without a fronting proxy or auth layer. +- `-max-body` and `-max-conn` exist to bound resource usage (memory and + thread count) against abusive clients. + +## Source layout + +| File | Role | +|------|------| +| `main.c` | Entry point, flag parsing, TCP listen, thread-per-connection with concurrency cap. | +| `server.c` | HTTP parsing, routing, SSE emitters, libcurl upstream calls, streaming state machine. | +| `jsonapi.c` | OpenAI <-> Gradio payload translation and stream parsing. | +| `util.c` | Growable string buffer (`strbuf`), UUID, backoff, helpers. | +| `hygate.h` | Shared types and declarations. | + +## Credits + +Created by Luxferre in 2026, released into the public domain with no warranties. diff --git a/hygate.h b/hygate.h new file mode 100644 index 0000000..d11a1ff --- /dev/null +++ b/hygate.h @@ -0,0 +1,117 @@ +#ifndef HYGATE_H +#define HYGATE_H + +#include +#include +#include + +#define DEFAULT_UA "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0" +#define DEFAULT_PORT "8080" +#define DEFAULT_ENDPOINT "https://tencent-hy3.hf.space" +#define DEFAULT_MODEL "hy3" +#define MAX_RETRIES 5 +#define UPSTREAM_TIMEOUT 300L +#define MAX_CONCURRENT_THREADS 16 +#define MAX_BODY_BYTES (5u * 1024u * 1024u) /* 5 MiB request body cap */ + +/* ------------------------------------------------------------------ */ +/* Dynamic string buffer (always NUL terminated) */ +/* ------------------------------------------------------------------ */ +typedef struct { + char *buf; + size_t len; + size_t cap; +} strbuf; + +void sb_init(strbuf *s); +void sb_free(strbuf *s); +int sb_append(strbuf *s, const char *data, size_t n); +int sb_append_str(strbuf *s, const char *str); +void sb_reset(strbuf *s); + +/* ------------------------------------------------------------------ */ +/* Small utilities */ +/* ------------------------------------------------------------------ */ +void generate_uuid(char out[37]); +long fib_delay(int attempt); +int write_all(int fd, const char *buf, size_t n); +int iequals(const char *a, const char *b); + +/* ------------------------------------------------------------------ */ +/* Tool call representation (parsed from Gradio stream) */ +/* ------------------------------------------------------------------ */ +typedef struct { + char *id; + char *type; + char *name; + char *arguments; +} tool_call_t; + +void free_toolcalls(tool_call_t *tc, int n); + +/* Parse a single Gradio SSE data payload: [["content","reasoning",[...]]] + * Returns 1 on success (caller frees *out_content, *out_reasoning, + * *out_tc via free_toolcalls). */ +int parse_gradio_stream(const char *data_json, + char **out_content, char **out_reasoning, + tool_call_t **out_tc, int *out_n_tc); + +/* ------------------------------------------------------------------ */ +/* JSON request <-> Gradio payload translation */ +/* ------------------------------------------------------------------ */ +/* Build the Gradio call payload {"data":[...]} from an OpenAI chat + * completion request object. Caller frees the result. Returns NULL on + * malformed input. */ +char *build_gradio_payload(void *req_obj, const char *model_default); + +/* Build a non-streaming OpenAI completion response JSON string. Caller + * frees the result. */ +char *build_completion_response(const char *id, long created, const char *model, + const char *content, const char *reasoning, + tool_call_t *tc, int n_tc); + +/* Build a streaming tool-call delta JSON array (first emission: full + * tool call with index). Caller frees the result. */ +char *toolcall_delta_json(tool_call_t *tc, int idx); + +/* Build a streaming tool-call argument-delta JSON array (index + + * function.arguments only). Caller frees the result. */ +char *toolcall_delta_argdelta(int idx, const char *delta); + +/* ------------------------------------------------------------------ */ +/* Server / request handling */ +/* ------------------------------------------------------------------ */ +typedef struct { + const char *port; + const char *endpoint; + const char *model; + const char *user_agent; /* empty string => use DEFAULT_UA */ + long max_body_bytes; + int max_connections; +} config_t; + +typedef struct { + char method[16]; + char path[4096]; + char content_type[256]; + long content_length; + char ua_override[1024]; + bool has_x_ua; + char *body; + size_t body_len; +} http_req_t; + +void http_req_free(http_req_t *r); + +/* Read the full HTTP request header block from fd into buf. Returns 0 + * on success, -1 on error/timeout. */ +int read_headers(int fd, strbuf *buf); + +/* Parse a buffered HTTP request. Returns 0 on success. */ +int parse_http_request(strbuf *buf, int fd, http_req_t *req, long max_body_bytes); + +/* Handle one accepted client connection (reads request, routes, + * writes response). */ +void handle_connection(int fd, const config_t *cfg); + +#endif /* HYGATE_H */ diff --git a/jsonapi.c b/jsonapi.c new file mode 100644 index 0000000..36e2a3e --- /dev/null +++ b/jsonapi.c @@ -0,0 +1,422 @@ +#include +#include "hygate.h" +#include +#include +#include + +/* Convert an OpenAI "content" field to a flat string. Caller frees. */ +static char *content_to_string(json_object *content) { + if (!content) return strdup(""); + enum json_type t = json_object_get_type(content); + if (t == json_type_string) + return strdup(json_object_get_string(content)); + if (t == json_type_array) { + strbuf sb; + sb_init(&sb); + int n = json_object_array_length(content); + for (int i = 0; i < n; i++) { + json_object *part = json_object_array_get_idx(content, i); + if (!part) continue; + enum json_type pt = json_object_get_type(part); + if (pt == json_type_string) { + sb_append_str(&sb, json_object_get_string(part)); + } else if (pt == json_type_object) { + json_object *txt = NULL; + if (json_object_object_get_ex(part, "text", &txt) && + json_object_get_type(txt) == json_type_string) + sb_append_str(&sb, json_object_get_string(txt)); + } + } + char *r = strdup(sb.buf); + sb_free(&sb); + return r; + } + const char *s = json_object_to_json_string(content); + return s ? strdup(s) : strdup(""); +} + +static const char *get_str(json_object *obj, const char *key) { + json_object *v = NULL; + if (json_object_object_get_ex(obj, key, &v) && + json_object_get_type(v) == json_type_string) + return json_object_get_string(v); + return ""; +} + +static bool has_nonnull(json_object *req, const char *key) { + json_object *o = NULL; + if (!json_object_object_get_ex(req, key, &o)) return false; + if (json_object_get_type(o) == json_type_null) return false; + return true; +} + +static double get_double(json_object *req, const char *key) { + json_object *o = NULL; + if (json_object_object_get_ex(req, key, &o)) { + enum json_type t = json_object_get_type(o); + if (t == json_type_double) return json_object_get_double(o); + if (t == json_type_int) return (double)json_object_get_int(o); + } + return 0.0; +} + +/* ------------------------------------------------------------------ */ +/* Build Gradio payload */ +/* ------------------------------------------------------------------ */ +char *build_gradio_payload(void *req_obj, const char *model_default) { + (void)model_default; + json_object *req = (json_object *)req_obj; + json_object *messages = NULL; + if (!json_object_object_get_ex(req, "messages", &messages) || + json_object_get_type(messages) != json_type_array) + return NULL; + int n = json_object_array_length(messages); + + char *system_str = strdup(""); + char *message_str = strdup(""); + json_object *history_arr = json_object_new_array(); + + /* First pass: pick first system message as system prompt, collect the + * rest as non-system messages. */ + int *nonSys = (int *)malloc(sizeof(int) * (size_t)(n > 0 ? n : 1)); + int nonSysN = 0; + for (int i = 0; i < n; i++) { + json_object *m = json_object_array_get_idx(messages, i); + if (!m) continue; + const char *roleStr = get_str(m, "role"); + if (strcmp(roleStr, "system") == 0 && system_str[0] == 0) { + char *cs = content_to_string(json_object_object_get(m, "content")); + free(system_str); + system_str = cs; + } else { + nonSys[nonSysN++] = i; + } + } + + /* Build history from all but the last non-system message. */ + for (int k = 0; k < nonSysN - 1; k++) { + int i = nonSys[k]; + json_object *m = json_object_array_get_idx(messages, i); + const char *roleStr = get_str(m, "role"); + json_object *item = json_object_new_object(); + json_object_object_add(item, "role", json_object_new_string(roleStr)); + + json_object *content = json_object_object_get(m, "content"); + char *cs = content_to_string(content); + if (strcmp(roleStr, "assistant") == 0) { + if (cs[0] != 0) + json_object_object_add(item, "content", json_object_new_string(cs)); + else + json_object_object_add(item, "content", json_object_new_null()); + } else { + json_object_object_add(item, "content", json_object_new_string(cs)); + } + free(cs); + + if (strcmp(roleStr, "assistant") == 0) { + const char *rc = get_str(m, "reasoning_content"); + if (rc[0] != 0) + json_object_object_add(item, "reasoning_content", + json_object_new_string(rc)); + json_object *tc = NULL; + if (json_object_object_get_ex(m, "tool_calls", &tc) && + json_object_get_type(tc) == json_type_array && + json_object_array_length(tc) > 0) { + /* Deep-copy the request's tool_calls into a fresh object so the + * output tree does not alias a subtree owned by the caller's + * request object. Aliasing caused a double-free / heap corruption + * when json_object_put(root) and json_object_put(jreq) both tried + * to release the shared node under concurrent worker threads. */ + json_object *tc_copy = NULL; + if (json_object_deep_copy(tc, &tc_copy, NULL) == 0 && tc_copy) + json_object_object_add(item, "tool_calls", tc_copy); + } + } else if (strcmp(roleStr, "tool") == 0 || + strcmp(roleStr, "function") == 0) { + json_object_object_del(item, "role"); + json_object_object_add(item, "role", json_object_new_string("tool")); + const char *toolID = get_str(m, "tool_call_id"); + if (toolID[0] == 0) toolID = get_str(m, "name"); + if (toolID[0] != 0) + json_object_object_add(item, "tool_call_id", + json_object_new_string(toolID)); + const char *nm = get_str(m, "name"); + if (nm[0] != 0) + json_object_object_add(item, "name", json_object_new_string(nm)); + } + json_object_array_add(history_arr, item); + } + + /* Last non-system message becomes the message string. */ + if (nonSysN > 0) { + int i = nonSys[nonSysN - 1]; + json_object *m = json_object_array_get_idx(messages, i); + const char *roleStr = get_str(m, "role"); + char *lastContent = content_to_string(json_object_object_get(m, "content")); + if (strcmp(roleStr, "tool") == 0 || strcmp(roleStr, "function") == 0) { + const char *toolName = get_str(m, "name"); + if (toolName[0] == 0) toolName = get_str(m, "tool_call_id"); + if (toolName[0] != 0) { + size_t need = strlen("Tool result for : ") + strlen(toolName) + + strlen(lastContent) + 1; + char *s = (char *)malloc(need); + snprintf(s, need, "Tool result for %s: %s", toolName, lastContent); + free(message_str); + message_str = s; + } else { + free(message_str); + message_str = strdup(lastContent); + } + } else { + free(message_str); + message_str = strdup(lastContent); + } + free(lastContent); + } + + free(nonSys); + + /* max_tokens resolution (mirrors ResolveMaxTokens). */ + long mt = 0; + if (has_nonnull(req, "max_tokens")) + mt = (long)get_double(req, "max_tokens"); + if (mt == 0 && has_nonnull(req, "max_completion_tokens")) + mt = (long)get_double(req, "max_completion_tokens"); + if (mt <= 0) mt = 131072; + + bool temp_present = has_nonnull(req, "temperature"); + double temp = get_double(req, "temperature"); + bool topp_present = has_nonnull(req, "top_p"); + double topp = topp_present ? get_double(req, "top_p") : 0.0; + + char *functions_json = NULL; + json_object *tools = NULL; + if (json_object_object_get_ex(req, "tools", &tools) && + json_object_get_type(tools) == json_type_array) { + const char *s = json_object_to_json_string(tools); + if (s) functions_json = strdup(s); + } + if (!functions_json) functions_json = strdup(""); + + /* Assemble the Gradio data array. */ + json_object *root = json_object_new_object(); + json_object *data = json_object_new_array(); + json_object_array_add(data, json_object_new_string(message_str)); + json_object_array_add(data, json_object_new_string(system_str)); + json_object_array_add(data, history_arr); + json_object_array_add(data, json_object_new_string("high")); + if (temp_present) + json_object_array_add(data, json_object_new_double(temp)); + else + json_object_array_add(data, json_object_new_null()); + json_object_array_add(data, json_object_new_int((int)mt)); + if (topp_present) + json_object_array_add(data, json_object_new_double(topp)); + else + json_object_array_add(data, json_object_new_int(0)); + json_object_array_add(data, json_object_new_null()); + json_object_array_add(data, json_object_new_string(functions_json)); + json_object_object_add(root, "data", data); + + const char *s = json_object_to_json_string(root); + char *payload = s ? strdup(s) : NULL; + json_object_put(root); + free(system_str); + free(message_str); + free(functions_json); + return payload; +} + +/* ------------------------------------------------------------------ */ +/* Parse Gradio stream payload */ +/* ------------------------------------------------------------------ */ +int parse_gradio_stream(const char *data_json, + char **out_content, char **out_reasoning, + tool_call_t **out_tc, int *out_n_tc) { + *out_content = NULL; + *out_reasoning = NULL; + *out_tc = NULL; + *out_n_tc = 0; + + json_object *root = json_tokener_parse(data_json); + if (!root) return 0; + if (json_object_get_type(root) != json_type_array) { + json_object_put(root); + return 0; + } + int n = json_object_array_length(root); + if (n == 0) { + json_object_put(root); + return 0; + } + /* Gradio emits a double-wrapped array: [[content, reasoning, tools]]. + Tolerate a single-wrapped [content, reasoning, tools] as well. */ + json_object *first = json_object_array_get_idx(root, 0); + json_object *inner; + if (json_object_get_type(first) == json_type_array) { + inner = first; + } else if (n >= 2) { + inner = root; + } else { + json_object_put(root); + return 0; + } + int m = json_object_array_length(inner); + if (m < 2) { + json_object_put(root); + return 0; + } + + json_object *c = json_object_array_get_idx(inner, 0); + json_object *r = json_object_array_get_idx(inner, 1); + *out_content = (c && json_object_get_type(c) == json_type_string) + ? strdup(json_object_get_string(c)) + : strdup(""); + *out_reasoning = (r && json_object_get_type(r) == json_type_string) + ? strdup(json_object_get_string(r)) + : strdup(""); + + if (m >= 3) { + json_object *tcobj = json_object_array_get_idx(inner, 2); + if (tcobj && json_object_get_type(tcobj) == json_type_array) { + int len = json_object_array_length(tcobj); + if (len > 0) { + tool_call_t *arr = (tool_call_t *)calloc((size_t)len, sizeof(tool_call_t)); + int cnt = 0; + for (int i = 0; i < len; i++) { + json_object *e = json_object_array_get_idx(tcobj, i); + if (json_object_get_type(e) != json_type_object) continue; + json_object *fid = NULL, *ffn = NULL, *fname = NULL, *fargs = NULL; + json_object_object_get_ex(e, "id", &fid); + json_object_object_get_ex(e, "function", &ffn); + if (ffn && json_object_get_type(ffn) == json_type_object) { + json_object_object_get_ex(ffn, "name", &fname); + json_object_object_get_ex(ffn, "arguments", &fargs); + } + arr[cnt].id = (fid && json_object_get_type(fid) == json_type_string) + ? strdup(json_object_get_string(fid)) + : strdup(""); + arr[cnt].type = (json_object_object_get_ex(e, "type", &fid) && + json_object_get_type(fid) == json_type_string) + ? strdup(json_object_get_string(fid)) + : strdup(""); + arr[cnt].name = (fname && json_object_get_type(fname) == json_type_string) + ? strdup(json_object_get_string(fname)) + : strdup(""); + arr[cnt].arguments = + (fargs && json_object_get_type(fargs) == json_type_string) + ? strdup(json_object_get_string(fargs)) + : strdup(""); + cnt++; + } + if (cnt > 0) { + *out_tc = arr; + *out_n_tc = cnt; + } else { + free(arr); + } + } + } + } + + json_object_put(root); + return 1; +} + +/* ------------------------------------------------------------------ */ +/* Build completion responses */ +/* ------------------------------------------------------------------ */ +char *build_completion_response(const char *id, long created, const char *model, + const char *content, const char *reasoning, + tool_call_t *tc, int n_tc) { + bool has_tc = n_tc > 0; + json_object *root = json_object_new_object(); + json_object_object_add(root, "id", json_object_new_string(id)); + json_object_object_add(root, "object", json_object_new_string("chat.completion")); + json_object_object_add(root, "created", json_object_new_int64(created)); + json_object_object_add(root, "model", json_object_new_string(model)); + + json_object *choices = json_object_new_array(); + json_object *choice = json_object_new_object(); + json_object_object_add(choice, "index", json_object_new_int(0)); + + json_object *msg = json_object_new_object(); + json_object_object_add(msg, "role", json_object_new_string("assistant")); + if (has_tc && (content == NULL || content[0] == 0)) + json_object_object_add(msg, "content", json_object_new_null()); + else + json_object_object_add(msg, "content", + json_object_new_string(content ? content : "")); + if (reasoning && reasoning[0] != 0) + json_object_object_add(msg, "reasoning_content", + json_object_new_string(reasoning)); + if (has_tc) { + json_object *tcs = json_object_new_array(); + for (int i = 0; i < n_tc; i++) { + json_object *t = json_object_new_object(); + if (tc[i].id && tc[i].id[0]) + json_object_object_add(t, "id", json_object_new_string(tc[i].id)); + if (tc[i].type && tc[i].type[0]) + json_object_object_add(t, "type", json_object_new_string(tc[i].type)); + json_object *fn = json_object_new_object(); + if (tc[i].name && tc[i].name[0]) + json_object_object_add(fn, "name", json_object_new_string(tc[i].name)); + json_object_object_add(fn, "arguments", + json_object_new_string(tc[i].arguments ? tc[i].arguments : "")); + json_object_object_add(t, "function", fn); + json_object_array_add(tcs, t); + } + json_object_object_add(msg, "tool_calls", tcs); + } + json_object_object_add(choice, "message", msg); + json_object_object_add(choice, "finish_reason", + json_object_new_string(has_tc ? "tool_calls" : "stop")); + json_object_array_add(choices, choice); + json_object_object_add(root, "choices", choices); + + json_object *usage = json_object_new_object(); + json_object_object_add(usage, "prompt_tokens", json_object_new_int(0)); + json_object_object_add(usage, "completion_tokens", json_object_new_int(0)); + json_object_object_add(usage, "total_tokens", json_object_new_int(0)); + json_object_object_add(root, "usage", usage); + + const char *s = json_object_to_json_string(root); + char *out = s ? strdup(s) : NULL; + json_object_put(root); + return out; +} + +char *toolcall_delta_json(tool_call_t *tc, int idx) { + json_object *arr = json_object_new_array(); + json_object *t = json_object_new_object(); + json_object_object_add(t, "index", json_object_new_int(idx)); + if (tc->id && tc->id[0]) + json_object_object_add(t, "id", json_object_new_string(tc->id)); + if (tc->type && tc->type[0]) + json_object_object_add(t, "type", json_object_new_string(tc->type)); + json_object *fn = json_object_new_object(); + if (tc->name && tc->name[0]) + json_object_object_add(fn, "name", json_object_new_string(tc->name)); + json_object_object_add(fn, "arguments", + json_object_new_string(tc->arguments ? tc->arguments : "")); + json_object_object_add(t, "function", fn); + json_object_array_add(arr, t); + const char *s = json_object_to_json_string(arr); + char *out = s ? strdup(s) : NULL; + json_object_put(arr); + return out; +} + +char *toolcall_delta_argdelta(int idx, const char *delta) { + json_object *arr = json_object_new_array(); + json_object *t = json_object_new_object(); + json_object_object_add(t, "index", json_object_new_int(idx)); + json_object *fn = json_object_new_object(); + json_object_object_add(fn, "arguments", json_object_new_string(delta ? delta : "")); + json_object_object_add(t, "function", fn); + json_object_array_add(arr, t); + const char *s = json_object_to_json_string(arr); + char *out = s ? strdup(s) : NULL; + json_object_put(arr); + return out; +} diff --git a/main.c b/main.c new file mode 100644 index 0000000..410990f --- /dev/null +++ b/main.c @@ -0,0 +1,129 @@ +#include "hygate.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static config_t g_cfg; +static sem_t g_conn_sem; + +static void *worker(void *arg) { + int fd = (int)(intptr_t)arg; + handle_connection(fd, &g_cfg); + close(fd); + sem_post(&g_conn_sem); + return NULL; +} + +int main(int argc, char **argv) { + config_t cfg; + cfg.port = DEFAULT_PORT; + cfg.endpoint = DEFAULT_ENDPOINT; + cfg.model = DEFAULT_MODEL; + cfg.user_agent = ""; + cfg.max_body_bytes = (long)MAX_BODY_BYTES; + cfg.max_connections = MAX_CONCURRENT_THREADS; + + /* Simple flag parsing: -key value or -key=value */ + for (int i = 1; i < argc; i++) { + char *a = argv[i]; + char *val = NULL; + if (a[0] == '-') { + char *eq = strchr(a, '='); + if (eq) { + *eq = 0; + val = eq + 1; + } else if (i + 1 < argc) { + val = argv[i + 1]; + i++; + } + if (strcmp(a, "-port") == 0) cfg.port = val; + else if (strcmp(a, "-endpoint") == 0) cfg.endpoint = val; + else if (strcmp(a, "-model") == 0) cfg.model = val; + else if (strcmp(a, "-user-agent") == 0) cfg.user_agent = val; + else if (strcmp(a, "-ua") == 0) cfg.user_agent = val; + else if (strcmp(a, "-max-body") == 0) { + if (val) { + long v = (long)strtoul(val, NULL, 10); + if (v > 0) cfg.max_body_bytes = v; + } + } else if (strcmp(a, "-max-conn") == 0) { + if (val) { + int v = atoi(val); + if (v > 0) cfg.max_connections = v; + } + } else { + fprintf(stderr, "unknown flag: %s\n", a); + return 2; + } + } + } + g_cfg = cfg; + + if (sem_init(&g_conn_sem, 0, (unsigned int)cfg.max_connections) != 0) { + perror("sem_init"); + return 1; + } + + curl_global_init(CURL_GLOBAL_ALL); + signal(SIGPIPE, SIG_IGN); + + int srv = socket(AF_INET, SOCK_STREAM, 0); + if (srv < 0) { perror("socket"); return 1; } + int one = 1; + setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in addr; + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)atoi(cfg.port)); + addr.sin_addr.s_addr = INADDR_ANY; + if (bind(srv, (struct sockaddr *)&addr, sizeof addr) < 0) { perror("bind"); return 1; } + if (listen(srv, 128) < 0) { perror("listen"); return 1; } + + printf("hygate starting on port %s...\n", cfg.port); + printf("Target Endpoint: %s\n", cfg.endpoint); + printf("Model Name: %s\n", cfg.model); + printf("User-Agent: %s\n", cfg.user_agent && cfg.user_agent[0] ? cfg.user_agent : DEFAULT_UA); + printf("Max request body: %ld bytes\n", cfg.max_body_bytes); + printf("Max concurrent connections: %d\n", cfg.max_connections); + printf("Endpoints:\n"); + printf(" GET http://localhost:%s/v1/models\n", cfg.port); + printf(" POST http://localhost:%s/v1/chat/completions\n", cfg.port); + + for (;;) { + /* Wait for a free worker slot before accepting, so the number of + in-flight connections (and thus worker threads) is capped at + cfg.max_connections. Excess connections queue in the kernel + listen backlog. */ + while (sem_wait(&g_conn_sem) != 0) { + if (errno == EINTR) continue; + perror("sem_wait"); + break; + } + int cfd = accept(srv, NULL, NULL); + if (cfd < 0) { + if (errno == EINTR) { sem_post(&g_conn_sem); continue; } + perror("accept"); + sem_post(&g_conn_sem); + continue; + } + pthread_t tid; + if (pthread_create(&tid, NULL, worker, (void *)(intptr_t)cfd) == 0) { + pthread_detach(tid); + } else { + handle_connection(cfd, &g_cfg); + close(cfd); + sem_post(&g_conn_sem); + } + } + return 0; +} diff --git a/server.c b/server.c new file mode 100644 index 0000000..3f3fab5 --- /dev/null +++ b/server.c @@ -0,0 +1,642 @@ +#include "hygate.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* Streaming context */ +/* ------------------------------------------------------------------ */ +struct tcst { + char *key; + char *prev_args; + bool emitted; +}; + +typedef struct { + int client_fd; + bool streaming; + char completion_id[64]; + long created; + const char *model; + strbuf line; /* SSE line buffer (used for both modes) */ + strbuf emitted_content; /* streaming: content emitted so far */ + strbuf emitted_reasoning;/* streaming: reasoning emitted so far */ + struct tcst *tc_state; + int tc_state_n; + int tc_state_cap; + bool any_toolcall; + char *final_content; /* non-streaming accumulation */ + char *final_reasoning; + tool_call_t *final_tc; + int final_n_tc; + int status; + bool headers_sent; +} sse_ctx_t; + +/* ------------------------------------------------------------------ */ +/* Low-level response writers */ +/* ------------------------------------------------------------------ */ +static void send_sse_headers(int fd) { + const char *h = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, Authorization, api-key, X-User-Agent\r\n" + "\r\n"; + write_all(fd, h, strlen(h)); +} + +static void send_options(int fd) { + const char *h = + "HTTP/1.1 200 OK\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, Authorization, api-key, X-User-Agent\r\n" + "Connection: close\r\n" + "Content-Length: 0\r\n\r\n"; + write_all(fd, h, strlen(h)); +} + +static void send_simple(int fd, int code, const char *codestr, const char *body) { + char header[512]; + int hl = snprintf(header, sizeof header, + "HTTP/1.1 %d %s\r\n" + "Content-Type: text/plain; charset=utf-8\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, Authorization, api-key, X-User-Agent\r\n" + "Connection: close\r\n" + "Content-Length: %zu\r\n\r\n", code, codestr, strlen(body)); + write_all(fd, header, hl); + write_all(fd, body, strlen(body)); +} + +static void send_json(int fd, const char *json) { + char header[512]; + int hl = snprintf(header, sizeof header, + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, Authorization, api-key, X-User-Agent\r\n" + "Connection: close\r\n" + "Content-Length: %zu\r\n\r\n", strlen(json)); + write_all(fd, header, hl); + write_all(fd, json, strlen(json)); +} + +/* ------------------------------------------------------------------ */ +/* SSE chunk emitter */ +/* ------------------------------------------------------------------ */ +static void emit_chunk(sse_ctx_t *ctx, const char *role, const char *content, + const char *reasoning, const char *toolcalls_json, + const char *finish_reason) { + json_object *root = json_object_new_object(); + json_object_object_add(root, "id", json_object_new_string(ctx->completion_id)); + json_object_object_add(root, "object", json_object_new_string("chat.completion.chunk")); + json_object_object_add(root, "created", json_object_new_int64(ctx->created)); + json_object_object_add(root, "model", json_object_new_string(ctx->model)); + json_object *choices = json_object_new_array(); + json_object *choice = json_object_new_object(); + json_object_object_add(choice, "index", json_object_new_int(0)); + json_object *delta = json_object_new_object(); + if (role) json_object_object_add(delta, "role", json_object_new_string(role)); + if (content) json_object_object_add(delta, "content", json_object_new_string(content)); + if (reasoning) json_object_object_add(delta, "reasoning_content", json_object_new_string(reasoning)); + if (toolcalls_json) { + json_object *tc = json_tokener_parse(toolcalls_json); + if (tc) json_object_object_add(delta, "tool_calls", tc); + } + json_object_object_add(choice, "delta", delta); + if (finish_reason) + json_object_object_add(choice, "finish_reason", json_object_new_string(finish_reason)); + json_object_array_add(choices, choice); + json_object_object_add(root, "choices", choices); + + const char *s = json_object_to_json_string(root); + if (s) { + size_t need = strlen(s) + 32; + char *buf = (char *)malloc(need); + int bl = snprintf(buf, need, "data: %s\n\n", s); + write_all(ctx->client_fd, buf, (size_t)bl); + free(buf); + } + json_object_put(root); +} + +/* ------------------------------------------------------------------ */ +/* SSE line processing */ +/* ------------------------------------------------------------------ */ +static void handle_stream_delta(sse_ctx_t *ctx, char *c, char *r, + tool_call_t *tc, int ntc); + +static void process_one_line(sse_ctx_t *ctx, char *line) { + while (*line == ' ' || *line == '\t' || *line == '\r' || *line == '\n') line++; + if (*line == 0) return; + char *e = line + strlen(line) - 1; + while (e > line && (*e == ' ' || *e == '\t' || *e == '\r' || *e == '\n')) *e-- = 0; + if (strncmp(line, "data: ", 6) != 0) return; + const char *payload = line + 6; + + char *c = NULL, *r = NULL; + tool_call_t *tc = NULL; + int ntc = 0; + if (!parse_gradio_stream(payload, &c, &r, &tc, &ntc)) { + free(c); + free(r); + free_toolcalls(tc, ntc); + return; + } + + if (ctx->streaming) { + handle_stream_delta(ctx, c, r, tc, ntc); + free(c); + free(r); + free_toolcalls(tc, ntc); + } else { + free(ctx->final_content); + ctx->final_content = c ? c : strdup(""); + free(ctx->final_reasoning); + ctx->final_reasoning = r ? r : strdup(""); + free_toolcalls(ctx->final_tc, ctx->final_n_tc); + if (ntc > 0) { + ctx->final_tc = tc; + ctx->final_n_tc = ntc; + } else { + ctx->final_tc = NULL; + ctx->final_n_tc = 0; + free(tc); + } + } +} + +static void process_complete_lines(sse_ctx_t *ctx) { + char *buf = ctx->line.buf; + while (1) { + char *nl = strchr(buf, '\n'); + if (!nl) break; + *nl = 0; + char *line = buf; + size_t ll = strlen(line); + if (ll > 0 && line[ll - 1] == '\r') line[ll - 1] = 0; + process_one_line(ctx, line); + size_t consumed = (size_t)(nl - buf) + 1; + size_t rest = ctx->line.len - consumed; + memmove(buf, nl + 1, rest + 1); + ctx->line.len = rest; + } +} + +static void flush_remaining(sse_ctx_t *ctx) { + if (ctx->line.len == 0) return; + char *line = ctx->line.buf; + size_t ll = ctx->line.len; + if (ll > 0 && line[ll - 1] == '\r') line[ll - 1] = 0; + if (ll > 0 && line[ll - 1] == '\n') line[ll - 1] = 0; + process_one_line(ctx, line); + sb_reset(&ctx->line); +} + +static void handle_stream_delta(sse_ctx_t *ctx, char *c, char *r, + tool_call_t *tc, int ntc) { + if (strlen(r) > ctx->emitted_reasoning.len) { + const char *delta = r + ctx->emitted_reasoning.len; + sb_append(&ctx->emitted_reasoning, delta, strlen(delta)); + emit_chunk(ctx, NULL, NULL, delta, NULL, NULL); + } + if (strlen(c) > ctx->emitted_content.len) { + const char *delta = c + ctx->emitted_content.len; + sb_append(&ctx->emitted_content, delta, strlen(delta)); + emit_chunk(ctx, NULL, delta, NULL, NULL, NULL); + } + for (int i = 0; i < ntc; i++) { + char keybuf[64]; + const char *key; + if (tc[i].id && tc[i].id[0]) key = tc[i].id; + else { snprintf(keybuf, sizeof keybuf, "idx_%d", i); key = keybuf; } + struct tcst *st = NULL; + for (int j = 0; j < ctx->tc_state_n; j++) + if (strcmp(ctx->tc_state[j].key, key) == 0) { st = &ctx->tc_state[j]; break; } + if (!st) { + if (ctx->tc_state_n >= ctx->tc_state_cap) { + ctx->tc_state_cap = ctx->tc_state_cap ? ctx->tc_state_cap * 2 : 8; + ctx->tc_state = (struct tcst *)realloc(ctx->tc_state, + (size_t)ctx->tc_state_cap * sizeof(*ctx->tc_state)); + } + st = &ctx->tc_state[ctx->tc_state_n++]; + st->key = strdup(key); + st->prev_args = strdup(""); + st->emitted = false; + } + const char *cur = tc[i].arguments ? tc[i].arguments : ""; + if (!st->emitted) { + st->emitted = true; + free(st->prev_args); + st->prev_args = strdup(cur); + ctx->any_toolcall = true; + char *js = toolcall_delta_json(&tc[i], i); + emit_chunk(ctx, NULL, NULL, NULL, js, NULL); + free(js); + } else { + size_t prev = strlen(st->prev_args); + size_t curlen = strlen(cur); + if (curlen > prev) { + const char *delta = cur + prev; + free(st->prev_args); + st->prev_args = strdup(cur); + char *js = toolcall_delta_argdelta(i, delta); + emit_chunk(ctx, NULL, NULL, NULL, js, NULL); + free(js); + } + } + } +} + +/* ------------------------------------------------------------------ */ +/* libcurl callbacks */ +/* ------------------------------------------------------------------ */ +static size_t write_mem_cb(void *ptr, size_t sz, size_t nmemb, void *data) { + strbuf *b = (strbuf *)data; + sb_append(b, ptr, sz * nmemb); + return sz * nmemb; +} + +static size_t header_cb(char *buf, size_t sz, size_t nmemb, void *data) { + sse_ctx_t *ctx = (sse_ctx_t *)data; + size_t len = sz * nmemb; + if (len >= 5 && strncmp(buf, "HTTP/", 5) == 0) { + char *p = buf; + while (*p && *p != ' ') p++; + if (*p == ' ') ctx->status = atoi(p + 1); + if (ctx->streaming && ctx->status == 200 && !ctx->headers_sent) { + send_sse_headers(ctx->client_fd); + ctx->headers_sent = true; + emit_chunk(ctx, "assistant", NULL, NULL, NULL, NULL); + } + } + return len; +} + +static size_t write_sse_cb(void *ptr, size_t sz, size_t nmemb, void *data) { + sse_ctx_t *ctx = (sse_ctx_t *)data; + sb_append(&ctx->line, ptr, sz * nmemb); + process_complete_lines(ctx); + return sz * nmemb; +} + +/* ------------------------------------------------------------------ */ +/* Upstream calls (with Fibonacci backoff retry) */ +/* ------------------------------------------------------------------ */ +static int upstream_post(const char *url, const char *body, const char *ua, + strbuf *resp, long *http_code) { + for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { + CURL *c = curl_easy_init(); + if (!c) return -1; + struct curl_slist *hdrs = NULL; + hdrs = curl_slist_append(hdrs, "Content-Type: application/json"); + char uah[2048]; + snprintf(uah, sizeof uah, "User-Agent: %s", ua); + hdrs = curl_slist_append(hdrs, uah); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, write_mem_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, resp); + curl_easy_setopt(c, CURLOPT_TIMEOUT, UPSTREAM_TIMEOUT); + CURLcode rc = curl_easy_perform(c); + long code = 0; + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code); + curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + if (rc == CURLE_OK && code == 200) { + if (http_code) *http_code = code; + return 0; + } + if (attempt < MAX_RETRIES) sleep((unsigned int)fib_delay(attempt)); + } + return -1; +} + +static int upstream_get_stream(const char *url, const char *ua, sse_ctx_t *ctx) { + for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { + CURL *c = curl_easy_init(); + if (!c) return -1; + struct curl_slist *hdrs = NULL; + hdrs = curl_slist_append(hdrs, "Accept: text/event-stream"); + char uah[2048]; + snprintf(uah, sizeof uah, "User-Agent: %s", ua); + hdrs = curl_slist_append(hdrs, uah); + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); + curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, header_cb); + curl_easy_setopt(c, CURLOPT_HEADERDATA, ctx); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, write_sse_cb); + curl_easy_setopt(c, CURLOPT_WRITEDATA, ctx); + curl_easy_setopt(c, CURLOPT_TIMEOUT, UPSTREAM_TIMEOUT); + CURLcode rc = curl_easy_perform(c); + long code = 0; + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code); + curl_slist_free_all(hdrs); + curl_easy_cleanup(c); + if (rc == CURLE_OK && code == 200) return 0; + if (attempt < MAX_RETRIES) sleep((unsigned int)fib_delay(attempt)); + } + return -1; +} + +/* ------------------------------------------------------------------ */ +/* Endpoint handlers */ +/* ------------------------------------------------------------------ */ +static void handle_models(int fd, const config_t *cfg) { + json_object *root = json_object_new_object(); + json_object_object_add(root, "object", json_object_new_string("list")); + json_object *data = json_object_new_array(); + json_object *m = json_object_new_object(); + json_object_object_add(m, "id", json_object_new_string(cfg->model)); + json_object_object_add(m, "object", json_object_new_string("model")); + json_object_object_add(m, "created", json_object_new_int64((long)time(NULL))); + json_object_object_add(m, "owned_by", json_object_new_string("hunyuan")); + json_object_array_add(data, m); + json_object_object_add(root, "data", data); + const char *s = json_object_to_json_string(root); + if (s) send_json(fd, s); + json_object_put(root); +} + +static void handle_chat(int fd, http_req_t *req, const config_t *cfg) { + json_object *jreq = json_tokener_parse(req->body); + if (!jreq || json_object_get_type(jreq) != json_type_object) { + send_simple(fd, 400, "Bad Request", "Invalid JSON payload"); + if (jreq) json_object_put(jreq); + return; + } + bool stream = false; + json_object *o = NULL; + if (json_object_object_get_ex(jreq, "stream", &o) && + json_object_get_type(o) == json_type_boolean) + stream = json_object_get_boolean(o); + char model_buf[256]; + const char *model = cfg->model; + if (json_object_object_get_ex(jreq, "model", &o) && + json_object_get_type(o) == json_type_string && + json_object_get_string(o)[0]) { + snprintf(model_buf, sizeof model_buf, "%s", json_object_get_string(o)); + model = model_buf; + } + + char *payload = build_gradio_payload(jreq, model); + json_object_put(jreq); + if (!payload) { + send_simple(fd, 400, "Bad Request", "Invalid request: missing messages"); + return; + } + + const char *ua = req->has_x_ua ? req->ua_override + : (cfg->user_agent && cfg->user_agent[0] ? cfg->user_agent : DEFAULT_UA); + + /* endpoint without trailing slash (mirrors TrimRight(endpoint, "/")) */ + const char *ep = cfg->endpoint; + size_t eplen = strlen(ep); + char epclean[2048]; + if (eplen > 0 && ep[eplen - 1] == '/') { + memcpy(epclean, ep, eplen - 1); + epclean[eplen - 1] = 0; + } else { + strncpy(epclean, ep, sizeof epclean - 1); + epclean[sizeof epclean - 1] = 0; + } + + char call_url[4096]; + snprintf(call_url, sizeof call_url, "%s/gradio_api/call/chat", epclean); + + strbuf post_resp; + sb_init(&post_resp); + long code = 0; + if (upstream_post(call_url, payload, ua, &post_resp, &code) != 0) { + send_simple(fd, 502, "Bad Gateway", + "Upstream error: failed to reach Gradio call endpoint"); + sb_free(&post_resp); + free(payload); + return; + } + + char event_id[256]; + event_id[0] = 0; + json_object *jr = json_tokener_parse(post_resp.buf); + if (jr) { + json_object *ev = NULL; + if (json_object_object_get_ex(jr, "event_id", &ev) && + json_object_get_type(ev) == json_type_string) + strncpy(event_id, json_object_get_string(ev), sizeof event_id - 1); + json_object_put(jr); + } + sb_free(&post_resp); + if (event_id[0] == 0) { + send_simple(fd, 502, "Bad Gateway", + "Upstream error: failed to parse Gradio event ID"); + free(payload); + return; + } + + char stream_url[8192]; + snprintf(stream_url, sizeof stream_url, "%s/gradio_api/call/chat/%s", epclean, event_id); + + sse_ctx_t ctx; + memset(&ctx, 0, sizeof ctx); + ctx.client_fd = fd; + ctx.streaming = stream; + ctx.model = model; + ctx.created = (long)time(NULL); + char uuid[37]; + generate_uuid(uuid); + snprintf(ctx.completion_id, sizeof ctx.completion_id, "chatcmpl-%s", uuid); + sb_init(&ctx.line); + sb_init(&ctx.emitted_content); + sb_init(&ctx.emitted_reasoning); + ctx.final_content = strdup(""); + ctx.final_reasoning = strdup(""); + + int rc = upstream_get_stream(stream_url, ua, &ctx); + + if (rc != 0 || ctx.status != 200) { + if (!ctx.headers_sent) + send_simple(fd, 502, "Bad Gateway", "Upstream stream error"); + goto cleanup; + } + + flush_remaining(&ctx); + + if (stream) { + emit_chunk(&ctx, NULL, NULL, NULL, NULL, ctx.any_toolcall ? "tool_calls" : "stop"); + const char *done = "data: [DONE]\n\n"; + write_all(fd, done, strlen(done)); + } else { + char *resp = build_completion_response(ctx.completion_id, ctx.created, ctx.model, + ctx.final_content, ctx.final_reasoning, + ctx.final_tc, ctx.final_n_tc); + if (resp) { + send_json(fd, resp); + free(resp); + } else { + send_simple(fd, 500, "Internal Server Error", "Failed to build response"); + } + } + +cleanup: + sb_free(&ctx.line); + sb_free(&ctx.emitted_content); + sb_free(&ctx.emitted_reasoning); + for (int i = 0; i < ctx.tc_state_n; i++) { + free(ctx.tc_state[i].key); + free(ctx.tc_state[i].prev_args); + } + free(ctx.tc_state); + free(ctx.final_content); + free(ctx.final_reasoning); + free_toolcalls(ctx.final_tc, ctx.final_n_tc); + free(payload); +} + +/* ------------------------------------------------------------------ */ +/* Request parsing / connection dispatch */ +/* ------------------------------------------------------------------ */ +int read_headers(int fd, strbuf *buf) { + char tmp[4096]; + while (1) { + if (strstr(buf->buf, "\r\n\r\n")) break; + ssize_t n = read(fd, tmp, sizeof tmp); + if (n <= 0) return -1; + if (sb_append(buf, tmp, (size_t)n) != 0) return -1; + if (buf->len > (1 << 20)) return -1; + } + return 0; +} + +int parse_http_request(strbuf *buf, int fd, http_req_t *req, long max_body_bytes) { + char *p = buf->buf; + char *line_end = strstr(p, "\r\n"); + if (!line_end) return -1; + *line_end = 0; + char *sp1 = strchr(p, ' '); + if (!sp1) return -1; + *sp1 = 0; + strncpy(req->method, p, sizeof req->method - 1); + req->method[sizeof req->method - 1] = 0; + char *rest = sp1 + 1; + char *sp2 = strchr(rest, ' '); + if (sp2) *sp2 = 0; + strncpy(req->path, rest, sizeof req->path - 1); + req->path[sizeof req->path - 1] = 0; + + char *h = line_end + 2; + /* buf->buf was NUL-terminated at the first CRLF above, so searching from + buf->buf would only see the request line. Search from h (the intact + header block) for the end-of-headers marker. */ + char *hend = strstr(h, "\r\n\r\n"); + if (!hend) return -1; + char *hlimit = hend; + while (h < hlimit) { + char *le = strstr(h, "\r\n"); + if (!le) break; + *le = 0; + char *colon = strchr(h, ':'); + if (colon) { + *colon = 0; + char *k = h; + char *v = colon + 1; + if (*v == ' ') v++; + if (iequals(k, "content-length")) req->content_length = atol(v); + else if (iequals(k, "content-type")) { + strncpy(req->content_type, v, sizeof req->content_type - 1); + req->content_type[sizeof req->content_type - 1] = 0; + } else if (iequals(k, "x-user-agent")) { + strncpy(req->ua_override, v, sizeof req->ua_override - 1); + req->ua_override[sizeof req->ua_override - 1] = 0; + req->has_x_ua = true; + } + } + h = le + 2; + } + + char *body_start = hlimit + 4; + size_t body_avail = buf->len - (size_t)(body_start - buf->buf); + if (max_body_bytes > 0 && req->content_length > max_body_bytes) return -2; + if (req->content_length > 0) { + req->body = (char *)malloc((size_t)req->content_length + 1); + if (!req->body) return -1; + size_t got = body_avail < (size_t)req->content_length + ? body_avail + : (size_t)req->content_length; + if (got > 0) memcpy(req->body, body_start, got); + while (got < (size_t)req->content_length) { + ssize_t n = read(fd, req->body + got, (size_t)req->content_length - got); + if (n <= 0) break; + got += (size_t)n; + } + req->body[got] = 0; + req->body_len = got; + } else { + req->body = strdup(""); + req->body_len = 0; + } + return 0; +} + +void http_req_free(http_req_t *r) { + free(r->body); + r->body = NULL; +} + +void handle_connection(int fd, const config_t *cfg) { + strbuf reqbuf; + sb_init(&reqbuf); + if (read_headers(fd, &reqbuf) != 0) { + sb_free(&reqbuf); + return; + } + http_req_t req; + memset(&req, 0, sizeof req); + int prc = parse_http_request(&reqbuf, fd, &req, cfg->max_body_bytes); + if (prc != 0) { + if (prc == -2) + send_simple(fd, 413, "Payload Too Large", + "Request body exceeds the maximum allowed size"); + http_req_free(&req); + sb_free(&reqbuf); + return; + } + + char *q = strchr(req.path, '?'); + if (q) *q = 0; + + bool is_models = (strcmp(req.path, "/models") == 0 || + strcmp(req.path, "/v1/models") == 0); + bool is_chat = (strcmp(req.path, "/chat/completions") == 0 || + strcmp(req.path, "/v1/chat/completions") == 0); + + if (strcmp(req.method, "OPTIONS") == 0) { + send_options(fd); + } else if (is_models) { + if (strcmp(req.method, "GET") == 0) handle_models(fd, cfg); + else send_simple(fd, 405, "Method Not Allowed", "Method not allowed"); + } else if (is_chat) { + if (strcmp(req.method, "POST") == 0) handle_chat(fd, &req, cfg); + else send_simple(fd, 405, "Method Not Allowed", "Method not allowed"); + } else { + send_simple(fd, 404, "Not Found", "Not Found"); + } + + http_req_free(&req); + sb_free(&reqbuf); +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..ed81d97 --- /dev/null +++ b/util.c @@ -0,0 +1,120 @@ +#include +#include "hygate.h" +#include +#include +#include +#include +#include +#include + +/* ----------------------------- strbuf ------------------------------ */ + +void sb_init(strbuf *s) { + s->cap = 64; + s->buf = (char *)malloc(s->cap); + if (!s->buf) { s->cap = 0; s->len = 0; return; } + s->len = 0; + s->buf[0] = '\0'; +} + +void sb_free(strbuf *s) { + free(s->buf); + s->buf = NULL; + s->len = s->cap = 0; +} + +int sb_append(strbuf *s, const char *data, size_t n) { + if (n == 0) return 0; + if (s->len + n + 1 > s->cap) { + size_t ncap = s->cap ? s->cap * 2 : 64; + while (ncap < s->len + n + 1) ncap *= 2; + char *nb = (char *)realloc(s->buf, ncap); + if (!nb) return -1; + s->buf = nb; + s->cap = ncap; + } + memcpy(s->buf + s->len, data, n); + s->len += n; + s->buf[s->len] = '\0'; + return 0; +} + +int sb_append_str(strbuf *s, const char *str) { + return sb_append(s, str, strlen(str)); +} + +void sb_reset(strbuf *s) { + s->len = 0; + if (s->buf) s->buf[0] = '\0'; +} + +/* --------------------------- utilities ---------------------------- */ + +void generate_uuid(char out[37]) { + unsigned char b[16]; + size_t i; + FILE *f = fopen("/dev/urandom", "rb"); + if (f) { + size_t got = fread(b, 1, 16, f); + fclose(f); + if (got != 16) { + for (i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + } + } else { + for (i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + } + b[6] = (unsigned char)((b[6] & 0x0f) | 0x40); + b[8] = (unsigned char)((b[8] & 0x3f) | 0x80); + snprintf(out, 37, + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]); +} + +long fib_delay(int attempt) { + if (attempt <= 0) return 1; + long a = 1, b = 1; + for (int i = 1; i < attempt; i++) { + long t = a + b; + a = b; + b = t; + } + return a; +} + +int write_all(int fd, const char *buf, size_t n) { + size_t off = 0; + while (off < n) { + ssize_t w = write(fd, buf + off, n - off); + if (w < 0) { + if (errno == EINTR) continue; + return -1; + } + if (w == 0) break; + off += (size_t)w; + } + return (off == n) ? 0 : -1; +} + +int iequals(const char *a, const char *b) { + while (*a && *b) { + char ca = *a, cb = *b; + if (ca >= 'A' && ca <= 'Z') ca += 32; + if (cb >= 'A' && cb <= 'Z') cb += 32; + if (ca != cb) return 0; + a++; + b++; + } + return *a == *b; +} + +void free_toolcalls(tool_call_t *tc, int n) { + if (!tc) return; + for (int i = 0; i < n; i++) { + free(tc[i].id); + free(tc[i].type); + free(tc[i].name); + free(tc[i].arguments); + } + free(tc); +}