init commit

This commit is contained in:
Luxferre
2026-10-14 09:18:19 +03:00
commit b8b03f3b41
10 changed files with 6159 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
.POSIX:
# Dynagate LLM Gateway (C++20 port) - POSIX-compliant Makefile
# Targets: Linux, FreeBSD, macOS, Haiku (compatible with bmake, gmake, smake, etc.)
# Dependencies: libcurl (HTTP). JSON is handled natively in json.cpp
# (no extra or vendored dependency).
# Override compiler or flags if needed, e.g.:
# make CXX=clang++ CPPFLAGS="-I/opt/homebrew/include -I." LDLIBS="-L/opt/homebrew/lib -lcurl -pthread"
#
# 32-bit build: make 32bit
# Or: make CXXFLAGS="-std=c++20 -O2 -m32" LDFLAGS="-m32"
# Large file support on 32-bit: make CPPFLAGS="-D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE"
CXX = c++
STRIP = strip
CXXFLAGS = -std=c++20 -s -Wall -Wextra -Wpedantic -O2 -march=native -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE -D_DARWIN_C_SOURCE
CPPFLAGS = -I/usr/local/include -I. -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
CURL_CFLAGS = $$(pkg-config --cflags libcurl 2>/dev/null || curl-config --cflags 2>/dev/null)
CURL_LIBS = $$(pkg-config --libs libcurl 2>/dev/null || curl-config --libs 2>/dev/null || echo '-lcurl')
LDFLAGS = -s
LDLIBS = -L/usr/local/lib $(CURL_LIBS) -pthread
# Location of the AddressSanitizer/UBSan runtime libraries plus a replacement
# libasan_preinit.o (so the runtime initializes before main()). On a normal
# system these ship with the compiler; override with
# make debug ASANDIR= (empty) — the gcc driver finds them automatically.
ASANDIR = /tmp/asanlib
CORE_OBJS = json.o config.o handler.o server.o
OBJS = main.o $(CORE_OBJS)
TEST_OBJS = gateway_test.o $(CORE_OBJS)
.PHONY: all build clean test run install debug 32bit
all: build
build: dynagate
dynagate: $(OBJS)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -o dynagate $(OBJS) $(LDLIBS)
$(STRIP) dynagate
main.o: main.cpp dynagate.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(CURL_CFLAGS) -c main.cpp
json.o: json.cpp dynagate.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(CURL_CFLAGS) -c json.cpp
config.o: config.cpp dynagate.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(CURL_CFLAGS) -c config.cpp
handler.o: handler.cpp dynagate.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(CURL_CFLAGS) -c handler.cpp
server.o: server.cpp dynagate.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(CURL_CFLAGS) -c server.cpp
gateway_test.o: gateway_test.cpp dynagate.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(CURL_CFLAGS) -c gateway_test.cpp
gateway_test: $(TEST_OBJS)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -o gateway_test $(TEST_OBJS) $(LDLIBS)
test: gateway_test
./gateway_test
run: build
./dynagate -port 8080 -csv models.csv
install: dynagate
install -m 0755 dynagate $(DESTDIR)/usr/local/bin/dynagate
# Debug build with AddressSanitizer + UndefinedBehaviorSanitizer.
# Rebuilds ./dynagate — run `make clean && make` afterwards to restore the
# optimized build. ASANDIR points the gcc driver (-B) at the runtime libs and
# the replacement libasan_preinit.o, and sets an rpath so they load at run time.
debug:
$(MAKE) CXXFLAGS="-std=c++20 -Wall -Wextra -Wpedantic -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE -D_DARWIN_C_SOURCE -g -O0 -fsanitize=address,undefined -B$(ASANDIR) -Wl,-rpath,$(ASANDIR)" LDFLAGS="" STRIP=":" dynagate
# 32-bit build target
# Usage: make 32bit
# This builds a 32-bit binary with large file support enabled.
32bit:
$(MAKE) CXXFLAGS="$(CXXFLAGS) -m32" LDFLAGS="$(LDFLAGS) -m32" dynagate
clean:
rm -f $(OBJS) gateway_test.o dynagate gateway_test
+210
View File
@@ -0,0 +1,210 @@
# Dynagate LLM gateway (C++ port)
Dynagate is a lightweight, high-performance LLM gateway that acts as a proxy for OpenAI-compatible APIs. This directory is a modern **C++20** port of the Go reference implementation (`dynagate`), using a native RFC 8259 JSON engine (`json.cpp`) and **libcurl** for HTTP networking.
It provides seamless model routing, automatic key and model failover, live configuration hot-reloading without restarts, token authentication, and custom header injection.
## Features
1. **Multi-API Protocol Endpoints (Stateless)**:
- `/v1/models` and `/models` (GET): Dynamically lists unique active model IDs.
- `/v1/chat/completions` and `/chat/completions` (POST): Proxies non-streaming and streaming (`text/event-stream`) completions.
- `/v1/responses` and `/responses` (POST): Stateless-style OpenAI Responses API endpoint (automatically routed to upstream chat completions endpoints).
- `/v1/messages` and `/messages` (POST): Anthropic-compatible Messages API endpoint (automatically routed to upstream chat completions endpoints).
- `/v1/images/generations` and `/images/generations` (POST): Proxies image generation requests.
> **Note** Dynagate operates in a completely stateless manner across all API formats (`/v1/chat/completions`, `/v1/responses`, `/v1/messages`). Dynagate does not store or persist server-side conversation state. Clients must include the full conversation history in each request for multi-turn dialogues.
2. **Zero-downtime live-reloading**:
- Watches `models.csv` (overridable via command-line flags) continuously using a background thread and automatically reloads configuration updates without dropping active connections.
3. **Fallback and retry orchestration**:
- Attempts keys configured for the requested model sequentially using Fibonacci backoff delays (`-retry-delay`).
- Cascades automatically to the next model in the priority chain (wrapping around to cover all entries) if all keys for the requested model fail.
- Updates the `"model"` field dynamically in the outgoing request body before proxying, ensuring upstream compatibility.
4. **Flexible authorization**:
- Optional gateway token authentication via command-line key file.
- Key options (`""`, `"-"`) to strip authorization headers when proxying to local or open upstreams.
- Special `"-blank-"` key config to pass an empty `Authorization: Bearer` header.
5. **External config updater**:
- Executes a periodic CLI command (`-csv-updater`) at a custom interval (`-csv-update-interval`) to fetch, generate, or download fresh model listings.
6. **Custom header injection (`extra` field)**:
- Sets or overrides custom headers dynamically per model deployment using a JSON configuration string.
## Tech stack
- **Language**: C++20 (`-std=c++20`)
- **Dependencies**:
- `libcurl` (HTTP/HTTPS upstream requests and SSE streaming)
- Native RFC 8259 JSON parser and serializer in `json.cpp` (no extra or vendored third-party JSON dependency)
- POSIX sockets and standard C++20 threads/atomics/filesystem
- **Target Platforms**: Linux, FreeBSD, macOS, Haiku, and any POSIX-compliant OS (64-bit and 32-bit hosts)
## Repository layout
```text
dynagate.hpp Shared declarations, data structures, and interfaces
json.cpp Native RFC 8259 JSON parser and serializer
config.cpp RFC 4180 CSV parser, ConfigManager, hot-reloader, CSV updater
handler.cpp Auth, normalization, protocol conversions, libcurl proxying
server.cpp Multi-threaded POSIX HTTP/1.1 server and streaming writer
main.cpp CLI flag parsing, signal handling, server lifecycle
gateway_test.cpp Comprehensive unit and end-to-end test suite
Makefile POSIX-compliant Makefile (bmake, gmake, smake compatible)
models.csv Example model configuration file
free-model-collector.sh Free model discovery and verification script
freekeys.sh Helper script for collecting free keys
```
## Prerequisites
- A C++20 compiler (`c++`, `g++`, or `clang++`)
- Any POSIX `make` (`bmake`, `gmake`, `make`)
- `libcurl` development headers and library:
```bash
# Debian / Ubuntu
apt-get install build-essential libcurl4-openssl-dev
# Fedora / RHEL
dnf install gcc-c++ libcurl-devel
# FreeBSD
pkg install curl
# macOS (Xcode Command Line Tools include libcurl, or via Homebrew)
xcode-select --install
```
## Getting started
### 1. Building from source
```bash
make # builds ./dynagate
make 32bit # builds a 32-bit ./dynagate binary with large file support
make debug # builds ./dynagate with AddressSanitizer + UBSan
make test # builds and runs the test suite
make clean # removes build artifacts
make install # installs to $(DESTDIR)/usr/local/bin/dynagate
```
If `libcurl` or custom headers live in a non-standard prefix, override flags on the `make` command line:
```bash
make CXX=clang++ CPPFLAGS="-I/opt/homebrew/include -I." LDLIBS="-L/opt/homebrew/lib -lcurl -pthread"
```
### 2. Configure models
Create a `models.csv` file in the same directory. The file supports both header and header-less formats.
**Example `models.csv` with Header**:
```csv
model,key,endpoint,extra
gpt-4o,sk-proj-key1,https://api.openai.com/v1,"{""Openai-Organization"":""org-123""}"
gpt-4o,sk-proj-key2,https://api.openai.com/v1,
gpt-3.5-turbo,-blank-,https://api.openai.com/v1,
deepseek-chat,sk-ds-key,https://api.deepseek.com/v1,"{""X-Custom-Env"":""production""}"
ollama-llama3,-,http://localhost:11434/v1,
```
### 3. Setup gateway key (optional)
To restrict access to your gateway, write one or more plaintext keys (one per line) to a file (e.g. `gatekey.txt`):
```text
my-secure-gateway-token-1
my-secure-gateway-token-2
```
### 4. Run the server
```bash
./dynagate -port 8080 -csv models.csv -key gatekey.txt
```
## Configuration reference
### Command line flags
| Flag | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `-port` | int | `8080` | Port the gateway listens on. |
| `-csv` | string | `models.csv` | Path to the model configuration CSV. |
| `-key` | string | `""` | Path to a file containing the gateway's access tokens (one per line). If blank, authentication is disabled. |
| `-csv-updater` | string | `""` | Command/script to run periodically to update `models.csv`. |
| `-csv-update-interval`| int | `10` | Frequency in minutes to invoke `-csv-updater`. |
| `-retry-delay` | int | `100` | Base delay in milliseconds for Fibonacci failover retries (0 to disable). |
### Model CSV format rules
The gateway maps columns dynamically by looking at the header row. If no header is present, it defaults to:
- Column 0: `model` (Model ID)
- Column 1: `key` (API Key / Authorization Token)
- Column 2: `endpoint` (Upstream base URL endpoint)
- Column 3: `extra` (Optional JSON string object containing header mappings)
#### Auth override key values:
- **`""` or `"-"`**: The gateway will strip the `Authorization` header completely (useful for local LLM endpoints or unauthenticated proxies).
- **`"-blank-"`**: The gateway will attach `Authorization: Bearer` without any key appended.
- **Any other string**: The gateway will attach `Authorization: Bearer <key>`.
### Stateless Operation & Multi-Turn Conversations
Dynagate is designed as a lightweight, zero-state proxy gateway. All supported request formats (`/v1/chat/completions`, `/v1/responses`, `/v1/messages`) are processed statelessly without storing session context or conversation state on disk or in memory.
To maintain context in multi-turn conversations, clients **must include the entire conversation history** in every request payload:
- **OpenAI Chat Completions (`/v1/chat/completions`)**: Pass the full array of system, user, and assistant messages in `messages`.
- **OpenAI Responses API (`/v1/responses`)**: Pass previous conversation turns in `input` (or `messages`) along with `instructions`.
- **Anthropic Messages API (`/v1/messages`)**: Pass the full history of user and assistant messages in `messages` along with `system`.
### Manual testing with curl
#### 1. Model listing (authenticated)
```bash
curl -i -H "Authorization: Bearer my-secure-gateway-token" http://localhost:8080/v1/models
```
#### 2. Chat completions proxying
```bash
curl -i -X POST http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer my-secure-gateway-token" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Explain gravity in one sentence"}]
}'
```
#### 3. Chat completions streaming
```bash
curl -i -X POST http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer my-secure-gateway-token" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"stream": true,
"messages": [{"role": "user", "content": "Count from 1 to 5"}]
}'
```
#### 4. OpenAI-compatible Responses API (`/v1/responses`)
```bash
curl -i -X POST http://localhost:8080/v1/responses \
-H "Authorization: Bearer my-secure-gateway-token" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"instructions": "Be concise",
"input": "Explain relativity in one sentence"
}'
```
#### 5. Anthropic-compatible Messages API (`/v1/messages`)
```bash
curl -i -X POST http://localhost:8080/v1/messages \
-H "Authorization: Bearer my-secure-gateway-token" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-3-5-sonnet",
"system": "You are a helpful assistant",
"messages": [{"role": "user", "content": "Hello world"}]
}'
```
## Credits
Created by Luxferre in 2026, released into the public domain with no warranties.
+576
View File
@@ -0,0 +1,576 @@
/*
Dynagate config manager, CSV parser, and utilities (C++20 port)
Created by Luxferre in 2026, released into the public domain
*/
#ifndef _DARWIN_C_SOURCE
#define _DARWIN_C_SOURCE
#endif
#include "dynagate.hpp"
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <fstream>
#include <sstream>
#include <thread>
#include <unordered_set>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
namespace {
std::mutex g_log_mu;
std::string run_shell_command(const std::string &command, int &exit_status) {
exit_status = -1;
int pipefd[2];
if (pipe(pipefd) != 0) {
return "pipe() failed: " + std::string(std::strerror(errno));
}
pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
return "fork() failed: " + std::string(std::strerror(errno));
}
if (pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
execl("/bin/sh", "sh", "-c", command.c_str(), static_cast<char *>(nullptr));
_exit(127);
}
close(pipefd[1]);
std::string output;
char buf[4096];
for (;;) {
ssize_t n = read(pipefd[0], buf, sizeof(buf));
if (n > 0) {
output.append(buf, static_cast<size_t>(n));
} else if (n == 0) {
break;
} else if (errno != EINTR) {
break;
}
}
close(pipefd[0]);
int status = 0;
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
break;
}
}
if (WIFEXITED(status)) {
exit_status = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
exit_status = 128 + WTERMSIG(status);
}
return output;
}
} // namespace
void log_msg(const char *fmt, ...) {
std::lock_guard<std::mutex> lock(g_log_mu);
std::time_t now = std::time(nullptr);
struct tm tm_now;
localtime_r(&now, &tm_now);
char time_buf[32];
std::strftime(time_buf, sizeof(time_buf), "%Y/%m/%d %H:%M:%S", &tm_now);
char msg_buf[2048];
va_list ap;
va_start(ap, fmt);
std::vsnprintf(msg_buf, sizeof(msg_buf), fmt, ap);
va_end(ap);
std::fprintf(stderr, "%s %s\n", time_buf, msg_buf);
std::fflush(stderr);
}
std::string str_trim(std::string_view sv) {
size_t start = 0;
while (start < sv.size() &&
std::isspace(static_cast<unsigned char>(sv[start]))) {
++start;
}
size_t end = sv.size();
while (end > start &&
std::isspace(static_cast<unsigned char>(sv[end - 1]))) {
--end;
}
return std::string(sv.substr(start, end - start));
}
std::string str_to_lower(std::string_view sv) {
std::string out(sv);
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return out;
}
bool str_iequals(std::string_view a, std::string_view b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); ++i) {
if (std::tolower(static_cast<unsigned char>(a[i])) !=
std::tolower(static_cast<unsigned char>(b[i]))) {
return false;
}
}
return true;
}
void HeaderList::add(std::string_view key, std::string_view value) {
items.emplace_back(std::string(key), std::string(value));
}
void HeaderList::set(std::string_view key, std::string_view value) {
del(key);
items.emplace_back(std::string(key), std::string(value));
}
void HeaderList::del(std::string_view key) {
items.erase(
std::remove_if(items.begin(), items.end(), [&](const auto &kv) {
return str_iequals(kv.first, key);
}),
items.end());
}
std::string HeaderList::get(std::string_view key) const {
for (const auto &kv : items) {
if (str_iequals(kv.first, key)) {
return kv.second;
}
}
return "";
}
std::vector<std::string> HeaderList::values(std::string_view key) const {
std::vector<std::string> res;
for (const auto &kv : items) {
if (str_iequals(kv.first, key)) {
res.push_back(kv.second);
}
}
return res;
}
bool parse_csv(std::string_view content,
std::vector<std::vector<std::string>> &rows,
std::string *err_out) {
rows.clear();
size_t i = 0;
const size_t n = content.size();
while (i < n) {
// Skip empty lines (matching Go encoding/csv behavior)
if (content[i] == '\n') {
++i;
continue;
}
if (content[i] == '\r' && i + 1 < n && content[i + 1] == '\n') {
i += 2;
continue;
}
std::vector<std::string> row;
bool line_done = false;
while (!line_done) {
std::string field;
if (i < n && content[i] == '"') {
// Quoted field
++i;
bool closed = false;
while (i < n) {
if (content[i] == '"') {
if (i + 1 < n && content[i + 1] == '"') {
field.push_back('"');
i += 2;
} else {
++i;
closed = true;
break;
}
} else {
if (content[i] == '\r' && i + 1 < n && content[i + 1] == '\n') {
field.push_back('\n');
i += 2;
} else {
field.push_back(content[i]);
++i;
}
}
}
if (!closed) {
if (err_out) {
*err_out = "parse error: bare or unclosed \" in quoted-field";
}
return false;
}
if (i < n) {
if (content[i] == ',') {
row.push_back(std::move(field));
++i;
if (i == n || content[i] == '\n' ||
(content[i] == '\r' && i + 1 < n && content[i + 1] == '\n')) {
row.emplace_back("");
if (i < n && content[i] == '\r') {
i += 2;
} else if (i < n && content[i] == '\n') {
++i;
}
line_done = true;
}
} else if (content[i] == '\n') {
row.push_back(std::move(field));
++i;
line_done = true;
} else if (content[i] == '\r' && i + 1 < n &&
content[i + 1] == '\n') {
row.push_back(std::move(field));
i += 2;
line_done = true;
} else {
if (err_out) {
*err_out = "parse error: extraneous or missing \" in quoted-field";
}
return false;
}
} else {
row.push_back(std::move(field));
line_done = true;
}
} else {
// Unquoted field
while (i < n && content[i] != ',' && content[i] != '\n' &&
!(content[i] == '\r' && i + 1 < n && content[i + 1] == '\n')) {
if (content[i] == '"') {
if (err_out) {
*err_out = "parse error: bare \" in non-quoted-field";
}
return false;
}
field.push_back(content[i]);
++i;
}
row.push_back(std::move(field));
if (i < n && content[i] == ',') {
++i;
if (i == n || content[i] == '\n' ||
(content[i] == '\r' && i + 1 < n && content[i + 1] == '\n')) {
row.emplace_back("");
if (i < n && content[i] == '\r') {
i += 2;
} else if (i < n && content[i] == '\n') {
++i;
}
line_done = true;
}
} else {
if (i < n && content[i] == '\r' && i + 1 < n &&
content[i + 1] == '\n') {
i += 2;
} else if (i < n && content[i] == '\n') {
++i;
}
line_done = true;
}
}
}
rows.push_back(std::move(row));
}
return true;
}
std::vector<std::string> parse_auth_tokens(std::string_view content) {
std::vector<std::string> tokens;
size_t pos = 0;
while (pos <= content.size()) {
size_t nl = content.find('\n', pos);
std::string_view line = (nl == std::string_view::npos)
? content.substr(pos)
: content.substr(pos, nl - pos);
std::string token = str_trim(line);
if (!token.empty()) {
tokens.push_back(std::move(token));
}
if (nl == std::string_view::npos) {
break;
}
pos = nl + 1;
}
return tokens;
}
ConfigManager::ConfigManager(std::string file_path)
: file_path_(std::move(file_path)) {
std::string err;
if (!load(&err)) {
log_msg(
"Initial configuration load failed or file not found (will retry on change): %s",
err.c_str());
}
}
ConfigManager::ConfigManager(std::vector<ModelConfig> configs,
std::vector<std::string> unique_models)
: configs_(std::move(configs)), unique_models_(std::move(unique_models)) {}
bool ConfigManager::load(std::string *err_out) {
std::error_code ec;
auto mod_time = std::filesystem::last_write_time(file_path_, ec);
if (ec) {
if (err_out) {
*err_out = ec.message();
}
return false;
}
auto fsize = std::filesystem::file_size(file_path_, ec);
if (ec) {
if (err_out) {
*err_out = ec.message();
}
return false;
}
std::ifstream ifs(file_path_, std::ios::binary);
if (!ifs.is_open()) {
if (err_out) {
*err_out = "failed to open " + file_path_;
}
return false;
}
std::ostringstream oss;
oss << ifs.rdbuf();
std::string content = oss.str();
std::vector<std::vector<std::string>> rows;
if (!parse_csv(content, rows, err_out)) {
return false;
}
std::vector<ModelConfig> configs;
if (rows.empty()) {
update(std::move(configs), mod_time, fsize, true);
return true;
}
// Detect header row
bool has_header = false;
const auto &first_row = rows[0];
for (const auto &col : first_row) {
std::string col_lower = str_to_lower(str_trim(col));
if (col_lower == "model" || col_lower == "key" || col_lower == "endpoint") {
has_header = true;
break;
}
}
size_t start_idx = 0;
int model_idx = 0;
int key_idx = 1;
int endpoint_idx = 2;
int extra_idx = -1;
if (has_header) {
start_idx = 1;
model_idx = -1;
key_idx = -1;
endpoint_idx = -1;
extra_idx = -1;
for (size_t i = 0; i < first_row.size(); ++i) {
std::string col_lower = str_to_lower(str_trim(first_row[i]));
if (col_lower == "model") {
model_idx = static_cast<int>(i);
} else if (col_lower == "key") {
key_idx = static_cast<int>(i);
} else if (col_lower == "endpoint") {
endpoint_idx = static_cast<int>(i);
} else if (col_lower == "extra") {
extra_idx = static_cast<int>(i);
}
}
if (model_idx == -1) {
model_idx = 0;
}
if (key_idx == -1) {
key_idx = 1;
}
if (endpoint_idx == -1) {
endpoint_idx = 2;
}
} else {
start_idx = 0;
}
for (size_t i = start_idx; i < rows.size(); ++i) {
const auto &row = rows[i];
int max_idx = std::max({model_idx, key_idx, endpoint_idx});
if (static_cast<int>(row.size()) <= max_idx) {
continue;
}
std::string m = str_trim(row[static_cast<size_t>(model_idx)]);
std::string k = str_trim(row[static_cast<size_t>(key_idx)]);
std::string ep = str_trim(row[static_cast<size_t>(endpoint_idx)]);
if (m.empty() || k.empty() || ep.empty()) {
continue;
}
std::string extra;
if (has_header) {
if (extra_idx != -1 && static_cast<int>(row.size()) > extra_idx) {
extra = str_trim(row[static_cast<size_t>(extra_idx)]);
}
} else {
if (row.size() > 3) {
extra = str_trim(row[3]);
}
}
configs.push_back(ModelConfig{
std::move(m),
std::move(k),
std::move(ep),
std::move(extra),
});
}
size_t count = configs.size();
update(std::move(configs), mod_time, fsize, true);
log_msg("Loaded %zu model configurations from %s", count, file_path_.c_str());
return true;
}
void ConfigManager::update(std::vector<ModelConfig> configs,
std::filesystem::file_time_type mod_time,
std::uintmax_t size,
bool has_stat) {
std::unique_lock<std::shared_mutex> lock(mu_);
configs_ = std::move(configs);
last_mod_time_ = mod_time;
last_size_ = size;
has_stat_ = has_stat;
std::vector<std::string> unique_models;
std::unordered_set<std::string> seen;
for (const auto &cfg : configs_) {
if (seen.insert(cfg.model).second) {
unique_models.push_back(cfg.model);
}
}
unique_models_ = std::move(unique_models);
}
std::vector<ModelConfig> ConfigManager::get_configs() const {
std::shared_lock<std::shared_mutex> lock(mu_);
return configs_;
}
std::vector<std::string> ConfigManager::get_unique_models() const {
std::shared_lock<std::shared_mutex> lock(mu_);
return unique_models_;
}
void ConfigManager::watch(std::atomic<bool> &stop_flag) {
while (!stop_flag.load()) {
for (int i = 0; i < 20 && !stop_flag.load(); ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
if (stop_flag.load()) {
return;
}
std::error_code ec;
auto mod_time = std::filesystem::last_write_time(file_path_, ec);
if (ec) {
continue;
}
auto fsize = std::filesystem::file_size(file_path_, ec);
if (ec) {
continue;
}
bool changed = false;
{
std::shared_lock<std::shared_mutex> lock(mu_);
if (!has_stat_ || mod_time > last_mod_time_ || fsize != last_size_) {
changed = true;
}
}
if (changed) {
log_msg("Detected changes in %s, reloading...", file_path_.c_str());
std::string err;
if (!load(&err)) {
log_msg("Error reloading configuration: %s", err.c_str());
} else {
log_msg("Configuration reloaded successfully");
}
}
}
}
void start_csv_updater(std::atomic<bool> &stop_flag,
const std::string &command,
std::chrono::milliseconds interval) {
auto run_updater = [&]() {
log_msg("Running CSV updater command: %s", command.c_str());
int exit_status = 0;
std::string output = run_shell_command(command, exit_status);
if (exit_status != 0) {
std::string trimmed = str_trim(output);
log_msg("CSV updater command failed: exit status %d, output: \"%s\"",
exit_status, trimmed.c_str());
} else {
log_msg("CSV updater command completed successfully");
}
};
// Run once immediately at startup
run_updater();
while (!stop_flag.load()) {
auto start = std::chrono::steady_clock::now();
while (!stop_flag.load()) {
auto elapsed = std::chrono::steady_clock::now() - start;
if (elapsed >= interval) {
break;
}
auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
interval - elapsed);
auto sleep_dur = std::min(remaining, std::chrono::milliseconds(50));
if (sleep_dur.count() > 0) {
std::this_thread::sleep_for(sleep_dur);
}
}
if (stop_flag.load()) {
return;
}
run_updater();
}
}
+278
View File
@@ -0,0 +1,278 @@
/*
Dynagate LLM Gateway: dynamically change models on the fly (C++20 port)
Created by Luxferre in 2026, released into the public domain
*/
#ifndef DYNAGATE_HPP
#define DYNAGATE_HPP
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <initializer_list>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
// Minimal RFC 8259 JSON value, parser and serializer (json.cpp)
enum class JType { Null, Bool, Int, Float, Str, Arr, Obj };
struct Json {
JType type = JType::Null;
bool bval = false;
int64_t ival = 0;
double dval = 0.0;
std::string sval;
std::vector<Json> aval;
std::vector<std::pair<std::string, Json>> oval;
Json();
~Json();
Json(const Json &);
Json(Json &&) noexcept;
Json &operator=(const Json &);
Json &operator=(Json &&) noexcept;
Json(std::nullptr_t);
Json(bool b);
Json(int n);
Json(int64_t n);
Json(double d);
Json(const char *s);
Json(std::string s);
Json(std::string_view s);
static Json object();
static Json object(
std::initializer_list<std::pair<std::string_view, Json>> init);
static Json array();
static Json array(std::initializer_list<Json> init);
bool is_null() const { return type == JType::Null; }
bool is_bool() const { return type == JType::Bool; }
bool is_boolean() const { return type == JType::Bool; }
bool is_number_integer() const { return type == JType::Int; }
bool is_number() const { return type == JType::Int || type == JType::Float; }
bool is_string() const { return type == JType::Str; }
bool is_array() const { return type == JType::Arr; }
bool is_object() const { return type == JType::Obj; }
bool empty() const;
size_t size() const;
bool contains(std::string_view k) const;
void erase(std::string_view k);
Json &operator[](std::string_view k);
const Json &operator[](std::string_view k) const;
Json &at(size_t i);
const Json &at(size_t i) const;
void push_back(Json v);
std::string str(std::string_view def = "") const;
bool as_bool(bool def = false) const;
int64_t as_int64(int64_t def = 0) const;
int as_int(int def = 0) const;
std::string dump(int indent = -1) const;
static bool parse(std::string_view src, Json &out);
static Json parse(std::string_view src);
};
struct ModelConfig {
std::string model;
std::string key;
std::string endpoint;
std::string extra;
bool operator==(const ModelConfig &other) const = default;
};
class ConfigManager {
public:
explicit ConfigManager(std::string file_path);
ConfigManager(std::vector<ModelConfig> configs,
std::vector<std::string> unique_models);
bool load(std::string *err_out = nullptr);
std::vector<ModelConfig> get_configs() const;
std::vector<std::string> get_unique_models() const;
void watch(std::atomic<bool> &stop_flag);
private:
void update(std::vector<ModelConfig> configs,
std::filesystem::file_time_type mod_time,
std::uintmax_t size,
bool has_stat);
std::string file_path_;
mutable std::shared_mutex mu_;
std::vector<ModelConfig> configs_;
std::vector<std::string> unique_models_;
std::filesystem::file_time_type last_mod_time_{};
std::uintmax_t last_size_ = 0;
bool has_stat_ = false;
};
struct HeaderList {
std::vector<std::pair<std::string, std::string>> items;
void add(std::string_view key, std::string_view value);
void set(std::string_view key, std::string_view value);
void del(std::string_view key);
std::string get(std::string_view key) const;
std::vector<std::string> values(std::string_view key) const;
};
struct HttpRequest {
std::string method;
std::string path;
HeaderList headers;
std::string body;
int client_fd = -1;
std::function<bool()> is_cancelled;
};
class ResponseWriter {
public:
virtual ~ResponseWriter() = default;
virtual HeaderList &header() = 0;
virtual void write_header(int status_code) = 0;
virtual bool write(std::string_view data) = 0;
virtual void flush() = 0;
};
class RecorderResponseWriter : public ResponseWriter {
public:
HeaderList &header() override { return headers_; }
void write_header(int status_code) override {
if (!header_written_) {
status_code_ = status_code;
header_written_ = true;
}
}
bool write(std::string_view data) override {
if (!header_written_) {
write_header(200);
}
body_.append(data.data(), data.size());
return true;
}
void flush() override {}
int status_code() const { return status_code_; }
const HeaderList &headers() const { return headers_; }
const std::string &body() const { return body_; }
private:
int status_code_ = 200;
bool header_written_ = false;
HeaderList headers_;
std::string body_;
};
// Logging and string utilities (config.cpp)
void log_msg(const char *fmt, ...);
std::string str_trim(std::string_view sv);
std::string str_to_lower(std::string_view sv);
bool str_iequals(std::string_view a, std::string_view b);
bool parse_csv(std::string_view content,
std::vector<std::vector<std::string>> &rows,
std::string *err_out = nullptr);
std::vector<std::string> parse_auth_tokens(std::string_view content);
void start_csv_updater(std::atomic<bool> &stop_flag,
const std::string &command,
std::chrono::milliseconds interval);
// Core gateway logic and handlers (handler.cpp)
void ensure_curl_initialized();
bool check_auth(const std::vector<std::string> &expected_tokens,
const HttpRequest &req);
void send_unauthorized(ResponseWriter &w);
std::vector<ModelConfig> get_trial_configs(
const std::vector<ModelConfig> &configs,
const std::vector<std::string> &unique_models,
std::string_view requested_model);
std::string build_url(std::string_view endpoint, std::string_view req_path);
int64_t fibonacci(int n);
bool is_media_content(const Json &m);
Json normalize_content(const Json &content_any);
void autodetect_and_normalize_messages(Json &body_map);
Json convert_anthropic_to_openai(const Json &body_map);
Json convert_anthropic_content_to_openai(const Json &cnt_any);
Json convert_openai_to_anthropic_response(
const Json &openai_resp,
const std::string &requested_model);
Json convert_responses_to_openai(const Json &body_map);
Json convert_openai_to_responses_response(
const Json &openai_resp,
const std::string &requested_model);
void handle_models(ConfigManager &cm,
const std::vector<std::string> &expected_tokens,
const HttpRequest &req,
ResponseWriter &w);
void handle_chat_completions(
ConfigManager &cm,
const std::vector<std::string> &expected_tokens,
std::chrono::milliseconds retry_base_delay,
const HttpRequest &req,
ResponseWriter &w);
void handle_image_generations(
ConfigManager &cm,
const std::vector<std::string> &expected_tokens,
std::chrono::milliseconds retry_base_delay,
const HttpRequest &req,
ResponseWriter &w);
void handle_messages(
ConfigManager &cm,
const std::vector<std::string> &expected_tokens,
std::chrono::milliseconds retry_base_delay,
const HttpRequest &req,
ResponseWriter &w);
void handle_responses(
ConfigManager &cm,
const std::vector<std::string> &expected_tokens,
std::chrono::milliseconds retry_base_delay,
const HttpRequest &req,
ResponseWriter &w);
// HTTP server (server.cpp)
class GatewayServer {
public:
GatewayServer(int port,
ConfigManager &cm,
std::vector<std::string> auth_tokens,
std::chrono::milliseconds retry_delay);
~GatewayServer();
GatewayServer(const GatewayServer &) = delete;
GatewayServer &operator=(const GatewayServer &) = delete;
bool start(std::string *err_out = nullptr);
void serve();
void stop();
int bound_port() const { return bound_port_; }
private:
void handle_client(int client_fd);
int requested_port_;
int bound_port_ = 0;
int listen_fd_ = -1;
ConfigManager &cm_;
std::vector<std::string> auth_tokens_;
std::chrono::milliseconds retry_delay_;
std::atomic<bool> stopping_{false};
mutable std::mutex active_mu_;
std::condition_variable active_cv_;
int active_connections_ = 0;
static constexpr int kMaxConcurrentConnections = 256;
};
#endif // DYNAGATE_HPP
+1405
View File
File diff suppressed because it is too large Load Diff
+1887
View File
File diff suppressed because it is too large Load Diff
+746
View File
@@ -0,0 +1,746 @@
/*
Dynagate native RFC 8259 JSON parser and serializer (C++20 port)
Created by Luxferre in 2026, released into the public domain
*/
#include "dynagate.hpp"
#include <cerrno>
#include <climits>
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace {
void str_appendf(std::string &s, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
va_list cp;
va_copy(cp, ap);
int need = std::vsnprintf(nullptr, 0, fmt, cp);
va_end(cp);
if (need > 0) {
std::string more;
more.resize(static_cast<size_t>(need));
std::vsnprintf(more.data(), static_cast<size_t>(need) + 1, fmt, ap);
s += more;
}
va_end(ap);
}
int utf8_decode(const unsigned char *p, size_t avail, size_t *plen) {
if (avail == 0) {
*plen = 0;
return 0xFFFD;
}
unsigned char b0 = p[0];
if (b0 < 0x80) {
*plen = 1;
return b0;
}
if ((b0 & 0xE0) == 0xC0) {
if (avail < 2 || (p[1] & 0xC0) != 0x80) {
*plen = 1;
return 0xFFFD;
}
int cp = ((b0 & 0x1F) << 6) | (p[1] & 0x3F);
if (cp < 0x80) {
*plen = 1;
return 0xFFFD;
}
*plen = 2;
return cp;
}
if ((b0 & 0xF0) == 0xE0) {
if (avail < 3 || (p[1] & 0xC0) != 0x80 || (p[2] & 0xC0) != 0x80) {
*plen = 1;
return 0xFFFD;
}
int cp = ((b0 & 0x0F) << 12) | ((p[1] & 0x3F) << 6) | (p[2] & 0x3F);
if (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF)) {
*plen = 1;
return 0xFFFD;
}
*plen = 3;
return cp;
}
if ((b0 & 0xF8) == 0xF0) {
if (avail < 4 || (p[1] & 0xC0) != 0x80 || (p[2] & 0xC0) != 0x80 ||
(p[3] & 0xC0) != 0x80) {
*plen = 1;
return 0xFFFD;
}
int cp = ((b0 & 0x07) << 18) | ((p[1] & 0x3F) << 12) |
((p[2] & 0x3F) << 6) | (p[3] & 0x3F);
if (cp < 0x10000 || cp > 0x10FFFF) {
*plen = 1;
return 0xFFFD;
}
*plen = 4;
return cp;
}
*plen = 1;
return 0xFFFD;
}
const Json kNullJson{};
void dump_str(std::string &out, std::string_view s) {
out += '"';
const auto *p = reinterpret_cast<const unsigned char *>(s.data());
size_t avail = s.size();
while (avail > 0) {
unsigned char c = p[0];
if (c < 0x80) {
p++;
avail--;
switch (c) {
case '"':
out += "\\\"";
break;
case '\\':
out += "\\\\";
break;
case '\b':
out += "\\b";
break;
case '\f':
out += "\\f";
break;
case '\n':
out += "\\n";
break;
case '\r':
out += "\\r";
break;
case '\t':
out += "\\t";
break;
default:
if (c < 0x20) {
str_appendf(out, "\\u%04x", static_cast<unsigned>(c));
} else {
out += static_cast<char>(c);
}
break;
}
} else {
size_t pl = 0;
int cp = utf8_decode(p, avail, &pl);
if (cp == 0xFFFD && pl == 1) {
out += "\xEF\xBF\xBD";
p++;
avail--;
} else {
out.append(reinterpret_cast<const char *>(p), pl);
p += pl;
avail -= pl;
}
}
}
out += '"';
}
void dump_rec(std::string &out, const Json &j, int indent, int depth) {
switch (j.type) {
case JType::Null:
out += "null";
break;
case JType::Bool:
out += (j.bval ? "true" : "false");
break;
case JType::Int:
str_appendf(out, "%lld", static_cast<long long>(j.ival));
break;
case JType::Float:
if (!std::isfinite(j.dval)) {
out += "null";
} else {
str_appendf(out, "%.15g", j.dval);
}
break;
case JType::Str:
dump_str(out, j.sval);
break;
case JType::Arr:
if (j.aval.empty()) {
out += "[]";
} else {
out += '[';
for (size_t i = 0; i < j.aval.size(); i++) {
if (i > 0) {
out += ',';
}
if (indent >= 0) {
out += '\n';
out.append(static_cast<size_t>(depth + 1) *
static_cast<size_t>(indent),
' ');
}
dump_rec(out, j.aval[i], indent, depth + 1);
}
if (indent >= 0) {
out += '\n';
out.append(static_cast<size_t>(depth) * static_cast<size_t>(indent),
' ');
}
out += ']';
}
break;
case JType::Obj:
if (j.oval.empty()) {
out += "{}";
} else {
out += '{';
for (size_t i = 0; i < j.oval.size(); i++) {
if (i > 0) {
out += ',';
}
if (indent >= 0) {
out += '\n';
out.append(static_cast<size_t>(depth + 1) *
static_cast<size_t>(indent),
' ');
}
dump_str(out, j.oval[i].first);
out += (indent >= 0) ? ": " : ":";
dump_rec(out, j.oval[i].second, indent, depth + 1);
}
if (indent >= 0) {
out += '\n';
out.append(static_cast<size_t>(depth) * static_cast<size_t>(indent),
' ');
}
out += '}';
}
break;
}
}
struct JParser {
std::string_view src;
size_t pos = 0;
void skip_ws() {
while (pos < src.size()) {
char c = src[pos];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
pos++;
} else {
break;
}
}
}
static int hex_val(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
return -1;
}
bool parse_hex4(uint32_t &out) {
if (pos + 4 > src.size()) {
return false;
}
uint32_t v = 0;
for (int i = 0; i < 4; i++) {
int h = hex_val(src[pos + static_cast<size_t>(i)]);
if (h < 0) {
return false;
}
v = (v << 4) | static_cast<uint32_t>(h);
}
pos += 4;
out = v;
return true;
}
static void append_utf8(std::string &s, uint32_t cp) {
if (cp <= 0x7F) {
s += static_cast<char>(cp);
} else if (cp <= 0x7FF) {
s += static_cast<char>(0xC0 | (cp >> 6));
s += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp <= 0xFFFF) {
s += static_cast<char>(0xE0 | (cp >> 12));
s += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
s += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp <= 0x10FFFF) {
s += static_cast<char>(0xF0 | (cp >> 18));
s += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
s += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
s += static_cast<char>(0x80 | (cp & 0x3F));
} else {
s += "\xEF\xBF\xBD";
}
}
bool parse_string(std::string &out) {
if (pos >= src.size() || src[pos] != '"') {
return false;
}
pos++;
out.clear();
while (pos < src.size()) {
auto c = static_cast<unsigned char>(src[pos++]);
if (c == '"') {
return true;
}
if (c < 0x20) {
return false;
}
if (c != '\\') {
out += static_cast<char>(c);
continue;
}
if (pos >= src.size()) {
return false;
}
char esc = src[pos++];
switch (esc) {
case '"':
out += '"';
break;
case '\\':
out += '\\';
break;
case '/':
out += '/';
break;
case 'b':
out += '\b';
break;
case 'f':
out += '\f';
break;
case 'n':
out += '\n';
break;
case 'r':
out += '\r';
break;
case 't':
out += '\t';
break;
case 'u': {
uint32_t cp = 0;
if (!parse_hex4(cp)) {
return false;
}
if (cp >= 0xD800 && cp <= 0xDBFF) {
if (pos + 6 <= src.size() && src[pos] == '\\' &&
src[pos + 1] == 'u') {
size_t saved = pos;
pos += 2;
uint32_t low = 0;
if (parse_hex4(low) && low >= 0xDC00 && low <= 0xDFFF) {
cp = 0x10000u + (((cp - 0xD800u) << 10) | (low - 0xDC00u));
} else {
pos = saved;
cp = 0xFFFDu;
}
} else {
cp = 0xFFFDu;
}
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
cp = 0xFFFDu;
}
append_utf8(out, cp);
break;
}
default:
return false;
}
}
return false;
}
bool parse_number(Json &out) {
size_t start = pos;
if (src[pos] == '-') {
pos++;
}
if (pos >= src.size()) {
return false;
}
if (src[pos] == '0') {
pos++;
if (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') {
return false;
}
} else if (src[pos] >= '1' && src[pos] <= '9') {
while (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') {
pos++;
}
} else {
return false;
}
bool is_float = false;
if (pos < src.size() && src[pos] == '.') {
is_float = true;
pos++;
if (pos >= src.size() || src[pos] < '0' || src[pos] > '9') {
return false;
}
while (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') {
pos++;
}
}
if (pos < src.size() && (src[pos] == 'e' || src[pos] == 'E')) {
is_float = true;
pos++;
if (pos < src.size() && (src[pos] == '+' || src[pos] == '-')) {
pos++;
}
if (pos >= src.size() || src[pos] < '0' || src[pos] > '9') {
return false;
}
while (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') {
pos++;
}
}
std::string num_str(src.substr(start, pos - start));
if (!is_float) {
errno = 0;
char *end = nullptr;
long long iv = std::strtoll(num_str.c_str(), &end, 10);
if (errno == 0 && end == num_str.c_str() + num_str.size()) {
out = Json(static_cast<int64_t>(iv));
return true;
}
}
errno = 0;
char *end = nullptr;
double dv = std::strtod(num_str.c_str(), &end);
if (end != num_str.c_str() + num_str.size() || !std::isfinite(dv)) {
return false;
}
out = Json(dv);
return true;
}
bool parse_value(Json &out, int depth) {
if (depth > 256) {
return false;
}
skip_ws();
if (pos >= src.size()) {
return false;
}
char c = src[pos];
if (c == 'n') {
if (src.substr(pos, 4) != "null") {
return false;
}
pos += 4;
out = Json(nullptr);
return true;
}
if (c == 't') {
if (src.substr(pos, 4) != "true") {
return false;
}
pos += 4;
out = Json(true);
return true;
}
if (c == 'f') {
if (src.substr(pos, 5) != "false") {
return false;
}
pos += 5;
out = Json(false);
return true;
}
if (c == '"') {
std::string s;
if (!parse_string(s)) {
return false;
}
out = Json(std::move(s));
return true;
}
if (c == '[') {
pos++;
skip_ws();
out = Json::array();
if (pos < src.size() && src[pos] == ']') {
pos++;
return true;
}
while (true) {
Json elem;
if (!parse_value(elem, depth + 1)) {
return false;
}
out.aval.push_back(std::move(elem));
skip_ws();
if (pos < src.size() && src[pos] == ',') {
pos++;
continue;
}
if (pos < src.size() && src[pos] == ']') {
pos++;
return true;
}
return false;
}
}
if (c == '{') {
pos++;
skip_ws();
out = Json::object();
if (pos < src.size() && src[pos] == '}') {
pos++;
return true;
}
while (true) {
skip_ws();
std::string key;
if (!parse_string(key)) {
return false;
}
skip_ws();
if (pos >= src.size() || src[pos] != ':') {
return false;
}
pos++;
Json val;
if (!parse_value(val, depth + 1)) {
return false;
}
out[key] = std::move(val);
skip_ws();
if (pos < src.size() && src[pos] == ',') {
pos++;
continue;
}
if (pos < src.size() && src[pos] == '}') {
pos++;
return true;
}
return false;
}
}
if (c == '-' || (c >= '0' && c <= '9')) {
return parse_number(out);
}
return false;
}
};
} // namespace
Json::Json() = default;
Json::~Json() = default;
Json::Json(const Json &) = default;
Json::Json(Json &&) noexcept = default;
Json &Json::operator=(const Json &) = default;
Json &Json::operator=(Json &&) noexcept = default;
Json::Json(std::nullptr_t) : type(JType::Null) {}
Json::Json(bool b) : type(JType::Bool), bval(b) {}
Json::Json(int n) : type(JType::Int), ival(n), dval(n) {}
Json::Json(int64_t n)
: type(JType::Int), ival(n), dval(static_cast<double>(n)) {}
Json::Json(double d)
: type(JType::Float), ival(static_cast<int64_t>(d)), dval(d) {}
Json::Json(const char *s) : type(JType::Str), sval(s ? s : "") {}
Json::Json(std::string s) : type(JType::Str), sval(std::move(s)) {}
Json::Json(std::string_view s) : type(JType::Str), sval(s) {}
Json Json::object() {
Json j;
j.type = JType::Obj;
return j;
}
Json Json::object(
std::initializer_list<std::pair<std::string_view, Json>> init) {
Json j;
j.type = JType::Obj;
for (const auto &kv : init) {
j[kv.first] = kv.second;
}
return j;
}
Json Json::array() {
Json j;
j.type = JType::Arr;
return j;
}
Json Json::array(std::initializer_list<Json> init) {
Json j;
j.type = JType::Arr;
j.aval.assign(init.begin(), init.end());
return j;
}
bool Json::empty() const {
if (type == JType::Arr) {
return aval.empty();
}
if (type == JType::Obj) {
return oval.empty();
}
if (type == JType::Str) {
return sval.empty();
}
return true;
}
size_t Json::size() const {
if (type == JType::Arr) {
return aval.size();
}
if (type == JType::Obj) {
return oval.size();
}
if (type == JType::Str) {
return sval.size();
}
return 0;
}
bool Json::contains(std::string_view k) const {
if (type != JType::Obj) {
return false;
}
for (const auto &kv : oval) {
if (kv.first == k) {
return true;
}
}
return false;
}
void Json::erase(std::string_view k) {
if (type != JType::Obj) {
return;
}
for (auto it = oval.begin(); it != oval.end(); ++it) {
if (it->first == k) {
oval.erase(it);
return;
}
}
}
Json &Json::operator[](std::string_view k) {
if (type != JType::Obj) {
*this = Json::object();
}
for (auto &kv : oval) {
if (kv.first == k) {
return kv.second;
}
}
oval.emplace_back(std::string(k), Json{});
return oval.back().second;
}
const Json &Json::operator[](std::string_view k) const {
if (type != JType::Obj) {
return kNullJson;
}
for (const auto &kv : oval) {
if (kv.first == k) {
return kv.second;
}
}
return kNullJson;
}
Json &Json::at(size_t i) {
return aval[i];
}
const Json &Json::at(size_t i) const {
if (type != JType::Arr || i >= aval.size()) {
return kNullJson;
}
return aval[i];
}
void Json::push_back(Json v) {
if (type != JType::Arr) {
*this = Json::array();
}
aval.push_back(std::move(v));
}
std::string Json::str(std::string_view def) const {
if (type == JType::Str) {
return sval;
}
return std::string(def);
}
bool Json::as_bool(bool def) const {
if (type == JType::Bool) {
return bval;
}
return def;
}
int64_t Json::as_int64(int64_t def) const {
if (type == JType::Int) {
return ival;
}
if (type == JType::Float) {
return static_cast<int64_t>(dval);
}
return def;
}
int Json::as_int(int def) const {
if (type != JType::Int && type != JType::Float) {
return def;
}
int64_t v = (type == JType::Int) ? ival : static_cast<int64_t>(dval);
if (v > INT_MAX) {
return INT_MAX;
}
if (v < INT_MIN) {
return INT_MIN;
}
return static_cast<int>(v);
}
std::string Json::dump(int indent) const {
std::string out;
dump_rec(out, *this, indent, 0);
return out;
}
bool Json::parse(std::string_view src, Json &out) {
JParser p{src, 0};
Json tmp;
if (!p.parse_value(tmp, 0)) {
return false;
}
p.skip_ws();
if (p.pos != src.size()) {
return false;
}
out = std::move(tmp);
return true;
}
Json Json::parse(std::string_view src) {
Json out;
parse(src, out);
return out;
}
+254
View File
@@ -0,0 +1,254 @@
/*
Dynagate LLM Gateway: dynamically change models on the fly (C++20 port)
Created by Luxferre in 2026, released into the public domain
*/
#ifndef _DARWIN_C_SOURCE
#define _DARWIN_C_SOURCE
#endif
#include "dynagate.hpp"
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
namespace {
volatile std::sig_atomic_t g_shutdown_requested = 0;
void handle_signal(int /*sig*/) {
g_shutdown_requested = 1;
}
void print_usage(const char *prog) {
std::fprintf(stderr,
"Dynagate LLM gateway by Luxferre (https://luxferre.top)\n\n"
"Usage of %s:\n"
" -csv string\n"
" \tPath to the CSV configuration file (default \"models.csv\")\n"
" -csv-update-interval int\n"
" \tInterval in minutes with which to run the CSV updater command (default 10)\n"
" -csv-updater string\n"
" \tCommand to run to update the configuration CSV file\n"
" -key string\n"
" \tPath to plaintext file containing expected auth token\n"
" -port int\n"
" \tPort to listen on (default 8080)\n"
" -retry-delay int\n"
" \tBase delay in milliseconds for Fibonacci failover retries (0 to disable) (default 100)\n",
prog);
}
bool parse_int_arg(const std::string &val, int &out) {
if (val.empty()) {
return false;
}
char *endptr = nullptr;
long parsed = std::strtol(val.c_str(), &endptr, 10);
if (endptr == val.c_str() || *endptr != '\0') {
return false;
}
out = static_cast<int>(parsed);
return true;
}
} // namespace
int main(int argc, char **argv) {
int port = 8080;
std::string csv_path = "models.csv";
std::string key_path;
std::string csv_updater;
int csv_update_interval = 10;
int retry_delay_ms = 100;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "-h" || arg == "-help" || arg == "--help") {
print_usage(argv[0]);
return 0;
}
std::string flag_name = arg;
std::string flag_val;
bool has_eq = false;
if (flag_name.rfind("--", 0) == 0) {
flag_name.erase(0, 1);
}
size_t eq_pos = flag_name.find('=');
if (eq_pos != std::string::npos) {
flag_val = flag_name.substr(eq_pos + 1);
flag_name.resize(eq_pos);
has_eq = true;
}
auto next_val = [&](std::string &out) -> bool {
if (has_eq) {
out = flag_val;
return true;
}
if (i + 1 >= argc) {
std::fprintf(stderr, "flag needs an argument: %s\n", arg.c_str());
print_usage(argv[0]);
return false;
}
out = argv[++i];
return true;
};
if (flag_name == "-port") {
std::string v;
if (!next_val(v) || !parse_int_arg(v, port)) {
std::fprintf(stderr, "invalid value for -port\n");
print_usage(argv[0]);
return 2;
}
} else if (flag_name == "-csv") {
if (!next_val(csv_path)) {
return 2;
}
} else if (flag_name == "-key") {
if (!next_val(key_path)) {
return 2;
}
} else if (flag_name == "-csv-updater") {
if (!next_val(csv_updater)) {
return 2;
}
} else if (flag_name == "-csv-update-interval") {
std::string v;
if (!next_val(v) || !parse_int_arg(v, csv_update_interval)) {
std::fprintf(stderr, "invalid value for -csv-update-interval\n");
print_usage(argv[0]);
return 2;
}
} else if (flag_name == "-retry-delay") {
std::string v;
if (!next_val(v) || !parse_int_arg(v, retry_delay_ms)) {
std::fprintf(stderr, "invalid value for -retry-delay\n");
print_usage(argv[0]);
return 2;
}
} else {
std::fprintf(stderr, "flag provided but not defined: %s\n", arg.c_str());
print_usage(argv[0]);
return 2;
}
}
std::signal(SIGPIPE, SIG_IGN);
ensure_curl_initialized();
auto retry_delay = std::chrono::milliseconds(retry_delay_ms);
std::vector<std::string> auth_tokens;
if (!key_path.empty()) {
std::ifstream ifs(key_path, std::ios::binary);
if (!ifs.is_open()) {
log_msg("Failed to read auth token file: %s", key_path.c_str());
return 1;
}
std::ostringstream oss;
oss << ifs.rdbuf();
auto tokens = parse_auth_tokens(oss.str());
if (!tokens.empty()) {
auth_tokens = std::move(tokens);
log_msg(
"Starting Dynagate LLM Gateway. Config: %s, Port: %d, Auth: Required (%zu tokens loaded)",
csv_path.c_str(), port, auth_tokens.size());
} else {
log_msg(
"Starting Dynagate LLM Gateway. Config: %s, Port: %d, Auth: None (empty key file)",
csv_path.c_str(), port);
}
} else {
log_msg("Starting Dynagate LLM Gateway. Config: %s, Port: %d, Auth: None",
csv_path.c_str(), port);
}
// Initialize configuration manager
ConfigManager cm(csv_path);
std::atomic<bool> stop_flag{false};
// Start background config watcher
std::thread watcher_thread([&]() {
cm.watch(stop_flag);
});
// Start background CSV updater if specified
std::thread updater_thread;
if (!csv_updater.empty()) {
int mins = csv_update_interval;
if (mins <= 0) {
log_msg(
"Warning: csv-update-interval must be positive, defaulting to 10 minutes");
mins = 10;
}
auto interval = std::chrono::minutes(mins);
log_msg("Starting background CSV updater: command=\"%s\", interval=%dm0s",
csv_updater.c_str(), mins);
updater_thread = std::thread([&, interval]() {
start_csv_updater(stop_flag, csv_updater, interval);
});
}
GatewayServer server(port, cm, auth_tokens, retry_delay);
std::string listen_err;
if (!server.start(&listen_err)) {
log_msg("Server listen failed: %s", listen_err.c_str());
stop_flag.store(true);
if (watcher_thread.joinable()) {
watcher_thread.join();
}
if (updater_thread.joinable()) {
updater_thread.join();
}
return 1;
}
struct sigaction sa;
std::memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_signal;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
std::thread server_thread([&]() {
log_msg("Server listening on port %d", server.bound_port());
server.serve();
});
while (!g_shutdown_requested) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
log_msg("Shutting down Dynagate server...");
stop_flag.store(true);
server.stop();
if (server_thread.joinable()) {
server_thread.join();
}
if (watcher_thread.joinable()) {
watcher_thread.join();
}
if (updater_thread.joinable()) {
updater_thread.join();
}
log_msg("Server exited cleanly");
return 0;
}
+93
View File
@@ -0,0 +1,93 @@
model,key,endpoint,extra
01-ai/yi-large,-,https://zsnonsky-nim-proxy.hf.space/v1,
adept/fuyu-8b,-,https://zsnonsky-nim-proxy.hf.space/v1,
ai21labs/jamba-1.5-large-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
aisingapore/sea-lion-7b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
bigcode/starcoder2-15b,-,https://zsnonsky-nim-proxy.hf.space/v1,
big-pickle,-,https://opencode.ai/zen/v1,"{""User-Agent"":""opencode/1.18.30""}"
codestral-latest,-,https://api.llm7.io/v1,
cohere/north-mini-code:free,-,https://api.kilo.ai/api/gateway,
databricks/dbrx-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
deepseek-ai/deepseek-coder-6.7b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
deepseek-ai/deepseek-v4-flash-0731,-,https://garystu-openai-nim-proxy.hf.space/v1,
deepseek-ai/deepseek-v4-flash-0731,-,https://zsnonsky-nim-proxy.hf.space/v1,
dots-studio/dots-3-note-preview:free,-,https://api.kilo.ai/api/gateway,
google/codegemma-1.1-7b,-,https://zsnonsky-nim-proxy.hf.space/v1,
google/codegemma-7b,-,https://zsnonsky-nim-proxy.hf.space/v1,
google/deplot,-,https://zsnonsky-nim-proxy.hf.space/v1,
google/gemma-2b,-,https://zsnonsky-nim-proxy.hf.space/v1,
google/gemma-3-12b-it,-,https://zsnonsky-nim-proxy.hf.space/v1,
google/gemma-3-4b-it,-,https://zsnonsky-nim-proxy.hf.space/v1,
google/recurrentgemma-2b,-,https://zsnonsky-nim-proxy.hf.space/v1,
ibm/granite-3.0-3b-a800m-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
ibm/granite-3.0-8b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
ibm/granite-34b-code-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
ibm/granite-8b-code-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
inclusionai/ling-3.0-flash-fin:free,-,https://api.kilo.ai/api/gateway,
inclusionai/ling-3.0-flash-sante:free,-,https://api.kilo.ai/api/gateway,
inclusionai/ling-3.0-flash-vl:free,-,https://api.kilo.ai/api/gateway,
kilo-auto/free,-,https://api.kilo.ai/api/gateway,
ling-3.0-flash-fin-free,-,https://opencode.ai/zen/v1,"{""User-Agent"":""opencode/1.18.30""}"
liquid/lfm-2.5-2.6b:free,-,https://api.kilo.ai/api/gateway,
Lorbus/Qwen3.6-27B-int4-AutoRound,-,https://hermes.ai.unturf.com/v1,
meta/codellama-70b,-,https://zsnonsky-nim-proxy.hf.space/v1,
meta/llama2-70b,-,https://zsnonsky-nim-proxy.hf.space/v1,
meta/llama-3.2-11b-vision-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
meta/muse-glimmer-30b,-,https://zsnonsky-nim-proxy.hf.space/v1,
microsoft/kosmos-2,-,https://zsnonsky-nim-proxy.hf.space/v1,
microsoft/phi-3.5-moe-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
microsoft/phi-3-vision-128k-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
mimo-v2.5-free,-,https://opencode.ai/zen/v1,"{""User-Agent"":""opencode/1.18.30""}"
minimax-m2.7,-,https://api.llm7.io/v1,
mistralai/codestral-22b-instruct-v0.1,-,https://zsnonsky-nim-proxy.hf.space/v1,
mistralai/mistral-7b-instruct-v0.3,-,https://zsnonsky-nim-proxy.hf.space/v1,
mistralai/mistral-large-2-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
mistralai/mistral-large,-,https://zsnonsky-nim-proxy.hf.space/v1,
mistralai/mistral-nemotron,-,https://zsnonsky-nim-proxy.hf.space/v1,
mistralai/mixtral-8x22b-v0.1,-,https://zsnonsky-nim-proxy.hf.space/v1,
mistral-Nemo-Instruct-2407,-,https://api.llm7.io/v1,
moonshotai/kimi-k2.6,-,https://zsnonsky-nim-proxy.hf.space/v1,
moonshotai/kimi-k3,-,https://zsnonsky-nim-proxy.hf.space/v1,
nex-agi/nex-n2.5-mini:free,-,https://api.kilo.ai/api/gateway,
nex-agi/nex-n2.5-pro:free,-,https://api.kilo.ai/api/gateway,
nvidia/ai-synthetic-video-detector,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/cosmos-reason2-8b,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/ising-calibration-1.5-31b,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/llama-3.1-nemoguard-8b-content-safety,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/llama-3.1-nemoguard-8b-topic-control,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/llama-3.1-nemotron-51b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/llama-3.1-nemotron-70b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/llama-3.1-nemotron-safety-guard-8b-v3,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/llama-3.1-nemotron-ultra-253b-v1,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/llama3-chatqa-1.5-70b,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/mistral-nemo-minitron-8b-8k-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nemotron-3.5-content-safety:free,-,https://api.kilo.ai/api/gateway,
nvidia/nemotron-3.5-content-safety,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free,-,https://api.kilo.ai/api/gateway,
nvidia/nemotron-3-nano-omni-30b-a3b-reasoning,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nemotron-3-super-120b-a12b:free,-,https://api.kilo.ai/api/gateway,
nvidia/nemotron-3-super-120b-a12b,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nemotron-4-340b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nemotron-4-340b-reward,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nemotron-nano-3-30b-a3b,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nemotron-parse-2.0,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nemotron-parse,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/neva-22b,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/nvclip,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/riva-translate-4b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/riva-translate-4b-instruct-v1.1,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/riva-translate-4b-instruct-v2,-,https://zsnonsky-nim-proxy.hf.space/v1,
nvidia/vila,-,https://zsnonsky-nim-proxy.hf.space/v1,
nv-mistralai/mistral-nemo-12b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
openai/gpt-oss-20b,-,https://zsnonsky-nim-proxy.hf.space/v1,
openrouter/free,-,https://api.kilo.ai/api/gateway,
poolside/laguna-s-2.1:free,-,https://api.kilo.ai/api/gateway,
poolside/laguna-xs-2.1:free,-,https://api.kilo.ai/api/gateway,
poolside/laguna-xs-2.1,-,https://zsnonsky-nim-proxy.hf.space/v1,
stepfun/step-3.7-flash:free,-,https://api.kilo.ai/api/gateway,
writer/palmyra-creative-122b,-,https://zsnonsky-nim-proxy.hf.space/v1,
writer/palmyra-fin-70b-32k,-,https://zsnonsky-nim-proxy.hf.space/v1,
writer/palmyra-med-70b-32k,-,https://zsnonsky-nim-proxy.hf.space/v1,
writer/palmyra-med-70b,-,https://zsnonsky-nim-proxy.hf.space/v1,
xortron,-,https://darkc0de-chat.hf.space/v1,
zyphra/zamba2-7b-instruct,-,https://zsnonsky-nim-proxy.hf.space/v1,
1 model key endpoint extra
2 01-ai/yi-large - https://zsnonsky-nim-proxy.hf.space/v1
3 adept/fuyu-8b - https://zsnonsky-nim-proxy.hf.space/v1
4 ai21labs/jamba-1.5-large-instruct - https://zsnonsky-nim-proxy.hf.space/v1
5 aisingapore/sea-lion-7b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
6 bigcode/starcoder2-15b - https://zsnonsky-nim-proxy.hf.space/v1
7 big-pickle - https://opencode.ai/zen/v1 {"User-Agent":"opencode/1.18.30"}
8 codestral-latest - https://api.llm7.io/v1
9 cohere/north-mini-code:free - https://api.kilo.ai/api/gateway
10 databricks/dbrx-instruct - https://zsnonsky-nim-proxy.hf.space/v1
11 deepseek-ai/deepseek-coder-6.7b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
12 deepseek-ai/deepseek-v4-flash-0731 - https://garystu-openai-nim-proxy.hf.space/v1
13 deepseek-ai/deepseek-v4-flash-0731 - https://zsnonsky-nim-proxy.hf.space/v1
14 dots-studio/dots-3-note-preview:free - https://api.kilo.ai/api/gateway
15 google/codegemma-1.1-7b - https://zsnonsky-nim-proxy.hf.space/v1
16 google/codegemma-7b - https://zsnonsky-nim-proxy.hf.space/v1
17 google/deplot - https://zsnonsky-nim-proxy.hf.space/v1
18 google/gemma-2b - https://zsnonsky-nim-proxy.hf.space/v1
19 google/gemma-3-12b-it - https://zsnonsky-nim-proxy.hf.space/v1
20 google/gemma-3-4b-it - https://zsnonsky-nim-proxy.hf.space/v1
21 google/recurrentgemma-2b - https://zsnonsky-nim-proxy.hf.space/v1
22 ibm/granite-3.0-3b-a800m-instruct - https://zsnonsky-nim-proxy.hf.space/v1
23 ibm/granite-3.0-8b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
24 ibm/granite-34b-code-instruct - https://zsnonsky-nim-proxy.hf.space/v1
25 ibm/granite-8b-code-instruct - https://zsnonsky-nim-proxy.hf.space/v1
26 inclusionai/ling-3.0-flash-fin:free - https://api.kilo.ai/api/gateway
27 inclusionai/ling-3.0-flash-sante:free - https://api.kilo.ai/api/gateway
28 inclusionai/ling-3.0-flash-vl:free - https://api.kilo.ai/api/gateway
29 kilo-auto/free - https://api.kilo.ai/api/gateway
30 ling-3.0-flash-fin-free - https://opencode.ai/zen/v1 {"User-Agent":"opencode/1.18.30"}
31 liquid/lfm-2.5-2.6b:free - https://api.kilo.ai/api/gateway
32 Lorbus/Qwen3.6-27B-int4-AutoRound - https://hermes.ai.unturf.com/v1
33 meta/codellama-70b - https://zsnonsky-nim-proxy.hf.space/v1
34 meta/llama2-70b - https://zsnonsky-nim-proxy.hf.space/v1
35 meta/llama-3.2-11b-vision-instruct - https://zsnonsky-nim-proxy.hf.space/v1
36 meta/muse-glimmer-30b - https://zsnonsky-nim-proxy.hf.space/v1
37 microsoft/kosmos-2 - https://zsnonsky-nim-proxy.hf.space/v1
38 microsoft/phi-3.5-moe-instruct - https://zsnonsky-nim-proxy.hf.space/v1
39 microsoft/phi-3-vision-128k-instruct - https://zsnonsky-nim-proxy.hf.space/v1
40 mimo-v2.5-free - https://opencode.ai/zen/v1 {"User-Agent":"opencode/1.18.30"}
41 minimax-m2.7 - https://api.llm7.io/v1
42 mistralai/codestral-22b-instruct-v0.1 - https://zsnonsky-nim-proxy.hf.space/v1
43 mistralai/mistral-7b-instruct-v0.3 - https://zsnonsky-nim-proxy.hf.space/v1
44 mistralai/mistral-large-2-instruct - https://zsnonsky-nim-proxy.hf.space/v1
45 mistralai/mistral-large - https://zsnonsky-nim-proxy.hf.space/v1
46 mistralai/mistral-nemotron - https://zsnonsky-nim-proxy.hf.space/v1
47 mistralai/mixtral-8x22b-v0.1 - https://zsnonsky-nim-proxy.hf.space/v1
48 mistral-Nemo-Instruct-2407 - https://api.llm7.io/v1
49 moonshotai/kimi-k2.6 - https://zsnonsky-nim-proxy.hf.space/v1
50 moonshotai/kimi-k3 - https://zsnonsky-nim-proxy.hf.space/v1
51 nex-agi/nex-n2.5-mini:free - https://api.kilo.ai/api/gateway
52 nex-agi/nex-n2.5-pro:free - https://api.kilo.ai/api/gateway
53 nvidia/ai-synthetic-video-detector - https://zsnonsky-nim-proxy.hf.space/v1
54 nvidia/cosmos-reason2-8b - https://zsnonsky-nim-proxy.hf.space/v1
55 nvidia/ising-calibration-1.5-31b - https://zsnonsky-nim-proxy.hf.space/v1
56 nvidia/llama-3.1-nemoguard-8b-content-safety - https://zsnonsky-nim-proxy.hf.space/v1
57 nvidia/llama-3.1-nemoguard-8b-topic-control - https://zsnonsky-nim-proxy.hf.space/v1
58 nvidia/llama-3.1-nemotron-51b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
59 nvidia/llama-3.1-nemotron-70b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
60 nvidia/llama-3.1-nemotron-safety-guard-8b-v3 - https://zsnonsky-nim-proxy.hf.space/v1
61 nvidia/llama-3.1-nemotron-ultra-253b-v1 - https://zsnonsky-nim-proxy.hf.space/v1
62 nvidia/llama3-chatqa-1.5-70b - https://zsnonsky-nim-proxy.hf.space/v1
63 nvidia/mistral-nemo-minitron-8b-8k-instruct - https://zsnonsky-nim-proxy.hf.space/v1
64 nvidia/nemotron-3.5-content-safety:free - https://api.kilo.ai/api/gateway
65 nvidia/nemotron-3.5-content-safety - https://zsnonsky-nim-proxy.hf.space/v1
66 nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free - https://api.kilo.ai/api/gateway
67 nvidia/nemotron-3-nano-omni-30b-a3b-reasoning - https://zsnonsky-nim-proxy.hf.space/v1
68 nvidia/nemotron-3-super-120b-a12b:free - https://api.kilo.ai/api/gateway
69 nvidia/nemotron-3-super-120b-a12b - https://zsnonsky-nim-proxy.hf.space/v1
70 nvidia/nemotron-4-340b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
71 nvidia/nemotron-4-340b-reward - https://zsnonsky-nim-proxy.hf.space/v1
72 nvidia/nemotron-nano-3-30b-a3b - https://zsnonsky-nim-proxy.hf.space/v1
73 nvidia/nemotron-parse-2.0 - https://zsnonsky-nim-proxy.hf.space/v1
74 nvidia/nemotron-parse - https://zsnonsky-nim-proxy.hf.space/v1
75 nvidia/neva-22b - https://zsnonsky-nim-proxy.hf.space/v1
76 nvidia/nvclip - https://zsnonsky-nim-proxy.hf.space/v1
77 nvidia/riva-translate-4b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
78 nvidia/riva-translate-4b-instruct-v1.1 - https://zsnonsky-nim-proxy.hf.space/v1
79 nvidia/riva-translate-4b-instruct-v2 - https://zsnonsky-nim-proxy.hf.space/v1
80 nvidia/vila - https://zsnonsky-nim-proxy.hf.space/v1
81 nv-mistralai/mistral-nemo-12b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
82 openai/gpt-oss-20b - https://zsnonsky-nim-proxy.hf.space/v1
83 openrouter/free - https://api.kilo.ai/api/gateway
84 poolside/laguna-s-2.1:free - https://api.kilo.ai/api/gateway
85 poolside/laguna-xs-2.1:free - https://api.kilo.ai/api/gateway
86 poolside/laguna-xs-2.1 - https://zsnonsky-nim-proxy.hf.space/v1
87 stepfun/step-3.7-flash:free - https://api.kilo.ai/api/gateway
88 writer/palmyra-creative-122b - https://zsnonsky-nim-proxy.hf.space/v1
89 writer/palmyra-fin-70b-32k - https://zsnonsky-nim-proxy.hf.space/v1
90 writer/palmyra-med-70b-32k - https://zsnonsky-nim-proxy.hf.space/v1
91 writer/palmyra-med-70b - https://zsnonsky-nim-proxy.hf.space/v1
92 xortron - https://darkc0de-chat.hf.space/v1
93 zyphra/zamba2-7b-instruct - https://zsnonsky-nim-proxy.hf.space/v1
+624
View File
@@ -0,0 +1,624 @@
/*
Dynagate HTTP/1.1 server implementation (C++20 port)
Created by Luxferre in 2026, released into the public domain
*/
#ifndef _DARWIN_C_SOURCE
#define _DARWIN_C_SOURCE
#endif
#include "dynagate.hpp"
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
namespace {
constexpr size_t kMaxHeaderBytes = 65536;
constexpr size_t kMaxBodyBytes = 64 * 1024 * 1024; // 64 MB
bool write_all(int fd, const char *data, size_t len) {
size_t written = 0;
while (written < len) {
ssize_t n = write(fd, data + written, len - written);
if (n > 0) {
written += static_cast<size_t>(n);
} else if (n < 0 && errno == EINTR) {
continue;
} else {
return false;
}
}
return true;
}
const char *status_reason_phrase(int code) {
switch (code) {
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 204:
return "No Content";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 408:
return "Request Timeout";
case 413:
return "Payload Too Large";
case 429:
return "Too Many Requests";
case 499:
return "Client Closed Request";
case 500:
return "Internal Server Error";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
default:
return "Status";
}
}
class SocketResponseWriter : public ResponseWriter {
public:
explicit SocketResponseWriter(int fd) : fd_(fd) {}
HeaderList &header() override { return headers_; }
void write_header(int status_code) override {
if (!header_written_) {
status_code_ = status_code;
header_written_ = true;
}
}
bool write(std::string_view data) override {
if (!header_written_) {
write_header(200);
}
write_called_ = true;
if (headers_sent_to_socket_) {
if (data.empty()) {
return !write_error_;
}
if (!write_all(fd_, data.data(), data.size())) {
write_error_ = true;
return false;
}
return true;
}
pending_body_.append(data.data(), data.size());
return true;
}
void flush() override {
if (write_error_) {
return;
}
if (!header_written_) {
write_header(200);
}
if (!headers_sent_to_socket_) {
headers_sent_to_socket_ = true;
std::string raw;
raw.reserve(512 + pending_body_.size());
raw.append("HTTP/1.1 ");
raw.append(std::to_string(status_code_));
raw.push_back(' ');
raw.append(status_reason_phrase(status_code_));
raw.append("\r\n");
for (const auto &kv : headers_.items) {
raw.append(kv.first);
raw.append(": ");
raw.append(kv.second);
raw.append("\r\n");
}
if (write_called_) {
if (headers_.get("Content-Length").empty()) {
raw.append("Content-Length: ");
raw.append(std::to_string(pending_body_.size()));
raw.append("\r\n");
}
if (headers_.get("Connection").empty()) {
raw.append("Connection: close\r\n");
}
} else {
if (headers_.get("Connection").empty()) {
raw.append("Connection: close\r\n");
}
}
raw.append("\r\n");
if (!pending_body_.empty()) {
raw.append(pending_body_);
pending_body_.clear();
}
if (!write_all(fd_, raw.data(), raw.size())) {
write_error_ = true;
}
}
}
private:
int fd_;
int status_code_ = 200;
bool header_written_ = false;
bool write_called_ = false;
bool headers_sent_to_socket_ = false;
bool write_error_ = false;
HeaderList headers_;
std::string pending_body_;
};
bool is_socket_disconnected(int fd) {
if (fd < 0) {
return true;
}
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN | POLLHUP | POLLERR;
pfd.revents = 0;
int rc = poll(&pfd, 1, 0);
if (rc < 0) {
return errno != EINTR;
}
if (rc == 0) {
return false;
}
if (pfd.revents & (POLLHUP | POLLERR | POLLNVAL)) {
return true;
}
if (pfd.revents & POLLIN) {
char ch = 0;
ssize_t n = recv(fd, &ch, 1, MSG_PEEK);
if (n == 0) {
return true;
}
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
return true;
}
}
return false;
}
bool read_chunked_body(int fd,
std::string &buffer,
size_t initial_offset,
std::string &out_body) {
size_t pos = initial_offset;
auto ensure_bytes = [&](size_t needed_end) -> bool {
char tmp[4096];
while (buffer.size() < needed_end) {
ssize_t n = read(fd, tmp, sizeof(tmp));
if (n > 0) {
buffer.append(tmp, static_cast<size_t>(n));
if (buffer.size() > kMaxBodyBytes + kMaxHeaderBytes) {
return false;
}
} else if (n < 0 && errno == EINTR) {
continue;
} else {
return false;
}
}
return true;
};
auto ensure_crlf = [&](size_t from, size_t &crlf_pos) -> bool {
char tmp[4096];
for (;;) {
crlf_pos = buffer.find("\r\n", from);
if (crlf_pos != std::string::npos) {
return true;
}
ssize_t n = read(fd, tmp, sizeof(tmp));
if (n > 0) {
buffer.append(tmp, static_cast<size_t>(n));
if (buffer.size() > kMaxBodyBytes + kMaxHeaderBytes) {
return false;
}
} else if (n < 0 && errno == EINTR) {
continue;
} else {
return false;
}
}
};
for (;;) {
size_t line_end = 0;
if (!ensure_crlf(pos, line_end)) {
return false;
}
std::string size_str = str_trim(
std::string_view(buffer).substr(pos, line_end - pos));
size_t semi = size_str.find(';');
if (semi != std::string::npos) {
size_str.resize(semi);
}
if (size_str.empty()) {
return false;
}
char *endptr = nullptr;
unsigned long chunk_len = std::strtoul(size_str.c_str(), &endptr, 16);
if (endptr == size_str.c_str() || chunk_len > kMaxBodyBytes) {
return false;
}
pos = line_end + 2;
if (chunk_len == 0) {
size_t trailer_end = 0;
if (!ensure_crlf(pos, trailer_end)) {
return false;
}
return true;
}
if (out_body.size() + chunk_len > kMaxBodyBytes) {
return false;
}
if (!ensure_bytes(pos + chunk_len + 2)) {
return false;
}
out_body.append(buffer.data() + pos, chunk_len);
pos += chunk_len;
if (buffer.substr(pos, 2) != "\r\n") {
return false;
}
pos += 2;
}
}
bool read_http_request(int fd, HttpRequest &req, int &err_status) {
err_status = 400;
std::string buffer;
buffer.reserve(4096);
char tmp[4096];
size_t header_end = std::string::npos;
for (;;) {
header_end = buffer.find("\r\n\r\n");
if (header_end != std::string::npos) {
break;
}
if (buffer.size() >= kMaxHeaderBytes) {
return false;
}
ssize_t n = read(fd, tmp, sizeof(tmp));
if (n > 0) {
buffer.append(tmp, static_cast<size_t>(n));
} else if (n < 0 && errno == EINTR) {
continue;
} else {
return false;
}
}
std::string_view header_block(buffer.data(), header_end);
size_t first_line_end = header_block.find("\r\n");
std::string_view request_line =
(first_line_end == std::string_view::npos)
? header_block
: header_block.substr(0, first_line_end);
size_t sp1 = request_line.find(' ');
if (sp1 == std::string_view::npos) {
return false;
}
req.method = std::string(request_line.substr(0, sp1));
std::string_view rest = request_line.substr(sp1 + 1);
size_t sp2 = rest.find(' ');
std::string_view raw_target =
(sp2 == std::string_view::npos) ? rest : rest.substr(0, sp2);
size_t qpos = raw_target.find('?');
req.path = std::string(
(qpos == std::string_view::npos) ? raw_target : raw_target.substr(0, qpos));
size_t pos = (first_line_end == std::string_view::npos)
? header_block.size()
: first_line_end + 2;
long content_length = 0;
bool is_chunked = false;
bool expect_continue = false;
while (pos < header_block.size()) {
size_t le = header_block.find("\r\n", pos);
std::string_view line =
(le == std::string_view::npos)
? header_block.substr(pos)
: header_block.substr(pos, le - pos);
pos = (le == std::string_view::npos) ? header_block.size() : le + 2;
if (line.empty()) {
continue;
}
size_t colon = line.find(':');
if (colon == std::string_view::npos) {
continue;
}
std::string key = str_trim(line.substr(0, colon));
std::string val = str_trim(line.substr(colon + 1));
if (key.empty()) {
continue;
}
if (str_iequals(key, "Content-Length")) {
content_length = std::atol(val.c_str());
} else if (str_iequals(key, "Transfer-Encoding")) {
if (str_to_lower(val).find("chunked") != std::string::npos) {
is_chunked = true;
}
} else if (str_iequals(key, "Expect")) {
if (str_iequals(val, "100-continue")) {
expect_continue = true;
}
}
req.headers.add(key, val);
}
if (expect_continue) {
const char *cont = "HTTP/1.1 100 Continue\r\n\r\n";
write_all(fd, cont, std::strlen(cont));
}
size_t body_offset = header_end + 4;
if (is_chunked) {
if (!read_chunked_body(fd, buffer, body_offset, req.body)) {
return false;
}
return true;
}
if (content_length < 0) {
return false;
}
if (static_cast<size_t>(content_length) > kMaxBodyBytes) {
err_status = 413;
return false;
}
size_t needed = static_cast<size_t>(content_length);
size_t avail = buffer.size() - body_offset;
req.body.reserve(needed);
size_t to_copy = std::min(avail, needed);
if (to_copy > 0) {
req.body.append(buffer.data() + body_offset, to_copy);
}
while (req.body.size() < needed) {
size_t rem = needed - req.body.size();
ssize_t n = read(fd, tmp, std::min(sizeof(tmp), rem));
if (n > 0) {
req.body.append(tmp, static_cast<size_t>(n));
} else if (n < 0 && errno == EINTR) {
continue;
} else {
return false;
}
}
return true;
}
} // namespace
GatewayServer::GatewayServer(int port,
ConfigManager &cm,
std::vector<std::string> auth_tokens,
std::chrono::milliseconds retry_delay)
: requested_port_(port),
cm_(cm),
auth_tokens_(std::move(auth_tokens)),
retry_delay_(retry_delay) {}
GatewayServer::~GatewayServer() {
stop();
}
bool GatewayServer::start(std::string *err_out) {
ensure_curl_initialized();
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
if (err_out) {
*err_out = "socket() failed: " + std::string(std::strerror(errno));
}
return false;
}
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
#ifdef SO_NOSIGPIPE
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
#endif
struct sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(static_cast<uint16_t>(requested_port_));
if (bind(fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) < 0) {
if (err_out) {
*err_out = "bind() failed: " + std::string(std::strerror(errno));
}
close(fd);
return false;
}
if (listen(fd, 128) < 0) {
if (err_out) {
*err_out = "listen() failed: " + std::string(std::strerror(errno));
}
close(fd);
return false;
}
struct sockaddr_in bound_addr;
socklen_t addr_len = sizeof(bound_addr);
std::memset(&bound_addr, 0, sizeof(bound_addr));
if (getsockname(fd, reinterpret_cast<struct sockaddr *>(&bound_addr),
&addr_len) == 0) {
bound_port_ = ntohs(bound_addr.sin_port);
} else {
bound_port_ = requested_port_;
}
listen_fd_ = fd;
stopping_.store(false);
return true;
}
void GatewayServer::serve() {
while (!stopping_.load()) {
int lfd = listen_fd_;
if (lfd < 0) {
break;
}
struct pollfd pfd;
pfd.fd = lfd;
pfd.events = POLLIN;
pfd.revents = 0;
int prc = poll(&pfd, 1, 100);
if (prc < 0) {
if (errno == EINTR) {
continue;
}
break;
}
if (prc == 0 || !(pfd.revents & POLLIN)) {
continue;
}
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int cfd = accept(lfd, reinterpret_cast<struct sockaddr *>(&client_addr),
&client_len);
if (cfd < 0) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
if (stopping_.load()) {
break;
}
continue;
}
#ifdef SO_NOSIGPIPE
int one = 1;
setsockopt(cfd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
#endif
{
std::unique_lock<std::mutex> lock(active_mu_);
active_cv_.wait(lock, [&]() {
return stopping_.load() ||
active_connections_ < kMaxConcurrentConnections;
});
if (stopping_.load()) {
close(cfd);
break;
}
++active_connections_;
}
std::thread([this, cfd]() {
handle_client(cfd);
close(cfd);
{
std::lock_guard<std::mutex> lock(active_mu_);
--active_connections_;
}
active_cv_.notify_all();
}).detach();
}
}
void GatewayServer::stop() {
bool expected = false;
if (!stopping_.compare_exchange_strong(expected, true)) {
return;
}
if (listen_fd_ >= 0) {
shutdown(listen_fd_, SHUT_RDWR);
close(listen_fd_);
listen_fd_ = -1;
}
active_cv_.notify_all();
std::unique_lock<std::mutex> lock(active_mu_);
active_cv_.wait_for(lock, std::chrono::seconds(10), [&]() {
return active_connections_ == 0;
});
}
void GatewayServer::handle_client(int client_fd) {
HttpRequest req;
req.client_fd = client_fd;
req.is_cancelled = [this, client_fd]() {
return stopping_.load() || is_socket_disconnected(client_fd);
};
SocketResponseWriter w(client_fd);
int err_status = 400;
if (!read_http_request(client_fd, req, err_status)) {
if (err_status == 413) {
w.header().set("Content-Type", "text/plain; charset=utf-8");
w.write_header(413);
w.write("Payload Too Large\n");
w.flush();
}
return;
}
if (req.path == "/v1/models" || req.path == "/models") {
handle_models(cm_, auth_tokens_, req, w);
} else if (req.path == "/v1/chat/completions" ||
req.path == "/chat/completions") {
handle_chat_completions(cm_, auth_tokens_, retry_delay_, req, w);
} else if (req.path == "/v1/images/generations" ||
req.path == "/images/generations") {
handle_image_generations(cm_, auth_tokens_, retry_delay_, req, w);
} else if (req.path == "/v1/messages" || req.path == "/messages") {
handle_messages(cm_, auth_tokens_, retry_delay_, req, w);
} else if (req.path == "/v1/responses" || req.path == "/responses") {
handle_responses(cm_, auth_tokens_, retry_delay_, req, w);
} else {
w.header().set("Content-Type", "text/plain; charset=utf-8");
w.write_header(404);
w.write("404 page not found\n");
w.flush();
}
}