init upl
This commit is contained in:
@@ -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)
|
||||||
@@ -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.
|
||||||
@@ -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 */
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user