瀏覽代碼

feat(embeddings): OpenAI /v1/embeddings client + response parsing

Fszontagh 2 月之前
父節點
當前提交
d883eb996c
共有 5 個文件被更改,包括 78 次插入0 次删除
  1. 1 0
      src/CMakeLists.txt
  2. 30 0
      src/embeddings.cpp
  3. 14 0
      src/embeddings.hpp
  4. 4 0
      tests/CMakeLists.txt
  5. 29 0
      tests/test_embeddings.cpp

+ 1 - 0
src/CMakeLists.txt

@@ -9,6 +9,7 @@ add_library(vectorapi_core STATIC
     key_store.cpp
     settings_store.cpp
     collection_registry.cpp
+    embeddings.cpp
 )
 target_include_directories(vectorapi_core PUBLIC
     ${CMAKE_CURRENT_SOURCE_DIR}

+ 30 - 0
src/embeddings.cpp

@@ -0,0 +1,30 @@
+#include "embeddings.hpp"
+#include "errors.hpp"
+#include <httplib.h>
+namespace svapi {
+std::vector<float> parseEmbeddingResponse(const nlohmann::json& body) {
+    if (!body.contains("data") || !body["data"].is_array() || body["data"].empty())
+        throw ApiError(ErrCode::Unprocessable, "embedding_failed", "embedding response missing data[]");
+    const auto& emb = body["data"][0].value("embedding", nlohmann::json());
+    if (!emb.is_array() || emb.empty())
+        throw ApiError(ErrCode::Unprocessable, "embedding_failed", "embedding response missing embedding[]");
+    std::vector<float> out; out.reserve(emb.size());
+    for (const auto& v : emb) out.push_back(v.get<float>());
+    return out;
+}
+EmbeddingClient::EmbeddingClient(std::string apiBase, std::string apiKey)
+    : apiBase_(std::move(apiBase)), apiKey_(std::move(apiKey)) {}
+std::vector<float> EmbeddingClient::embed(const std::string& model, const std::string& text) {
+    httplib::Client cli(apiBase_);
+    cli.set_connection_timeout(10); cli.set_read_timeout(30);
+    cli.enable_server_certificate_verification(true);
+    if (!apiKey_.empty()) cli.set_bearer_token_auth(apiKey_);
+    nlohmann::json req{{"model", model}, {"input", text}};
+    auto res = cli.Post("/v1/embeddings", req.dump(), "application/json");
+    if (!res) throw ApiError(ErrCode::Unavailable, "embedding_unreachable", "could not reach embedding endpoint");
+    if (res->status != 200)
+        throw ApiError(ErrCode::Unprocessable, "embedding_failed",
+                       "embedding endpoint returned HTTP " + std::to_string(res->status));
+    return parseEmbeddingResponse(nlohmann::json::parse(res->body));
+}
+}

+ 14 - 0
src/embeddings.hpp

@@ -0,0 +1,14 @@
+#pragma once
+#include <string>
+#include <vector>
+#include <nlohmann/json.hpp>
+namespace svapi {
+std::vector<float> parseEmbeddingResponse(const nlohmann::json& body);
+class EmbeddingClient {
+public:
+    EmbeddingClient(std::string apiBase, std::string apiKey);
+    std::vector<float> embed(const std::string& model, const std::string& text);
+private:
+    std::string apiBase_, apiKey_;
+};
+}

+ 4 - 0
tests/CMakeLists.txt

@@ -32,3 +32,7 @@ gtest_discover_tests(test_stores)
 add_executable(test_registry test_registry.cpp)
 target_link_libraries(test_registry PRIVATE vectorapi_core GTest::gtest_main)
 gtest_discover_tests(test_registry)
+
+add_executable(test_embeddings test_embeddings.cpp)
+target_link_libraries(test_embeddings PRIVATE vectorapi_core GTest::gtest_main)
+gtest_discover_tests(test_embeddings)

+ 29 - 0
tests/test_embeddings.cpp

@@ -0,0 +1,29 @@
+#include <gtest/gtest.h>
+#include "embeddings.hpp"
+#include "errors.hpp"
+#include <httplib.h>
+#include <thread>
+using namespace svapi;
+
+TEST(Embeddings, ParsesWellFormedResponse) {
+    auto v = parseEmbeddingResponse(nlohmann::json::parse(R"({"data":[{"embedding":[0.5,1.0,1.5]}]})"));
+    ASSERT_EQ(v.size(), 3u); EXPECT_FLOAT_EQ(v[0], 0.5f); EXPECT_FLOAT_EQ(v[2], 1.5f);
+}
+TEST(Embeddings, MalformedThrows) {
+    EXPECT_THROW(parseEmbeddingResponse(nlohmann::json::parse(R"({"data":[]})")), ApiError);
+    EXPECT_THROW(parseEmbeddingResponse(nlohmann::json::object()), ApiError);
+}
+TEST(Embeddings, ClientHitsMockServer) {
+    httplib::Server mock;
+    mock.Post("/v1/embeddings", [](const httplib::Request& req, httplib::Response& res) {
+        auto j = nlohmann::json::parse(req.body);
+        EXPECT_EQ(j["model"], "m"); EXPECT_EQ(j["input"], "hello");
+        res.set_content(R"({"data":[{"embedding":[1,2,3,4]}]})", "application/json");
+    });
+    int port = mock.bind_to_any_port("127.0.0.1");
+    std::thread t([&]{ mock.listen_after_bind(); });
+    EmbeddingClient cli("http://127.0.0.1:" + std::to_string(port), "k");
+    auto v = cli.embed("m", "hello");
+    EXPECT_EQ(v.size(), 4u); EXPECT_FLOAT_EQ(v[3], 4.0f);
+    mock.stop(); t.join();
+}