Ver código fonte

feat(embeddings): configurable connect/read timeouts (default read 30→60s)

Adds embeddingConnectTimeoutSec (default 10) and embeddingReadTimeoutSec
(default 60, raised from hard-coded 30) as operator-configurable settings.
Both fields round-trip through fromJson/toJson, are mergeable via PUT
/api/v1/settings, and are forwarded to EmbeddingClient in resolveVector().
API docs (openapi.json, llms.txt) updated in sync.
Fszontagh 1 mês atrás
pai
commit
d5e638d7ec

+ 1 - 1
api/llms.txt

@@ -65,7 +65,7 @@ Requires a `kind=vector` collection. Vectors are stored with cosine-similarity i
 All settings endpoints require an admin key.
 
 - `GET  /api/v1/settings` — Return current settings as JSON. `openai_api_key` is masked: the field is absent and `openai_api_key_set` (bool) indicates whether a key is configured.
-- `PUT  /api/v1/settings` `{openai_api_base?, openai_api_key?, default_embedding_model?, cors_origins?, session_ttl_minutes?, webui_enabled?, default_project?}` — Merge the supplied fields into the current settings and persist. Omit `openai_api_key` (or pass an empty string) to keep the existing secret. Changes are hot-reloaded immediately.
+- `PUT  /api/v1/settings` `{openai_api_base?, openai_api_key?, default_embedding_model?, cors_origins?, session_ttl_minutes?, webui_enabled?, default_project?, embedding_connect_timeout_sec?, embedding_read_timeout_sec?}` — Merge the supplied fields into the current settings and persist. Omit `openai_api_key` (or pass an empty string) to keep the existing secret. Changes are hot-reloaded immediately.
 
 ## Stats
 

+ 3 - 1
api/openapi.json

@@ -846,7 +846,9 @@
                   "cors_origins": { "type": "array", "items": { "type": "string" } },
                   "session_ttl_minutes": { "type": "integer", "minimum": 1 },
                   "webui_enabled": { "type": "boolean" },
-                  "default_project": { "type": "string" }
+                  "default_project": { "type": "string" },
+                  "embedding_connect_timeout_sec": { "type": "integer", "minimum": 1, "description": "Embedding HTTP connect timeout in seconds (default 10)" },
+                  "embedding_read_timeout_sec": { "type": "integer", "minimum": 1, "description": "Embedding HTTP read timeout in seconds (default 60)" }
                 }
               }
             }

+ 5 - 3
src/embeddings.cpp

@@ -22,12 +22,14 @@ EmbeddingEndpoint splitEmbeddingBase(const std::string& apiBase) {
     return {base.substr(0, slash), base.substr(slash)};  // path keeps its leading '/', no trailing '/'
 }
 
-EmbeddingClient::EmbeddingClient(std::string apiBase, std::string apiKey)
-    : apiBase_(std::move(apiBase)), apiKey_(std::move(apiKey)) {}
+EmbeddingClient::EmbeddingClient(std::string apiBase, std::string apiKey,
+                                 uint32_t connectTimeoutSec, uint32_t readTimeoutSec)
+    : apiBase_(std::move(apiBase)), apiKey_(std::move(apiKey)),
+      connectTimeoutSec_(connectTimeoutSec), readTimeoutSec_(readTimeoutSec) {}
 std::vector<float> EmbeddingClient::embed(const std::string& model, const std::string& text) {
     EmbeddingEndpoint ep = splitEmbeddingBase(apiBase_);
     httplib::Client cli(ep.origin);
-    cli.set_connection_timeout(10); cli.set_read_timeout(30);
+    cli.set_connection_timeout(connectTimeoutSec_); cli.set_read_timeout(readTimeoutSec_);
     cli.enable_server_certificate_verification(true);
     if (!apiKey_.empty()) cli.set_bearer_token_auth(apiKey_);
     nlohmann::json req{{"model", model}, {"input", text}};

+ 3 - 1
src/embeddings.hpp

@@ -14,9 +14,11 @@ EmbeddingEndpoint splitEmbeddingBase(const std::string& apiBase);
 
 class EmbeddingClient {
 public:
-    EmbeddingClient(std::string apiBase, std::string apiKey);
+    EmbeddingClient(std::string apiBase, std::string apiKey,
+                    uint32_t connectTimeoutSec = 10, uint32_t readTimeoutSec = 60);
     std::vector<float> embed(const std::string& model, const std::string& text);
 private:
     std::string apiBase_, apiKey_;
+    uint32_t connectTimeoutSec_, readTimeoutSec_;
 };
 }

+ 4 - 0
src/handlers/settings.cpp

@@ -37,6 +37,10 @@ void registerSettingsRoutes(ApiServer& s) {
             updated.webuiEnabled = body["webui_enabled"].get<bool>();
         if (body.contains("default_project"))
             updated.defaultProject = body["default_project"].get<std::string>();
+        if (body.contains("embedding_connect_timeout_sec"))
+            updated.embeddingConnectTimeoutSec = body["embedding_connect_timeout_sec"].get<uint32_t>();
+        if (body.contains("embedding_read_timeout_sec"))
+            updated.embeddingReadTimeoutSec = body["embedding_read_timeout_sec"].get<uint32_t>();
         d->settings.save(updated);
         sendJson(res, 200, {{"ok", true}});
     });

