/**
* Embedder implementation â ONNX (internal) and HTTP (external) modes.
*/
#include "embedder.h"
#include "config.h"
#include "lang.h"
#include "tokenizer_wrapper.h"
#include
#include
#include
#include
#include
// --- External mode: HTTP client + JSON -----------------------------------------
#include "util/http_client.h"
#include "nlohmann_json.hpp"
namespace ragger {
using json = nlohmann::json;
// A response is "implausible" as an embedding if the model mistakenly
// serving /v1/embeddings isn't actually an embedder â e.g. a chat LLM
// bolted onto an OpenAI-compatible endpoint can return a fixed-size but
// meaningless vector (all zeros, all one repeated value, or NaN/Inf)
// instead of a real HTTP error. Catches the common failure shapes without
// being a strict statistical test â a legitimate embedding can still fail
// this in pathological cases, but a genuinely-wrong model reliably does.
static bool is_plausible_embedding(const std::vector& v) {
if (v.empty()) return false;
bool all_zero = true;
bool all_same = true;
double sumsq = 0.0;
for (size_t i = 0; i < v.size(); ++i) {
if (!std::isfinite(v[i])) return false; // NaN/Inf
if (v[i] != 0.0f) all_zero = false;
if (v[i] != v[0]) all_same = false;
sumsq += static_cast(v[i]) * v[i];
}
if (all_zero || all_same) return false;
// A real embedding has non-trivial magnitude; L2 norm collapsing to
// ~0 (denormals/underflow) is another sign of garbage output.
if (sumsq < 1e-12) return false;
return true;
}
// ====================================================================
// PIMPL â holds either an ONNX session or HTTP endpoint details.
// ====================================================================
struct Embedder::Impl {
bool external = false;
bool disabled = false; // default-constructed: cannot embed, never throws
// ---- Internal (ONNX) state ----
Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "ragger"};
Ort::SessionOptions session_options;
std::unique_ptr<:session> session;
std::unique_ptr tokenizer;
int onnx_dims = 0;
// ---- External (HTTP) state ----
std::string ext_host;
int ext_port = 0;
std::string ext_model;
std::string ext_api_key;
int ext_dims = 0; // 0 = not yet known
mutable std::string last_served_model; // model reported by the last response
mutable bool last_probe_sane = false; // plausibility check on the last probe vector
// ---- Disabled constructor ----
Impl() : disabled(true) {}
// ---- Internal constructor ----
explicit Impl(const std::string& model_dir) {
std::filesystem::path model_path = std::filesystem::path(model_dir) / "model.onnx";
// Some HuggingFace repos put the ONNX model in an onnx/ subdirectory.
if (!std::filesystem::exists(model_path)) {
model_path = std::filesystem::path(model_dir) / "onnx" / "model.onnx";
}
std::filesystem::path tokenizer_path = std::filesystem::path(model_dir) / "tokenizer.json";
if (!std::filesystem::exists(model_path)) {
throw std::runtime_error(std::format(lang::ERR_MODEL_NOT_FOUND, model_path.string()));
}
if (!std::filesystem::exists(tokenizer_path)) {
throw std::runtime_error(std::format(lang::ERR_TOKENIZER_NOT_FOUND, tokenizer_path.string()));
}
tokenizer = std::make_unique(tokenizer_path.string());
session = std::make_unique<:session>(env, model_path.c_str(), session_options);
onnx_dims = config().embedding_dimensions;
}
// ---- External constructor ----
Impl(const std::string& host, int port, const std::string& model,
const std::string& api_key, int dims)
: external(true), ext_host(host), ext_port(port),
ext_model(model), ext_api_key(api_key), ext_dims(dims) {}
// ---- ONNX encode ----
std::vector encode_onnx(const std::string& text) const {
auto encoded = tokenizer->encode_with_mask(text);
const auto& input_ids = encoded.input_ids;
const auto& attention_mask = encoded.attention_mask;
size_t seq_len = input_ids.size();
if (seq_len == 0) {
throw std::runtime_error(lang::ERR_EMPTY_TOKENIZATION);
}
Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
std::vector shape = {1, static_cast(seq_len)};
std::vector input_ids_i64(input_ids.begin(), input_ids.end());
std::vector attention_mask_i64(attention_mask.begin(), attention_mask.end());
std::vector token_type_ids_i64(seq_len, 0);
Ort::Value input_ids_tensor = Ort::Value::CreateTensor(
memory_info, input_ids_i64.data(), input_ids_i64.size(), shape.data(), shape.size());
Ort::Value attention_mask_tensor = Ort::Value::CreateTensor(
memory_info, attention_mask_i64.data(), attention_mask_i64.size(), shape.data(), shape.size());
Ort::Value token_type_ids_tensor = Ort::Value::CreateTensor(
memory_info, token_type_ids_i64.data(), token_type_ids_i64.size(), shape.data(), shape.size());
const char* input_names[] = {"input_ids", "attention_mask", "token_type_ids"};
const char* output_names[] = {"last_hidden_state"};
std::vector<:value> input_tensors;
input_tensors.push_back(std::move(input_ids_tensor));
input_tensors.push_back(std::move(attention_mask_tensor));
input_tensors.push_back(std::move(token_type_ids_tensor));
auto output_tensors = session->Run(
Ort::RunOptions{nullptr},
input_names, input_tensors.data(), 3,
output_names, 1
);
float* output_data = output_tensors[0].GetTensorMutableData();
auto output_shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape();
if (output_shape.size() != 3 || output_shape[0] != 1 || output_shape[2] != onnx_dims) {
throw std::runtime_error(lang::ERR_OUTPUT_SHAPE);
}
size_t output_seq_len = output_shape[1];
size_t hidden_size = output_shape[2];
// Mean pooling with attention mask
std::vector pooled(hidden_size, 0.0f);
float mask_sum = 0.0f;
for (size_t i = 0; i < output_seq_len; ++i) {
float mask_value = static_cast(attention_mask[i]);
mask_sum += mask_value;
for (size_t j = 0; j < hidden_size; ++j) {
pooled[j] += output_data[i * hidden_size + j] * mask_value;
}
}
if (mask_sum > 0.0f) {
for (auto& val : pooled) val /= mask_sum;
}
// L2 normalization
float norm = 0.0f;
for (float val : pooled) norm += val * val;
norm = std::sqrt(norm);
if (norm > 1e-12f) {
for (auto& val : pooled) val /= norm;
}
return pooled;
}
// ---- build base URL ----
std::string base_url() const {
return "http://" + ext_host + ":" + std::to_string(ext_port);
}
// ---- External encode via /v1/embeddings ----
std::vector encode_external(const std::string& text) const {
json body = {
{"input", text},
{"model", ext_model}
};
// Pass dimensions if explicitly set (MRL / truncated models).
if (ext_dims > 0) {
body["dimensions"] = ext_dims;
}
std::string payload = body.dump();
std::string url = base_url() + "/v1/embeddings";
std::vector<:string> headers{"Content-Type: application/json"};
if (!ext_api_key.empty()) {
headers.push_back(util::bearer_header(ext_api_key));
}
util::HttpClient http;
auto resp = http.post(url, payload, headers, /*timeout_sec=*/30);
if (!resp.ok()) {
throw std::runtime_error(std::format("Embedding request failed: {}", resp.error));
}
auto j = json::parse(resp.body, nullptr, false);
if (j.is_discarded()) {
throw std::runtime_error("Failed to parse embedding response");
}
// OpenAI format: { "data": [ { "embedding": [...] } ] }
if (!j.contains("data") || !j["data"].is_array() || j["data"].empty()) {
// Check for error
if (j.contains("error")) {
std::string msg = j["error"].is_string()
? j["error"].get<:string>()
: j["error"].value("message", "unknown error");
throw std::runtime_error("Embedding API error: " + msg);
}
throw std::runtime_error("Unexpected embedding response format");
}
auto& emb = j["data"][0]["embedding"];
if (!emb.is_array()) {
throw std::runtime_error("Embedding response missing 'embedding' array");
}
// Record which model the server says actually served the request â
// llama-swap / LM Studio may answer with a different loaded model
// than the one requested. The probe route surfaces this mismatch.
last_served_model = j.value("model", "");
std::vector vec;
vec.reserve(emb.size());
for (const auto& v : emb) {
vec.push_back(v.get());
}
return vec;
}
// ---- Dispatch ----
std::vector encode(const std::string& text) const {
// Empty, not an exception: callers gate on ready() and defer the
// write. Returning empty keeps a stray call from taking the daemon
// down, but an empty vector must never reach bind_embedding().
if (disabled) return {};
if (external) return encode_external(text);
return encode_onnx(text);
}
int dimensions() const {
if (disabled) return 0;
if (external) return ext_dims;
return onnx_dims;
}
std::vector<:string> list_remote_models() const {
if (!external) return {};
std::string url = base_url() + "/v1/models";
std::vector<:string> headers;
if (!ext_api_key.empty()) {
headers.push_back(util::bearer_header(ext_api_key));
}
util::HttpClient http;
auto resp = http.get(url, headers, /*timeout_sec=*/10);
if (!resp.ok()) return {};
auto j = json::parse(resp.body, nullptr, false);
if (j.is_discarded() || !j.contains("data") || !j["data"].is_array())
return {};
std::vector<:string> models;
for (const auto& m : j["data"]) {
if (m.contains("id") && m["id"].is_string()) {
models.push_back(m["id"].get<:string>());
}
}
return models;
}
int probe_dimensions() const {
if (!external) return 0;
last_probe_sane = false;
try {
auto vec = encode_external("dimension probe");
last_probe_sane = is_plausible_embedding(vec);
return static_cast(vec.size());
} catch (...) {
return 0;
}
}
};
// ====================================================================
// Public API
// ====================================================================
Embedder::Embedder()
: pImpl(std::make_unique()) {}
Embedder::Embedder(const std::string& model_dir)
: pImpl(std::make_unique(model_dir)) {}
Embedder::Embedder(const std::string& host, int port,
const std::string& model_name,
const std::string& api_key, int dimensions)
: pImpl(std::make_unique(host, port, model_name, api_key, dimensions)) {}
Embedder::~Embedder() = default;
std::vector Embedder::encode(const std::string& text) const {
return pImpl->encode(text);
}
int Embedder::dimensions() const {
return pImpl->dimensions();
}
bool Embedder::ready() const {
return !pImpl->disabled;
}
bool Embedder::is_external() const {
return pImpl->external;
}
std::vector<:string> Embedder::list_remote_models() const {
return pImpl->list_remote_models();
}
int Embedder::probe_dimensions() const {
return pImpl->probe_dimensions();
}
std::string Embedder::last_served_model() const {
return pImpl->last_served_model;
}
bool Embedder::last_probe_sane() const {
return pImpl->last_probe_sane;
}
std::optional<:vector>> Embedder::embed(const std::string& text) const {
if (!ready()) return std::nullopt;
auto vec = encode(text);
if (vec.empty()) return std::nullopt;
return vec;
}
std::vector<:optional>>>
Embedder::embed_batch(const std::vector<:string>& texts) const {
std::vector<:optional>>> out;
out.reserve(texts.size());
for (const auto& t : texts) out.push_back(embed(t));
return out;
}
} // namespace ragger