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