+ 2 - 1
src/handlers/vectors.cpp

@@ -23,7 +23,8 @@ std::vector<float> resolveVector(ServerDeps* d, const CollectionMeta& meta, cons
     } else if (body.contains(textField) && body[textField].is_string()) {
         auto snap = d->settings.snapshot();
         std::string model = meta.embeddingModel.empty() ? snap->defaultEmbeddingModel : meta.embeddingModel;
-        EmbeddingClient emb(snap->openaiApiBase, snap->openaiApiKey);
+        EmbeddingClient emb(snap->openaiApiBase, snap->openaiApiKey,
+                            snap->embeddingConnectTimeoutSec, snap->embeddingReadTimeoutSec);
         v = emb.embed(model, body[textField].get<std::string>());
     } else {
         throw ApiError(ErrCode::Unprocessable, "validation",

+ 8 - 4
src/settings.cpp

@@ -9,9 +9,11 @@ Settings Settings::fromJson(const nlohmann::json& j) {
     s.defaultEmbeddingModel = j.value("default_embedding_model", s.defaultEmbeddingModel);
     if (j.contains("cors_origins") && j["cors_origins"].is_array())
         s.corsOrigins = j["cors_origins"].get<std::vector<std::string>>();
-    s.sessionTtlMinutes = j.value("session_ttl_minutes", s.sessionTtlMinutes);
-    s.webuiEnabled      = j.value("webui_enabled", s.webuiEnabled);
-    s.defaultProject    = j.value("default_project", s.defaultProject);
+    s.sessionTtlMinutes          = j.value("session_ttl_minutes", s.sessionTtlMinutes);
+    s.webuiEnabled               = j.value("webui_enabled", s.webuiEnabled);
+    s.defaultProject             = j.value("default_project", s.defaultProject);
+    s.embeddingConnectTimeoutSec = j.value("embedding_connect_timeout_sec", s.embeddingConnectTimeoutSec);
+    s.embeddingReadTimeoutSec    = j.value("embedding_read_timeout_sec", s.embeddingReadTimeoutSec);
     return s;
 }
 
@@ -19,7 +21,9 @@ nlohmann::json Settings::toJson() const {
     return {{"openai_api_base", openaiApiBase}, {"openai_api_key", openaiApiKey},
             {"default_embedding_model", defaultEmbeddingModel}, {"cors_origins", corsOrigins},
             {"session_ttl_minutes", sessionTtlMinutes}, {"webui_enabled", webuiEnabled},
-            {"default_project", defaultProject}};
+            {"default_project", defaultProject},
+            {"embedding_connect_timeout_sec", embeddingConnectTimeoutSec},
+            {"embedding_read_timeout_sec", embeddingReadTimeoutSec}};
 }
 
 } // namespace svapi

+ 2 - 0
src/settings.hpp

@@ -14,6 +14,8 @@ struct Settings {
     uint32_t    sessionTtlMinutes        = 720;
     bool        webuiEnabled             = true;
     std::string defaultProject           = "default";
+    uint32_t    embeddingConnectTimeoutSec = 10;
+    uint32_t    embeddingReadTimeoutSec    = 60;
 
     static Settings fromJson(const nlohmann::json& j);
     nlohmann::json  toJson() const;

+ 17 - 0
tests/test_settings.cpp

@@ -14,6 +14,23 @@ TEST(Settings, DefaultsAndRoundTrip) {
     EXPECT_EQ(Settings::fromJson(s.toJson()).toJson(), s.toJson());
 }
 
+TEST(Settings, EmbeddingTimeoutDefaults) {
+    Settings s = Settings::fromJson(nlohmann::json::object());
+    EXPECT_EQ(s.embeddingConnectTimeoutSec, 10u);
+    EXPECT_EQ(s.embeddingReadTimeoutSec, 60u);
+}
+
+TEST(Settings, EmbeddingTimeoutRoundTrip) {
+    nlohmann::json j{{"embedding_connect_timeout_sec", 30}, {"embedding_read_timeout_sec", 120}};
+    Settings s = Settings::fromJson(j);
+    EXPECT_EQ(s.embeddingConnectTimeoutSec, 30u);
+    EXPECT_EQ(s.embeddingReadTimeoutSec, 120u);
+    // round-trip: toJson → fromJson preserves values
+    Settings s2 = Settings::fromJson(s.toJson());
+    EXPECT_EQ(s2.embeddingConnectTimeoutSec, 30u);
+    EXPECT_EQ(s2.embeddingReadTimeoutSec, 120u);
+}
+
 TEST(ApiKey, JsonRoundTripAndPublicMasks) {
     ApiKey k = ApiKey::fromJson(nlohmann::json::parse(
         R"({"key":"abcdef0123456789","label":"n8n","projects":["p1","p2"],"admin":false,"created_at":7})"));