/* Dynagate C++20 test suite (port of gateway_test.go) Created by Luxferre in 2026, released into the public domain */ #ifndef _DARWIN_C_SOURCE #define _DARWIN_C_SOURCE #endif #include "dynagate.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { int g_failures = 0; void fail_test(const char *test_name, int line, const std::string &msg) { std::fprintf(stderr, " [FAIL] %s (line %d): %s\n", test_name, line, msg.c_str()); ++g_failures; } #define EXPECT_TRUE(cond, msg) \ do { \ if (!(cond)) { \ fail_test(__func__, __LINE__, (msg)); \ return; \ } \ } while (0) #define EXPECT_EQ(actual, expected, msg) \ do { \ if (!((actual) == (expected))) { \ std::ostringstream _oss; \ _oss << (msg); \ fail_test(__func__, __LINE__, _oss.str()); \ return; \ } \ } while (0) bool write_all_fd(int fd, std::string_view data) { size_t written = 0; while (written < data.size()) { ssize_t n = write(fd, data.data() + written, data.size() - written); if (n > 0) { written += static_cast(n); } else if (n < 0 && errno == EINTR) { continue; } else { return false; } } return true; } class MockHttpServer { public: using HandlerFunc = std::function; explicit MockHttpServer(HandlerFunc handler) : handler_(std::move(handler)) { fd_ = socket(AF_INET, SOCK_STREAM, 0); 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_LOOPBACK); addr.sin_port = htons(0); if (bind(fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { std::perror("MockHttpServer bind"); std::exit(1); } if (listen(fd_, 64) < 0) { std::perror("MockHttpServer listen"); std::exit(1); } struct sockaddr_in bound; socklen_t len = sizeof(bound); getsockname(fd_, reinterpret_cast(&bound), &len); port_ = ntohs(bound.sin_port); url_ = "http://127.0.0.1:" + std::to_string(port_); thread_ = std::thread([this]() { serve_loop(); }); } ~MockHttpServer() { stop_.store(true); if (fd_ >= 0) { shutdown(fd_, SHUT_RDWR); close(fd_); fd_ = -1; } if (thread_.joinable()) { thread_.join(); } } const std::string &url() const { return url_; } static void send_response(int fd, int status_code, std::string_view content_type, std::string_view body) { std::string resp = "HTTP/1.1 " + std::to_string(status_code) + " Status\r\n"; if (!content_type.empty()) { resp += "Content-Type: " + std::string(content_type) + "\r\n"; } resp += "Content-Length: " + std::to_string(body.size()) + "\r\n"; resp += "Connection: close\r\n\r\n"; resp.append(body.data(), body.size()); write_all_fd(fd, resp); } private: void serve_loop() { while (!stop_.load()) { struct pollfd pfd; pfd.fd = fd_; pfd.events = POLLIN; pfd.revents = 0; int prc = poll(&pfd, 1, 50); if (prc <= 0 || !(pfd.revents & POLLIN)) { continue; } struct sockaddr_in caddr; socklen_t clen = sizeof(caddr); int cfd = accept(fd_, reinterpret_cast(&caddr), &clen); if (cfd < 0) { continue; } HttpRequest req; if (read_req(cfd, req)) { handler_(req, cfd); } close(cfd); } } static bool read_req(int fd, HttpRequest &req) { std::string buf; char tmp[4096]; size_t hend = std::string::npos; for (;;) { hend = buf.find("\r\n\r\n"); if (hend != std::string::npos) { break; } ssize_t n = read(fd, tmp, sizeof(tmp)); if (n <= 0) { return false; } buf.append(tmp, static_cast(n)); } std::string_view hblock(buf.data(), hend); size_t fl = hblock.find("\r\n"); std::string_view rline = (fl == std::string_view::npos) ? hblock : hblock.substr(0, fl); size_t sp1 = rline.find(' '); if (sp1 == std::string_view::npos) { return false; } req.method = std::string(rline.substr(0, sp1)); std::string_view rest = rline.substr(sp1 + 1); size_t sp2 = rest.find(' '); req.path = std::string( (sp2 == std::string_view::npos) ? rest : rest.substr(0, sp2)); size_t pos = (fl == std::string_view::npos) ? hblock.size() : fl + 2; long clen = 0; while (pos < hblock.size()) { size_t le = hblock.find("\r\n", pos); std::string_view line = (le == std::string_view::npos) ? hblock.substr(pos) : hblock.substr(pos, le - pos); pos = (le == std::string_view::npos) ? hblock.size() : le + 2; size_t colon = line.find(':'); if (colon != std::string_view::npos) { std::string k = str_trim(line.substr(0, colon)); std::string v = str_trim(line.substr(colon + 1)); if (str_iequals(k, "Content-Length")) { clen = std::atol(v.c_str()); } req.headers.add(k, v); } } size_t boff = hend + 4; size_t avail = buf.size() - boff; size_t needed = clen > 0 ? static_cast(clen) : 0; if (avail > 0) { req.body.append(buf.data() + boff, std::min(avail, needed)); } while (req.body.size() < needed) { ssize_t n = read(fd, tmp, std::min(sizeof(tmp), needed - req.body.size())); if (n <= 0) { break; } req.body.append(tmp, static_cast(n)); } return true; } int fd_ = -1; int port_ = 0; std::string url_; std::atomic stop_{false}; HandlerFunc handler_; std::thread thread_; }; std::filesystem::path make_temp_dir(std::string_view prefix) { auto base = std::filesystem::temp_directory_path(); auto now = std::chrono::steady_clock::now().time_since_epoch().count(); auto path = base / (std::string(prefix) + "-" + std::to_string(getpid()) + "-" + std::to_string(now)); std::filesystem::create_directories(path); return path; } struct TempDirGuard { std::filesystem::path path; explicit TempDirGuard(std::string_view prefix) : path(make_temp_dir(prefix)) {} ~TempDirGuard() { std::error_code ec; std::filesystem::remove_all(path, ec); } }; void TestURLBuilding() { struct TestCase { std::string endpoint; std::string req_path; std::string expected; }; std::vector tests = { {"https://api.openai.com/v1", "/v1/chat/completions", "https://api.openai.com/v1/chat/completions"}, {"https://api.openai.com/v1/", "/v1/chat/completions", "https://api.openai.com/v1/chat/completions"}, {"https://api.openai.com", "/v1/chat/completions", "https://api.openai.com/v1/chat/completions"}, {"http://localhost:8000/api/v1", "/v1/chat/completions", "http://localhost:8000/api/v1/chat/completions"}, {"http://localhost:11434", "/v1/chat/completions", "http://localhost:11434/v1/chat/completions"}, {"https://custom.endpoint/v1/custom", "/v1/chat/completions", "https://custom.endpoint/v1/custom/v1/chat/completions"}, }; for (const auto &tc : tests) { std::string got = build_url(tc.endpoint, tc.req_path); EXPECT_EQ(got, tc.expected, "build_url(" + tc.endpoint + ", " + tc.req_path + ") = " + got + "; want " + tc.expected); } } void TestTrialOrdering() { std::vector configs = { {"gpt-4", "key-a1", "ep-a", ""}, {"gpt-4", "key-a2", "ep-a", ""}, {"gpt-3.5", "key-b1", "ep-b", ""}, {"claude", "key-c1", "ep-c", ""}, }; std::vector unique_models = {"gpt-4", "gpt-3.5", "claude"}; // Case 1: "gpt-4" { auto trials = get_trial_configs(configs, unique_models, "gpt-4"); std::vector expected = { {"gpt-4", "key-a1", "ep-a", ""}, {"gpt-4", "key-a2", "ep-a", ""}, {"gpt-3.5", "key-b1", "ep-b", ""}, {"claude", "key-c1", "ep-c", ""}, }; EXPECT_EQ(trials, expected, "Trial ordering for gpt-4 mismatch"); } // Case 2: "gpt-3.5" { auto trials = get_trial_configs(configs, unique_models, "gpt-3.5"); std::vector expected = { {"gpt-3.5", "key-b1", "ep-b", ""}, {"claude", "key-c1", "ep-c", ""}, {"gpt-4", "key-a1", "ep-a", ""}, {"gpt-4", "key-a2", "ep-a", ""}, }; EXPECT_EQ(trials, expected, "Trial ordering for gpt-3.5 mismatch"); } // Case 3: "claude" { auto trials = get_trial_configs(configs, unique_models, "claude"); std::vector expected = { {"claude", "key-c1", "ep-c", ""}, {"gpt-4", "key-a1", "ep-a", ""}, {"gpt-4", "key-a2", "ep-a", ""}, {"gpt-3.5", "key-b1", "ep-b", ""}, }; EXPECT_EQ(trials, expected, "Trial ordering for claude mismatch"); } // Case 4: "non-existent" { auto trials = get_trial_configs(configs, unique_models, "non-existent"); std::vector expected = { {"gpt-4", "key-a1", "ep-a", ""}, {"gpt-4", "key-a2", "ep-a", ""}, {"gpt-3.5", "key-b1", "ep-b", ""}, {"claude", "key-c1", "ep-c", ""}, }; EXPECT_EQ(trials, expected, "Trial ordering for non-existent mismatch"); } } void TestConfigManagerCSVWatcher() { TempDirGuard tmp("dynagate-test"); auto csv_file = (tmp.path / "models.csv").string(); std::string initial_content = "model,key,endpoint,extra\n" "gpt-4,key-1,http://localhost:8001,\"{\"\"User-Agent\"\":\"\"test/1.0\"\"}\"\n" "gpt-3.5,key-2,http://localhost:8002,\n"; { std::ofstream ofs(csv_file, std::ios::binary); ofs << initial_content; } ConfigManager cm(csv_file); auto configs = cm.get_configs(); EXPECT_EQ(configs.size(), 2u, "Expected 2 initial configs"); EXPECT_EQ(configs[0].model, "gpt-4", "Unexpected model 0"); EXPECT_EQ(configs[0].extra, "{\"User-Agent\":\"test/1.0\"}", "Unexpected quoted extra JSON"); EXPECT_EQ(configs[1].model, "gpt-3.5", "Unexpected model 1"); std::atomic stop_flag{false}; std::thread watcher([&]() { cm.watch(stop_flag); }); std::this_thread::sleep_for(std::chrono::milliseconds(1100)); std::string updated_content = "model,key,endpoint\n" "gpt-4,key-1,http://localhost:8001\n" "gpt-3.5,key-2,http://localhost:8002\n" "claude,key-3,http://localhost:8003\n"; { std::ofstream ofs(csv_file, std::ios::binary | std::ios::trunc); ofs << updated_content; } auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); std::vector updated_configs; while (std::chrono::steady_clock::now() < deadline) { updated_configs = cm.get_configs(); if (updated_configs.size() == 3) { break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } stop_flag.store(true); if (watcher.joinable()) { watcher.join(); } EXPECT_EQ(updated_configs.size(), 3u, "Hot-reload failed to load 3 configs"); EXPECT_EQ(updated_configs[2].model, "claude", "Hot-reload third model mismatch"); } void TestGatewayCompletionsFailover() { MockHttpServer server_a([](const HttpRequest &, int fd) { MockHttpServer::send_response( fd, 500, "application/json", R"({"error": "Internal Server Error A"})"); }); MockHttpServer server_b([](const HttpRequest &, int fd) { MockHttpServer::send_response( fd, 401, "application/json", R"({"error": "Unauthorized B"})"); }); MockHttpServer server_c([](const HttpRequest &req, int fd) { Json body = Json::parse(req.body); std::string model_sent = body["model"].str(); std::string resp_body = R"({"choices":[{"message":{"content":"Hello from )" + model_sent + R"("}}]})"; MockHttpServer::send_response(fd, 200, "application/json", resp_body); }); std::vector configs = { {"model-1", "key-fail-1", server_a.url(), ""}, {"model-1", "key-fail-2", server_b.url(), ""}, {"model-2", "key-success", server_c.url(), ""}, }; ConfigManager cm(configs, {"model-1", "model-2"}); Json req_body_obj = Json::object({ {"model", "model-1"}, {"messages", Json::array({ Json::object({{"role", "user"}, {"content", "Hi"}}), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.body = req_body_obj.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(10), req, w); EXPECT_EQ(w.status_code(), 200, "Expected status 200 after failover"); Json resp_obj = Json::parse(w.body()); EXPECT_TRUE(resp_obj.contains("choices") && resp_obj["choices"].is_array() && !resp_obj["choices"].empty(), "Invalid response choices structure"); std::string content = resp_obj["choices"].at(0)["message"]["content"].str(); EXPECT_EQ(content, "Hello from model-2", "Expected content 'Hello from model-2', got: " + content); } void TestGatewayCompletionsStreamingFailover() { MockHttpServer server_a([](const HttpRequest &, int fd) { MockHttpServer::send_response(fd, 500, "text/plain", "Error"); }); MockHttpServer server_b([](const HttpRequest &, int fd) { std::string headers = "HTTP/1.1 200 OK\r\n" "Content-Type: text/event-stream\r\n" "Cache-Control: no-cache\r\n" "Connection: close\r\n\r\n"; write_all_fd(fd, headers); write_all_fd(fd, "data: chunk1\n\n"); write_all_fd(fd, "data: chunk2\n\n"); }); std::vector configs = { {"model-1", "key-fail", server_a.url(), ""}, {"model-2", "key-success", server_b.url(), ""}, }; ConfigManager cm(configs, {"model-1", "model-2"}); Json req_body_obj = Json::object({ {"model", "model-1"}, {"stream", true}, {"messages", Json::array({ Json::object({{"role", "user"}, {"content", "Hi"}}), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.body = req_body_obj.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(10), req, w); EXPECT_EQ(w.status_code(), 200, "Expected status 200 on stream failover"); EXPECT_TRUE(w.body().find("data: chunk1") != std::string::npos && w.body().find("data: chunk2") != std::string::npos, "Expected streaming chunks to be forwarded, got: " + w.body()); } void TestModelsEndpoint() { std::vector configs = { {"model-1", "key-1", "ep-1", ""}, {"model-1", "key-2", "ep-1", ""}, {"model-2", "key-3", "ep-2", ""}, }; ConfigManager cm(configs, {"model-1", "model-2"}); HttpRequest req; req.method = "GET"; req.path = "/v1/models"; RecorderResponseWriter w; handle_models(cm, {}, req, w); EXPECT_EQ(w.status_code(), 200, "Expected status 200 on /v1/models"); Json resp_obj = Json::parse(w.body()); EXPECT_TRUE(resp_obj.contains("data") && resp_obj["data"].is_array() && resp_obj["data"].size() == 2, "Expected 2 models in response data"); EXPECT_EQ(resp_obj["data"].at(0)["id"].str(), "model-1", "Expected first model to be model-1"); EXPECT_EQ(resp_obj["data"].at(1)["id"].str(), "model-2", "Expected second model to be model-2"); } void TestGatewayAuthentication() { std::vector configs = { {"model-1", "key-1", "http://127.0.0.1:1", ""}, }; ConfigManager cm(configs, {"model-1"}); std::vector expected_tokens = { "secret-token-1", "secret-token-2", }; // Case 1: No Authorization header { HttpRequest req; req.method = "GET"; req.path = "/v1/models"; RecorderResponseWriter w; handle_models(cm, expected_tokens, req, w); EXPECT_EQ(w.status_code(), 401, "Expected 401 Unauthorized for missing token"); } // Case 2: Incorrect Authorization header { HttpRequest req; req.method = "GET"; req.path = "/v1/models"; req.headers.set("Authorization", "Bearer wrong-token"); RecorderResponseWriter w; handle_models(cm, expected_tokens, req, w); EXPECT_EQ(w.status_code(), 401, "Expected 401 Unauthorized for wrong token"); } // Case 3: Correct Authorization header (first token) { HttpRequest req; req.method = "GET"; req.path = "/v1/models"; req.headers.set("Authorization", "Bearer secret-token-1"); RecorderResponseWriter w; handle_models(cm, expected_tokens, req, w); EXPECT_EQ(w.status_code(), 200, "Expected 200 OK for correct token 1"); } // Case 3b: Correct Authorization header (second token) { HttpRequest req; req.method = "GET"; req.path = "/v1/models"; req.headers.set("Authorization", "Bearer secret-token-2"); RecorderResponseWriter w; handle_models(cm, expected_tokens, req, w); EXPECT_EQ(w.status_code(), 200, "Expected 200 OK for correct token 2"); } // Case 4: Completions endpoint with correct token { HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Authorization", "Bearer secret-token-2"); req.headers.set("Content-Type", "application/json"); req.body = R"({"model":"model-1"})"; RecorderResponseWriter w; handle_chat_completions(cm, expected_tokens, std::chrono::milliseconds(0), req, w); EXPECT_TRUE(w.status_code() != 401, "Expected completions request to pass authentication"); } } void TestCSVUpdater() { TempDirGuard tmp("dynagate-updater-test"); auto csv_file = (tmp.path / "models.csv").string(); { std::ofstream ofs(csv_file, std::ios::binary); ofs << "model,key,endpoint\ngpt-4,key-1,http://localhost:8001\n"; } std::string command = "echo 'gpt-3.5,key-2,http://localhost:8002' >> " + csv_file; std::atomic stop_flag{false}; std::thread updater([&]() { start_csv_updater(stop_flag, command, std::chrono::milliseconds(100)); }); auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); bool contains_new_model = false; while (std::chrono::steady_clock::now() < deadline) { std::ifstream ifs(csv_file); std::ostringstream oss; oss << ifs.rdbuf(); if (oss.str().find("gpt-3.5") != std::string::npos) { contains_new_model = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(50)); } stop_flag.store(true); if (updater.joinable()) { updater.join(); } EXPECT_TRUE(contains_new_model, "Expected CSV file to contain updater-appended model"); } void TestGatewayImageGenerations() { MockHttpServer server([](const HttpRequest &req, int fd) { if (req.path != "/v1/images/generations") { MockHttpServer::send_response(fd, 404, "text/plain", "Not Found"); return; } Json body = Json::parse(req.body); std::string model_sent = body["model"].str(); std::string resp = R"({"created":1589478378,"data":[{"url":"http://image-url/generated-by-)" + model_sent + R"("}]})"; MockHttpServer::send_response(fd, 200, "application/json", resp); }); std::vector configs = { {"dall-e-3", "dalle-key", server.url(), ""}, }; ConfigManager cm(configs, {"dall-e-3"}); Json req_body = Json::object({ {"prompt", "a beautiful kitten"}, {"model", "dall-e-3"}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/images/generations"; req.headers.set("Content-Type", "application/json"); req.body = req_body.dump(); RecorderResponseWriter w; handle_image_generations(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected 200 OK for image generations"); Json resp_obj = Json::parse(w.body()); std::string url_val = resp_obj["data"].at(0)["url"].str(); EXPECT_EQ(url_val, "http://image-url/generated-by-dall-e-3", "Unexpected image URL: " + url_val); } void TestGatewayNoAuthHeaderOnEmptyKey() { std::atomic auth_was_empty{true}; MockHttpServer server([&](const HttpRequest &req, int fd) { if (!req.headers.get("Authorization").empty()) { auth_was_empty.store(false); MockHttpServer::send_response(fd, 400, "text/plain", "Unexpected Auth"); return; } MockHttpServer::send_response( fd, 200, "application/json", R"({"choices":[{"message":{"content":"Hello"}}]})"); }); std::vector configs = { {"model-no-key", "", server.url(), ""}, {"model-no-key-2", "-", server.url(), ""}, }; ConfigManager cm(configs, {"model-no-key", "model-no-key-2"}); // Case 1: Empty key { Json req_body = Json::object({ {"model", "model-no-key"}, {"messages", Json::array({ Json::object({{"role", "user"}, {"content", "Hi"}}), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.headers.set("Authorization", "Bearer some-client-token"); req.body = req_body.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected status 200 for empty key"); EXPECT_TRUE(auth_was_empty.load(), "Expected no Authorization header on upstream request"); } // Case 2: Dash key { Json req_body = Json::object({ {"model", "model-no-key-2"}, {"messages", Json::array({ Json::object({{"role", "user"}, {"content", "Hi"}}), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.headers.set("Authorization", "Bearer some-client-token"); req.body = req_body.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected status 200 for dash key"); EXPECT_TRUE(auth_was_empty.load(), "Expected no Authorization header on upstream request"); } } void TestGatewayBlankKey() { std::string received_auth; std::mutex mu; MockHttpServer server([&](const HttpRequest &req, int fd) { { std::lock_guard lock(mu); received_auth = req.headers.get("Authorization"); } if (received_auth != "Bearer") { MockHttpServer::send_response(fd, 400, "text/plain", "Bad Auth"); return; } MockHttpServer::send_response( fd, 200, "application/json", R"({"choices":[{"message":{"content":"Hello"}}]})"); }); std::vector configs = { {"model-blank-key", "-blank-", server.url(), ""}, }; ConfigManager cm(configs, {"model-blank-key"}); Json req_body = Json::object({ {"model", "model-blank-key"}, {"messages", Json::array({ Json::object({{"role", "user"}, {"content", "Hi"}}), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.headers.set("Authorization", "Bearer some-client-token"); req.body = req_body.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected status 200 for -blank- key"); EXPECT_EQ(received_auth, "Bearer", "Expected Authorization header 'Bearer', got: " + received_auth); } void TestGatewayExtraHeaders() { MockHttpServer server([](const HttpRequest &req, int fd) { if (req.headers.get("X-My-Custom-Header") != "HeaderValue" || req.headers.get("X-Another-Header") != "123") { MockHttpServer::send_response(fd, 400, "text/plain", "Missing headers"); return; } MockHttpServer::send_response( fd, 200, "application/json", R"({"choices":[{"message":{"content":"Hello"}}]})"); }); std::vector configs = { {"model-extra-headers", "my-key", server.url(), R"({"X-My-Custom-Header":"HeaderValue","X-Another-Header":123})"}, }; ConfigManager cm(configs, {"model-extra-headers"}); Json req_body = Json::object({ {"model", "model-extra-headers"}, {"messages", Json::array({ Json::object({{"role", "user"}, {"content", "Hi"}}), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.body = req_body.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected status 200 for extra headers"); } void TestMultipleTokensKeyFile() { std::string key_file_content = "\n token-a \n\ntoken-b\n \ntoken-c\n"; auto auth_tokens = parse_auth_tokens(key_file_content); std::vector expected = {"token-a", "token-b", "token-c"}; EXPECT_EQ(auth_tokens, expected, "Parsed auth tokens mismatch"); HttpRequest req_ok; req_ok.method = "GET"; req_ok.path = "/v1/models"; req_ok.headers.set("Authorization", "Bearer token-b"); EXPECT_TRUE(check_auth(auth_tokens, req_ok), "Expected token-b to authenticate"); HttpRequest req_wrong; req_wrong.method = "GET"; req_wrong.path = "/v1/models"; req_wrong.headers.set("Authorization", "Bearer token-wrong"); EXPECT_TRUE(!check_auth(auth_tokens, req_wrong), "Expected token-wrong to fail authentication"); auto empty_tokens = parse_auth_tokens("\n \n\n \n"); EXPECT_TRUE(empty_tokens.empty(), "Expected empty tokens vector"); HttpRequest req_no_auth; req_no_auth.method = "GET"; req_no_auth.path = "/v1/models"; EXPECT_TRUE(check_auth(empty_tokens, req_no_auth), "Expected check_auth to return true for empty expected_tokens"); } void TestRequestMessagesAutodetectionAndContentArrays() { Json received_body; std::mutex mu; MockHttpServer server([&](const HttpRequest &req, int fd) { { std::lock_guard lock(mu); received_body = Json::parse(req.body); } MockHttpServer::send_response( fd, 200, "application/json", R"({"choices":[{"message":{"content":"OK"}}]})"); }); std::vector configs = { {"test-model", "test-key", server.url(), ""}, }; ConfigManager cm(configs, {"test-model"}); // 1. Pi agent format - array content { Json req_obj = Json::object({ {"model", "test-model"}, {"messages", Json::array({ Json::object({ {"role", "user"}, {"content", Json::array({ Json::object({ {"type", "text"}, {"text", "Hello from Pi agent"}, }), })}, }), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.body = req_obj.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected 200 OK for Pi agent format"); EXPECT_EQ(received_body["messages"].size(), 1u, "Expected 1 message"); EXPECT_EQ(received_body["messages"].at(0)["content"].str(), "Hello from Pi agent", "Unexpected Pi agent normalized content"); } // 2. Top-level system prompt { Json req_obj = Json::object({ {"model", "test-model"}, {"system", "You are a helpful coding assistant"}, {"messages", Json::array({ Json::object({ {"role", "user"}, {"content", "Write a test"}, }), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.body = req_obj.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(received_body["messages"].size(), 2u, "Expected 2 messages"); EXPECT_EQ(received_body["messages"].at(0)["role"].str(), "system", "Expected system role first"); EXPECT_EQ(received_body["messages"].at(0)["content"].str(), "You are a helpful coding assistant", "Unexpected system message content"); EXPECT_EQ(received_body["messages"].at(1)["content"].str(), "Write a test", "Unexpected user message content"); EXPECT_TRUE(!received_body.contains("system"), "Expected top-level system field to be removed"); } // 3. Prompt field autodetection { Json req_obj = Json::object({ {"model", "test-model"}, {"prompt", "Explain recursion"}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.body = req_obj.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(received_body["messages"].size(), 1u, "Expected 1 message"); EXPECT_EQ(received_body["messages"].at(0)["role"].str(), "user", "Expected user role"); EXPECT_EQ(received_body["messages"].at(0)["content"].str(), "Explain recursion", "Unexpected prompt autodetection content"); } // 4. Multimodal array content preserved { Json req_obj = Json::object({ {"model", "test-model"}, {"messages", Json::array({ Json::object({ {"role", "user"}, {"content", Json::array({ Json::object({ {"type", "text"}, {"text", "Describe this image"}, }), Json::object({ {"type", "image_url"}, {"image_url", Json::object({ {"url", "http://example.com/img.png"}, })}, }), })}, }), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/chat/completions"; req.headers.set("Content-Type", "application/json"); req.body = req_obj.dump(); RecorderResponseWriter w; handle_chat_completions(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_TRUE(received_body["messages"].at(0)["content"].is_array() && received_body["messages"].at(0)["content"].size() == 2, "Expected multimodal array content to be preserved"); } } void TestFibonacciBackoff() { std::vector> expected_seq = { {0, 0}, {1, 1}, {2, 1}, {3, 2}, {4, 3}, {5, 5}, {6, 8}, {7, 13}, {8, 21}, {9, 34}, {10, 55}, }; for (const auto &p : expected_seq) { EXPECT_EQ(fibonacci(p.first), p.second, "fibonacci mismatch"); } } void TestAnthropicMessagesEndpoint() { Json received_openai_body; std::mutex mu; MockHttpServer server([&](const HttpRequest &req, int fd) { if (req.path != "/v1/chat/completions") { MockHttpServer::send_response(fd, 404, "text/plain", "Wrong path"); return; } Json body = Json::parse(req.body); { std::lock_guard lock(mu); received_openai_body = body; } bool is_stream = body["stream"].as_bool(false); if (is_stream) { std::string headers = "HTTP/1.1 200 OK\r\n" "Content-Type: text/event-stream\r\n" "Connection: close\r\n\r\n"; write_all_fd(fd, headers); write_all_fd( fd, "data: {\"choices\":[{\"delta\":{\"content\":\"Hello \"}}]}\n\n"); write_all_fd( fd, "data: {\"choices\":[{\"delta\":{\"content\":\"Anthropic!\"},\"finish_reason\":\"stop\"}]}\n\n"); write_all_fd(fd, "data: [DONE]\n\n"); return; } Json resp = Json::object({ {"id", "chatcmpl-test"}, {"model", "claude-model"}, {"choices", Json::array({ Json::object({ {"index", 0}, {"message", Json::object({ {"role", "assistant"}, {"content", "Hello from Anthropic proxy"}, })}, {"finish_reason", "stop"}, }), })}, {"usage", Json::object({ {"prompt_tokens", 10}, {"completion_tokens", 15}, {"total_tokens", 25}, })}, }); MockHttpServer::send_response(fd, 200, "application/json", resp.dump()); }); std::vector configs = { {"claude-model", "test-key", server.url(), ""}, }; ConfigManager cm(configs, {"claude-model"}); // 1. Non-streaming test { Json req_obj = Json::object({ {"model", "claude-model"}, {"system", "You are a helpful bot"}, {"messages", Json::array({ Json::object({ {"role", "user"}, {"content", "Hello"}, }), })}, {"max_tokens", 100}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/messages"; req.headers.set("Content-Type", "application/json"); req.body = req_obj.dump(); RecorderResponseWriter w; handle_messages(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected 200 OK for Anthropic messages"); Json anthropic_resp = Json::parse(w.body()); EXPECT_EQ(anthropic_resp["type"].str(), "message", "Expected type 'message'"); EXPECT_EQ(anthropic_resp["role"].str(), "assistant", "Expected role 'assistant'"); EXPECT_EQ(anthropic_resp["content"].at(0)["text"].str(), "Hello from Anthropic proxy", "Unexpected Anthropic text"); EXPECT_EQ(received_openai_body["messages"].size(), 2u, "Expected 2 messages in OpenAI body"); EXPECT_EQ(received_openai_body["messages"].at(0)["role"].str(), "system", "Expected first message to be system"); } // 2. Streaming test { Json req_obj = Json::object({ {"model", "claude-model"}, {"stream", true}, {"messages", Json::array({ Json::object({ {"role", "user"}, {"content", "Stream me"}, }), })}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/messages"; req.headers.set("Content-Type", "application/json"); req.body = req_obj.dump(); RecorderResponseWriter w; handle_messages(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected 200 OK for streaming Anthropic messages"); EXPECT_TRUE(w.body().find("event: message_start") != std::string::npos && w.body().find("event: content_block_delta") != std::string::npos && w.body().find("event: message_stop") != std::string::npos, "Expected Anthropic SSE stream events in response"); } } void TestOpenAIResponsesEndpoint() { Json received_openai_body; std::mutex mu; MockHttpServer server([&](const HttpRequest &req, int fd) { if (req.path != "/v1/chat/completions") { MockHttpServer::send_response(fd, 404, "text/plain", "Wrong path"); return; } Json body = Json::parse(req.body); { std::lock_guard lock(mu); received_openai_body = body; } bool is_stream = body["stream"].as_bool(false); if (is_stream) { std::string headers = "HTTP/1.1 200 OK\r\n" "Content-Type: text/event-stream\r\n" "Connection: close\r\n\r\n"; write_all_fd(fd, headers); write_all_fd( fd, "data: {\"choices\":[{\"delta\":{\"content\":\"Response \"}}]}\n\n"); write_all_fd( fd, "data: {\"choices\":[{\"delta\":{\"content\":\"Chunk\"},\"finish_reason\":\"stop\"}]}\n\n"); write_all_fd(fd, "data: [DONE]\n\n"); return; } Json resp = Json::object({ {"id", "chatcmpl-resp-test"}, {"model", "gpt-5-model"}, {"choices", Json::array({ Json::object({ {"index", 0}, {"message", Json::object({ {"role", "assistant"}, {"content", "Hello from Responses proxy"}, })}, {"finish_reason", "stop"}, }), })}, {"usage", Json::object({ {"prompt_tokens", 12}, {"completion_tokens", 18}, {"total_tokens", 30}, })}, }); MockHttpServer::send_response(fd, 200, "application/json", resp.dump()); }); std::vector configs = { {"gpt-5-model", "test-key", server.url(), ""}, }; ConfigManager cm(configs, {"gpt-5-model"}); // 1. Non-streaming test { Json req_obj = Json::object({ {"model", "gpt-5-model"}, {"instructions", "Be accurate"}, {"input", "What is 2+2?"}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/responses"; req.headers.set("Content-Type", "application/json"); req.body = req_obj.dump(); RecorderResponseWriter w; handle_responses(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected 200 OK for OpenAI Responses"); Json resp_obj = Json::parse(w.body()); EXPECT_EQ(resp_obj["object"].str(), "response", "Expected object 'response'"); EXPECT_EQ(resp_obj["status"].str(), "completed", "Expected status 'completed'"); EXPECT_EQ( resp_obj["output"].at(0)["content"].at(0)["text"].str(), "Hello from Responses proxy", "Unexpected Responses output text"); EXPECT_EQ(received_openai_body["messages"].size(), 2u, "Expected 2 messages in OpenAI body"); EXPECT_EQ(received_openai_body["messages"].at(0)["role"].str(), "system", "Expected system role for instructions"); } // 2. Streaming test { Json req_obj = Json::object({ {"model", "gpt-5-model"}, {"stream", true}, {"input", "Stream test"}, }); HttpRequest req; req.method = "POST"; req.path = "/v1/responses"; req.headers.set("Content-Type", "application/json"); req.body = req_obj.dump(); RecorderResponseWriter w; handle_responses(cm, {}, std::chrono::milliseconds(0), req, w); EXPECT_EQ(w.status_code(), 200, "Expected 200 OK for streaming OpenAI Responses"); EXPECT_TRUE( w.body().find("event: response.created") != std::string::npos && w.body().find("event: response.output_text.delta") != std::string::npos && w.body().find("event: response.completed") != std::string::npos, "Expected OpenAI Responses SSE events in response stream"); } } void TestFullGatewayServerEndToEnd() { MockHttpServer upstream([](const HttpRequest &req, int fd) { Json body = Json::parse(req.body); std::string model_sent = body["model"].str(); Json resp = Json::object({ {"id", "chatcmpl-e2e"}, {"model", model_sent}, {"choices", Json::array({ Json::object({ {"index", 0}, {"message", Json::object({ {"role", "assistant"}, {"content", "E2E OK from " + model_sent}, })}, {"finish_reason", "stop"}, }), })}, }); MockHttpServer::send_response(fd, 200, "application/json", resp.dump()); }); std::vector configs = { {"e2e-model", "e2e-key", upstream.url(), ""}, }; ConfigManager cm(configs, {"e2e-model"}); GatewayServer gw(0, cm, {"gw-token"}, std::chrono::milliseconds(0)); std::string err; EXPECT_TRUE(gw.start(&err), "GatewayServer start failed: " + err); std::thread gw_thread([&]() { gw.serve(); }); std::string base_url = "http://127.0.0.1:" + std::to_string(gw.bound_port()); // Perform a real HTTP GET /v1/models via libcurl { CURL *c = curl_easy_init(); std::string resp_body; struct curl_slist *hdrs = nullptr; hdrs = curl_slist_append(hdrs, "Authorization: Bearer gw-token"); std::string url = base_url + "/v1/models"; curl_easy_setopt(c, CURLOPT_URL, url.c_str()); curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); curl_easy_setopt( c, CURLOPT_WRITEFUNCTION, +[](char *p, size_t s, size_t n, void *ud) -> size_t { static_cast(ud)->append(p, s * n); return s * n; }); curl_easy_setopt(c, CURLOPT_WRITEDATA, &resp_body); 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); EXPECT_EQ(rc, CURLE_OK, "curl /v1/models failed"); EXPECT_EQ(code, 200L, "Expected 200 from GatewayServer /v1/models"); Json j = Json::parse(resp_body); EXPECT_EQ(j["data"].at(0)["id"].str(), "e2e-model", "Unexpected model from GatewayServer"); } // Perform a real HTTP POST /v1/chat/completions via libcurl { CURL *c = curl_easy_init(); std::string resp_body; struct curl_slist *hdrs = nullptr; hdrs = curl_slist_append(hdrs, "Authorization: Bearer gw-token"); hdrs = curl_slist_append(hdrs, "Content-Type: application/json"); std::string payload = R"({"model":"e2e-model","messages":[{"role":"user","content":"ping"}]})"; std::string url = base_url + "/v1/chat/completions"; curl_easy_setopt(c, CURLOPT_URL, url.c_str()); curl_easy_setopt(c, CURLOPT_POST, 1L); curl_easy_setopt(c, CURLOPT_POSTFIELDS, payload.c_str()); curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); curl_easy_setopt( c, CURLOPT_WRITEFUNCTION, +[](char *p, size_t s, size_t n, void *ud) -> size_t { static_cast(ud)->append(p, s * n); return s * n; }); curl_easy_setopt(c, CURLOPT_WRITEDATA, &resp_body); 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); EXPECT_EQ(rc, CURLE_OK, "curl /v1/chat/completions failed"); EXPECT_EQ(code, 200L, "Expected 200 from GatewayServer /v1/chat/completions"); Json j = Json::parse(resp_body); EXPECT_EQ(j["choices"].at(0)["message"]["content"].str(), "E2E OK from e2e-model", "Unexpected E2E chat completion"); } gw.stop(); if (gw_thread.joinable()) { gw_thread.join(); } } } // namespace int main() { std::signal(SIGPIPE, SIG_IGN); ensure_curl_initialized(); struct TestEntry { const char *name; void (*fn)(); }; std::vector tests = { {"TestURLBuilding", TestURLBuilding}, {"TestTrialOrdering", TestTrialOrdering}, {"TestConfigManagerCSVWatcher", TestConfigManagerCSVWatcher}, {"TestGatewayCompletionsFailover", TestGatewayCompletionsFailover}, {"TestGatewayCompletionsStreamingFailover", TestGatewayCompletionsStreamingFailover}, {"TestModelsEndpoint", TestModelsEndpoint}, {"TestGatewayAuthentication", TestGatewayAuthentication}, {"TestCSVUpdater", TestCSVUpdater}, {"TestGatewayImageGenerations", TestGatewayImageGenerations}, {"TestGatewayNoAuthHeaderOnEmptyKey", TestGatewayNoAuthHeaderOnEmptyKey}, {"TestGatewayBlankKey", TestGatewayBlankKey}, {"TestGatewayExtraHeaders", TestGatewayExtraHeaders}, {"TestMultipleTokensKeyFile", TestMultipleTokensKeyFile}, {"TestRequestMessagesAutodetectionAndContentArrays", TestRequestMessagesAutodetectionAndContentArrays}, {"TestFibonacciBackoff", TestFibonacciBackoff}, {"TestAnthropicMessagesEndpoint", TestAnthropicMessagesEndpoint}, {"TestOpenAIResponsesEndpoint", TestOpenAIResponsesEndpoint}, {"TestFullGatewayServerEndToEnd", TestFullGatewayServerEndToEnd}, }; for (const auto &t : tests) { int before = g_failures; std::printf("=== RUN %s\n", t.name); t.fn(); if (g_failures == before) { std::printf("--- PASS: %s\n", t.name); } else { std::printf("--- FAIL: %s\n", t.name); } } if (g_failures == 0) { std::printf("PASS\n"); return 0; } std::printf("FAIL (%d failures)\n", g_failures); return 1; }