This commit is contained in:
Luxferre
2026-10-12 07:43:34 +03:00
commit fc2e01da4d
7 changed files with 1554 additions and 0 deletions
+35
View File
@@ -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)
+89
View File
@@ -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 <n>` | `8080` | Listen port (binds `0.0.0.0`). |
| `-endpoint <url>` | `https://tencent-hy3.hf.space` | Upstream Gradio Space base URL (trailing slash trimmed). |
| `-model <name>` | `hy3` | Model name reported in `/v1/models` and responses. |
| `-user-agent <ua>` / `-ua <ua>` | built-in Firefox UA | Outgoing `User-Agent`. Empty string => default. |
| `-max-body <bytes>` | `5242880` (5 MiB) | Maximum request body size. Oversized requests are rejected with `413`. |
| `-max-conn <n>` | `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.
+117
View File
@@ -0,0 +1,117 @@
#ifndef HYGATE_H
#define HYGATE_H
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#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 */
+422
View File
@@ -0,0 +1,422 @@
#include <stdio.h>
#include "hygate.h"
#include <stdlib.h>
#include <string.h>
#include <json-c/json.h>
/* 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;
}
+129
View File
@@ -0,0 +1,129 @@
#include "hygate.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <stdint.h>
#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <curl/curl.h>
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;
}
+642
View File
@@ -0,0 +1,642 @@
#include "hygate.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <json-c/json.h>
#include <curl/curl.h>
/* ------------------------------------------------------------------ */
/* 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);
}
+120
View File
@@ -0,0 +1,120 @@
#include <stdio.h>
#include "hygate.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>
/* ----------------------------- 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);
}