Coverage Report

Created: 2026-09-11 20:48

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/function/ai/ai_adapter.h
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#pragma once
19
20
#include <gen_cpp/PaloInternalService_types.h>
21
#include <rapidjson/rapidjson.h>
22
23
#include <algorithm>
24
#include <cctype>
25
#include <memory>
26
#include <string>
27
#include <string_view>
28
#include <unordered_map>
29
#include <vector>
30
31
#include "common/status.h"
32
#include "core/string_buffer.hpp"
33
#include "rapidjson/document.h"
34
#include "rapidjson/stringbuffer.h"
35
#include "rapidjson/writer.h"
36
#include "service/http/http_client.h"
37
#include "service/http/http_headers.h"
38
#include "util/security.h"
39
40
namespace doris {
41
42
struct AIResource {
43
23
    AIResource() = default;
44
    AIResource(const TAIResource& tai)
45
15
            : endpoint(tai.endpoint),
46
15
              provider_type(tai.provider_type),
47
15
              model_name(tai.model_name),
48
15
              api_key(tai.api_key),
49
15
              temperature(tai.temperature),
50
15
              max_tokens(tai.max_tokens),
51
15
              max_retries(tai.max_retries),
52
15
              retry_delay_second(tai.retry_delay_second),
53
15
              anthropic_version(tai.anthropic_version),
54
15
              dimensions(tai.dimensions) {}
55
56
    std::string endpoint;
57
    std::string provider_type;
58
    std::string model_name;
59
    std::string api_key;
60
    double temperature;
61
    int64_t max_tokens;
62
    int32_t max_retries;
63
    int32_t retry_delay_second;
64
    std::string anthropic_version;
65
    int32_t dimensions;
66
67
1
    void serialize(BufferWritable& buf) const {
68
1
        buf.write_binary(endpoint);
69
1
        buf.write_binary(provider_type);
70
1
        buf.write_binary(model_name);
71
1
        buf.write_binary(api_key);
72
1
        buf.write_binary(temperature);
73
1
        buf.write_binary(max_tokens);
74
1
        buf.write_binary(max_retries);
75
1
        buf.write_binary(retry_delay_second);
76
1
        buf.write_binary(anthropic_version);
77
1
        buf.write_binary(dimensions);
78
1
    }
79
80
1
    void deserialize(BufferReadable& buf) {
81
1
        buf.read_binary(endpoint);
82
1
        buf.read_binary(provider_type);
83
1
        buf.read_binary(model_name);
84
1
        buf.read_binary(api_key);
85
1
        buf.read_binary(temperature);
86
1
        buf.read_binary(max_tokens);
87
1
        buf.read_binary(max_retries);
88
1
        buf.read_binary(retry_delay_second);
89
1
        buf.read_binary(anthropic_version);
90
1
        buf.read_binary(dimensions);
91
1
    }
92
};
93
94
enum class MultimodalType { IMAGE, VIDEO, AUDIO };
95
96
3
inline const char* multimodal_type_to_string(MultimodalType type) {
97
3
    switch (type) {
98
1
    case MultimodalType::IMAGE:
99
1
        return "image";
100
1
    case MultimodalType::VIDEO:
101
1
        return "video";
102
1
    case MultimodalType::AUDIO:
103
1
        return "audio";
104
3
    }
105
0
    return "unknown";
106
3
}
107
108
class AIAdapter {
109
public:
110
191
    virtual ~AIAdapter() = default;
111
112
    // Set authentication headers for the HTTP client
113
    virtual Status set_authentication(HttpClient* client) const = 0;
114
115
134
    virtual void init(const TAIResource& config) { _config = config; }
116
16
    virtual void init(const AIResource& config) {
117
16
        _config.endpoint = config.endpoint;
118
16
        _config.provider_type = config.provider_type;
119
16
        _config.model_name = config.model_name;
120
16
        _config.api_key = config.api_key;
121
16
        _config.temperature = config.temperature;
122
16
        _config.max_tokens = config.max_tokens;
123
16
        _config.max_retries = config.max_retries;
124
16
        _config.retry_delay_second = config.retry_delay_second;
125
16
        _config.anthropic_version = config.anthropic_version;
126
16
    }
127
128
    // Build request payload based on input text strings
129
    virtual Status build_request_payload(const std::vector<std::string>& inputs,
130
                                         const char* const system_prompt,
131
1
                                         std::string& request_body) const {
132
1
        return Status::NotSupported("{} don't support text generation", _config.provider_type);
133
1
    }
134
135
    // Parse response from AI service and extract generated text results
136
    virtual Status parse_response(const std::string& response_body,
137
                                  std::vector<std::string>& results,
138
1
                                  bool /* expand_batch */ = true) const {
139
1
        return Status::NotSupported("{} don't support text generation", _config.provider_type);
140
1
    }
141
142
    virtual Status build_embedding_request(const std::vector<std::string>& inputs,
143
0
                                           std::string& request_body) const {
144
0
        return embed_not_supported_status();
145
0
    }
146
147
    virtual Status build_multimodal_embedding_request(
148
            const std::vector<MultimodalType>& /*media_types*/,
149
            const std::vector<std::string>& /*media_urls*/,
150
            const std::vector<std::string>& /*media_content_types*/,
151
0
            std::string& /*request_body*/) const {
152
0
        return Status::NotSupported("{} does not support multimodal Embed feature.",
153
0
                                    _config.provider_type);
154
0
    }
155
156
    virtual Status parse_embedding_response(const std::string& response_body,
157
0
                                            std::vector<std::vector<float>>& results) const {
158
0
        return embed_not_supported_status();
159
0
    }
160
161
protected:
162
    TAIResource _config;
163
164
4
    Status embed_not_supported_status() const {
165
4
        return Status::NotSupported(
166
4
                "{} does not support the Embed feature. Currently supported providers are "
167
4
                "OpenAI, Gemini, Voyage, Jina, Qwen, and Minimax.",
168
4
                _config.provider_type);
169
4
    }
170
171
    // Appends one provider-parsed text result to `results`.
172
    // The adapter has already parsed the provider's outer response envelope before calling here.
173
    // Example:
174
    // provider response -> choices[0].message.content = "[\"1\",\"0\",\"1\"]"
175
    // this helper       -> appends "1", "0", "1" into `results`
176
    // Set expand_batch to false when AI_AGG needs the complete generated text as one result.
177
    static Status append_parsed_text_result(std::string_view text,
178
                                            std::vector<std::string>& results,
179
103
                                            bool expand_batch = true) {
180
103
        if (!expand_batch) {
181
10
            results.emplace_back(text.data(), text.size());
182
10
            return Status::OK();
183
10
        }
184
185
93
        size_t begin = 0;
186
93
        size_t end = text.size();
187
123
        while (begin < end && std::isspace(static_cast<unsigned char>(text[begin]))) {
188
30
            ++begin;
189
30
        }
190
117
        while (begin < end && std::isspace(static_cast<unsigned char>(text[end - 1]))) {
191
24
            --end;
192
24
        }
193
194
93
        if (begin < end && text[begin] == '[' && text[end - 1] == ']') {
195
74
            rapidjson::Document doc;
196
74
            doc.Parse(text.data() + begin, end - begin);
197
74
            if (!doc.HasParseError() && doc.IsArray()) {
198
162
                for (rapidjson::SizeType i = 0; i < doc.Size(); ++i) {
199
91
                    if (!doc[i].IsString()) {
200
1
                        return Status::InternalError(
201
1
                                "Invalid batch result format, array element {} is not a string", i);
202
1
                    }
203
90
                    results.emplace_back(doc[i].GetString(), doc[i].GetStringLength());
204
90
                }
205
71
                return Status::OK();
206
72
            }
207
74
        }
208
209
21
        results.emplace_back(text.data(), text.size());
210
21
        return Status::OK();
211
93
    }
212
213
    // return true if the model support dimension parameter
214
1
    virtual bool supports_dimension_param(const std::string& model_name) const { return false; }
215
216
    // Different providers may have different dimension parameter names.
217
0
    virtual std::string get_dimension_param_name() const { return "dimensions"; }
218
219
    virtual void add_dimension_params(rapidjson::Value& doc,
220
20
                                      rapidjson::Document::AllocatorType& allocator) const {
221
20
        if (_config.dimensions != -1 && supports_dimension_param(_config.model_name)) {
222
13
            std::string param_name = get_dimension_param_name();
223
13
            rapidjson::Value name(param_name.c_str(), allocator);
224
13
            doc.AddMember(name, _config.dimensions, allocator);
225
13
        }
226
20
    }
227
228
    // Validates common multimodal embedding request invariants shared by providers.
229
    Status validate_multimodal_embedding_inputs(
230
            std::string_view provider_name, const std::vector<MultimodalType>& media_types,
231
            const std::vector<std::string>& media_urls,
232
16
            std::initializer_list<MultimodalType> supported_types) const {
233
16
        if (media_urls.empty()) {
234
1
            return Status::InvalidArgument("{} multimodal embed inputs can not be empty",
235
1
                                           provider_name);
236
1
        }
237
15
        if (media_types.size() != media_urls.size()) {
238
1
            return Status::InvalidArgument(
239
1
                    "{} multimodal embed input size mismatch, media_types={}, media_urls={}",
240
1
                    provider_name, media_types.size(), media_urls.size());
241
1
        }
242
19
        for (MultimodalType media_type : media_types) {
243
19
            bool supported = false;
244
31
            for (MultimodalType supported_type : supported_types) {
245
31
                if (media_type == supported_type) {
246
18
                    supported = true;
247
18
                    break;
248
18
                }
249
31
            }
250
19
            if (!supported) [[unlikely]] {
251
1
                return Status::InvalidArgument(
252
1
                        "{} only supports {} multimodal embed, got {}", provider_name,
253
1
                        supported_multimodal_types_to_string(supported_types),
254
1
                        multimodal_type_to_string(media_type));
255
1
            }
256
19
        }
257
13
        return Status::OK();
258
14
    }
259
260
    static std::string supported_multimodal_types_to_string(
261
1
            std::initializer_list<MultimodalType> supported_types) {
262
1
        std::string result;
263
2
        for (MultimodalType type : supported_types) {
264
2
            if (!result.empty()) {
265
1
                result += "/";
266
1
            }
267
2
            result += multimodal_type_to_string(type);
268
2
        }
269
1
        return result;
270
1
    }
271
};
272
273
// Most LLM-providers' Embedding formats are based on VoyageAI.
274
// The following adapters inherit from VoyageAIAdapter to directly reuse its embedding logic.
275
class VoyageAIAdapter : public AIAdapter {
276
public:
277
2
    Status set_authentication(HttpClient* client) const override {
278
2
        client->set_header(HttpHeaders::AUTHORIZATION, "Bearer " + _config.api_key);
279
2
        client->set_content_type("application/json");
280
281
2
        return Status::OK();
282
2
    }
283
284
    Status build_embedding_request(const std::vector<std::string>& inputs,
285
8
                                   std::string& request_body) const override {
286
8
        rapidjson::Document doc;
287
8
        doc.SetObject();
288
8
        auto& allocator = doc.GetAllocator();
289
290
        /*{
291
            "model": "xxx",
292
            "input": [
293
              "xxx",
294
              "xxx",
295
              ...
296
            ],
297
            "output_dimensions": 512
298
        }*/
299
8
        doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator), allocator);
300
8
        add_dimension_params(doc, allocator);
301
302
8
        rapidjson::Value input(rapidjson::kArrayType);
303
8
        for (const auto& msg : inputs) {
304
8
            input.PushBack(rapidjson::Value(msg.c_str(), allocator), allocator);
305
8
        }
306
8
        doc.AddMember("input", input, allocator);
307
308
8
        rapidjson::StringBuffer buffer;
309
8
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
310
8
        doc.Accept(writer);
311
8
        request_body = buffer.GetString();
312
313
8
        return Status::OK();
314
8
    }
315
316
    Status build_multimodal_embedding_request(
317
            const std::vector<MultimodalType>& media_types,
318
            const std::vector<std::string>& media_urls,
319
            const std::vector<std::string>& /*media_content_types*/,
320
2
            std::string& request_body) const override {
321
2
        RETURN_IF_ERROR(validate_multimodal_embedding_inputs(
322
2
                "VoyageAI", media_types, media_urls,
323
2
                {MultimodalType::IMAGE, MultimodalType::VIDEO}));
324
2
        if (_config.dimensions != -1) {
325
2
            LOG(WARNING) << "VoyageAI multimodal embedding currently ignores dimensions parameter, "
326
2
                         << "model=" << _config.model_name << ", dimensions=" << _config.dimensions;
327
2
        }
328
329
2
        rapidjson::Document doc;
330
2
        doc.SetObject();
331
2
        auto& allocator = doc.GetAllocator();
332
333
        /*{
334
            "inputs": [
335
              {
336
                "content": [
337
                  {"type": "image_url", "image_url": "<url>"}
338
                ]
339
              },
340
              {
341
                "content": [
342
                  {"type": "video_url", "video_url": "<url>"}
343
                ]
344
              }
345
            ],
346
            "model": "voyage-multimodal-3.5"
347
        }*/
348
2
        doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator), allocator);
349
350
2
        rapidjson::Value request_inputs(rapidjson::kArrayType);
351
5
        for (size_t i = 0; i < media_urls.size(); ++i) {
352
3
            rapidjson::Value input(rapidjson::kObjectType);
353
3
            rapidjson::Value content(rapidjson::kArrayType);
354
3
            rapidjson::Value media_item(rapidjson::kObjectType);
355
3
            if (media_types[i] == MultimodalType::IMAGE) {
356
1
                media_item.AddMember("type", "image_url", allocator);
357
1
                media_item.AddMember("image_url",
358
1
                                     rapidjson::Value(media_urls[i].c_str(), allocator), allocator);
359
2
            } else {
360
2
                media_item.AddMember("type", "video_url", allocator);
361
2
                media_item.AddMember("video_url",
362
2
                                     rapidjson::Value(media_urls[i].c_str(), allocator), allocator);
363
2
            }
364
3
            content.PushBack(media_item, allocator);
365
3
            input.AddMember("content", content, allocator);
366
3
            request_inputs.PushBack(input, allocator);
367
3
        }
368
369
2
        doc.AddMember("inputs", request_inputs, allocator);
370
371
2
        rapidjson::StringBuffer buffer;
372
2
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
373
2
        doc.Accept(writer);
374
2
        request_body = buffer.GetString();
375
2
        return Status::OK();
376
2
    }
377
378
    Status parse_embedding_response(const std::string& response_body,
379
5
                                    std::vector<std::vector<float>>& results) const override {
380
5
        rapidjson::Document doc;
381
5
        doc.Parse(response_body.c_str());
382
383
5
        if (doc.HasParseError() || !doc.IsObject()) {
384
1
            return Status::InternalError("Failed to parse {} response: {}", _config.provider_type,
385
1
                                         response_body);
386
1
        }
387
4
        if (!doc.HasMember("data") || !doc["data"].IsArray()) {
388
1
            return Status::InternalError("Invalid {} response format: {}", _config.provider_type,
389
1
                                         response_body);
390
1
        }
391
392
        /*{
393
            "data":[
394
              {
395
                "object": "embedding",
396
                "embedding": [...], <- only need this
397
                "index": 0
398
              },
399
              {
400
                "object": "embedding",
401
                "embedding": [...],
402
                "index": 1
403
              }, ...
404
            ],
405
            "model"....
406
        }*/
407
3
        const auto& data = doc["data"];
408
3
        results.reserve(data.Size());
409
7
        for (rapidjson::SizeType i = 0; i < data.Size(); i++) {
410
5
            if (!data[i].HasMember("embedding") || !data[i]["embedding"].IsArray()) {
411
1
                return Status::InternalError("Invalid {} response format: {}",
412
1
                                             _config.provider_type, response_body);
413
1
            }
414
415
4
            std::transform(data[i]["embedding"].Begin(), data[i]["embedding"].End(),
416
4
                           std::back_inserter(results.emplace_back()),
417
10
                           [](const auto& val) { return val.GetFloat(); });
418
4
        }
419
420
2
        return Status::OK();
421
3
    }
422
423
protected:
424
4
    bool supports_dimension_param(const std::string& model_name) const override {
425
4
        static const std::unordered_set<std::string> no_dimension_models = {
426
4
                "voyage-law-2", "voyage-2", "voyage-code-2", "voyage-finance-2",
427
4
                "voyage-multimodal-3"};
428
4
        return !no_dimension_models.contains(model_name);
429
4
    }
430
431
1
    std::string get_dimension_param_name() const override { return "output_dimension"; }
432
};
433
434
// Local AI adapter for locally hosted models (Ollama, LLaMA, etc.)
435
class LocalAdapter : public AIAdapter {
436
public:
437
    // Local deployments typically don't need authentication
438
2
    Status set_authentication(HttpClient* client) const override {
439
2
        client->set_content_type("application/json");
440
2
        return Status::OK();
441
2
    }
442
443
    Status build_request_payload(const std::vector<std::string>& inputs,
444
                                 const char* const system_prompt,
445
3
                                 std::string& request_body) const override {
446
3
        rapidjson::Document doc;
447
3
        doc.SetObject();
448
3
        auto& allocator = doc.GetAllocator();
449
450
3
        std::string end_point = _config.endpoint;
451
3
        if (end_point.ends_with("chat") || end_point.ends_with("generate")) {
452
2
            RETURN_IF_ERROR(
453
2
                    build_ollama_request(doc, allocator, inputs, system_prompt, request_body));
454
2
        } else {
455
1
            RETURN_IF_ERROR(
456
1
                    build_default_request(doc, allocator, inputs, system_prompt, request_body));
457
1
        }
458
459
3
        rapidjson::StringBuffer buffer;
460
3
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
461
3
        doc.Accept(writer);
462
3
        request_body = buffer.GetString();
463
464
3
        return Status::OK();
465
3
    }
466
467
    Status parse_response(const std::string& response_body, std::vector<std::string>& results,
468
8
                          bool expand_batch = true) const override {
469
8
        rapidjson::Document doc;
470
8
        doc.Parse(response_body.c_str());
471
472
8
        if (doc.HasParseError() || !doc.IsObject()) {
473
1
            return Status::InternalError("Failed to parse {} response: {}", _config.provider_type,
474
1
                                         response_body);
475
1
        }
476
477
        // Handle various response formats from local LLMs
478
        // Format 1: OpenAI-compatible format with choices/message/content
479
7
        if (doc.HasMember("choices") && doc["choices"].IsArray()) {
480
2
            const auto& choices = doc["choices"];
481
2
            results.reserve(choices.Size());
482
483
4
            for (rapidjson::SizeType i = 0; i < choices.Size(); i++) {
484
2
                if (choices[i].HasMember("message") && choices[i]["message"].HasMember("content") &&
485
2
                    choices[i]["message"]["content"].IsString()) {
486
2
                    RETURN_IF_ERROR(append_parsed_text_result(
487
2
                            choices[i]["message"]["content"].GetString(), results, expand_batch));
488
2
                } else if (choices[i].HasMember("text") && choices[i]["text"].IsString()) {
489
                    // Some local LLMs use a simpler format
490
0
                    RETURN_IF_ERROR(append_parsed_text_result(choices[i]["text"].GetString(),
491
0
                                                              results, expand_batch));
492
0
                }
493
2
            }
494
5
        } else if (doc.HasMember("text") && doc["text"].IsString()) {
495
            // Format 2: Simple response with just "text" or "content" field
496
1
            RETURN_IF_ERROR(
497
1
                    append_parsed_text_result(doc["text"].GetString(), results, expand_batch));
498
4
        } else if (doc.HasMember("content") && doc["content"].IsString()) {
499
1
            RETURN_IF_ERROR(
500
1
                    append_parsed_text_result(doc["content"].GetString(), results, expand_batch));
501
3
        } else if (doc.HasMember("response") && doc["response"].IsString()) {
502
            // Format 3: Response field (Ollama `generate` format)
503
1
            RETURN_IF_ERROR(
504
1
                    append_parsed_text_result(doc["response"].GetString(), results, expand_batch));
505
2
        } else if (doc.HasMember("message") && doc["message"].IsObject() &&
506
2
                   doc["message"].HasMember("content") && doc["message"]["content"].IsString()) {
507
            // Format 4: message/content field (Ollama `chat` format)
508
1
            RETURN_IF_ERROR(append_parsed_text_result(doc["message"]["content"].GetString(),
509
1
                                                      results, expand_batch));
510
1
        } else {
511
1
            return Status::NotSupported("Unsupported response format from local AI.");
512
1
        }
513
6
        return Status::OK();
514
7
    }
515
516
    Status build_embedding_request(const std::vector<std::string>& inputs,
517
1
                                   std::string& request_body) const override {
518
1
        rapidjson::Document doc;
519
1
        doc.SetObject();
520
1
        auto& allocator = doc.GetAllocator();
521
522
1
        if (!_config.model_name.empty()) {
523
1
            doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator),
524
1
                          allocator);
525
1
        }
526
527
1
        add_dimension_params(doc, allocator);
528
529
1
        rapidjson::Value input(rapidjson::kArrayType);
530
1
        for (const auto& msg : inputs) {
531
1
            input.PushBack(rapidjson::Value(msg.c_str(), allocator), allocator);
532
1
        }
533
1
        doc.AddMember("input", input, allocator);
534
535
1
        rapidjson::StringBuffer buffer;
536
1
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
537
1
        doc.Accept(writer);
538
1
        request_body = buffer.GetString();
539
540
1
        return Status::OK();
541
1
    }
542
543
    Status build_multimodal_embedding_request(
544
            const std::vector<MultimodalType>& /*media_types*/,
545
            const std::vector<std::string>& /*media_urls*/,
546
            const std::vector<std::string>& /*media_content_types*/,
547
0
            std::string& /*request_body*/) const override {
548
0
        return Status::NotSupported("{} does not support multimodal Embed feature.",
549
0
                                    _config.provider_type);
550
0
    }
551
552
    Status parse_embedding_response(const std::string& response_body,
553
3
                                    std::vector<std::vector<float>>& results) const override {
554
3
        rapidjson::Document doc;
555
3
        doc.Parse(response_body.c_str());
556
557
3
        if (doc.HasParseError() || !doc.IsObject()) {
558
0
            return Status::InternalError("Failed to parse {} response: {}", _config.provider_type,
559
0
                                         response_body);
560
0
        }
561
562
        // parse different response format
563
3
        rapidjson::Value embedding;
564
3
        if (doc.HasMember("data") && doc["data"].IsArray()) {
565
            // "data":["object":"embedding", "embedding":[0.1, 0.2...], "index":0]
566
1
            const auto& data = doc["data"];
567
1
            results.reserve(data.Size());
568
3
            for (rapidjson::SizeType i = 0; i < data.Size(); i++) {
569
2
                if (!data[i].HasMember("embedding") || !data[i]["embedding"].IsArray()) {
570
0
                    return Status::InternalError("Invalid {} response format",
571
0
                                                 _config.provider_type);
572
0
                }
573
574
2
                std::transform(data[i]["embedding"].Begin(), data[i]["embedding"].End(),
575
2
                               std::back_inserter(results.emplace_back()),
576
5
                               [](const auto& val) { return val.GetFloat(); });
577
2
            }
578
2
        } else if (doc.HasMember("embeddings") && doc["embeddings"].IsArray()) {
579
            // "embeddings":[[0.1, 0.2, ...]]
580
1
            results.reserve(1);
581
2
            for (int i = 0; i < doc["embeddings"].Size(); i++) {
582
1
                embedding = doc["embeddings"][i];
583
1
                std::transform(embedding.Begin(), embedding.End(),
584
1
                               std::back_inserter(results.emplace_back()),
585
2
                               [](const auto& val) { return val.GetFloat(); });
586
1
            }
587
1
        } else if (doc.HasMember("embedding") && doc["embedding"].IsArray()) {
588
            // "embedding":[0.1, 0.2, ...]
589
1
            results.reserve(1);
590
1
            embedding = doc["embedding"];
591
1
            std::transform(embedding.Begin(), embedding.End(),
592
1
                           std::back_inserter(results.emplace_back()),
593
3
                           [](const auto& val) { return val.GetFloat(); });
594
1
        } else {
595
0
            return Status::InternalError("Invalid {} response format: {}", _config.provider_type,
596
0
                                         response_body);
597
0
        }
598
599
3
        return Status::OK();
600
3
    }
601
602
private:
603
    Status build_ollama_request(rapidjson::Document& doc,
604
                                rapidjson::Document::AllocatorType& allocator,
605
                                const std::vector<std::string>& inputs,
606
2
                                const char* const system_prompt, std::string& request_body) const {
607
        /*
608
        for endpoints end_with `/chat` like 'http://localhost:11434/api/chat':
609
        {
610
            "model": <model_name>,
611
            "stream": false,
612
            "think": false,
613
            "options": {
614
                "temperature": <temperature>,
615
                "max_token": <max_token>
616
            },
617
            "messages": [
618
                {"role": "system", "content": <system_prompt>},
619
                {"role": "user", "content": <user_prompt>}
620
            ]
621
        }
622
        
623
        for endpoints end_with `/generate` like 'http://localhost:11434/api/generate':
624
        {
625
            "model": <model_name>,
626
            "stream": false,
627
            "think": false
628
            "options": {
629
                "temperature": <temperature>,
630
                "max_token": <max_token>
631
            },
632
            "system": <system_prompt>,
633
            "prompt": <user_prompt>
634
        }
635
        */
636
637
        // For Ollama, only the prompt section ("system" + "prompt" or "role" + "content") is affected by the endpoint;
638
        // The rest remains identical.
639
2
        doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator), allocator);
640
2
        doc.AddMember("stream", false, allocator);
641
2
        doc.AddMember("think", false, allocator);
642
643
        // option section
644
2
        rapidjson::Value options(rapidjson::kObjectType);
645
2
        if (_config.temperature != -1) {
646
2
            options.AddMember("temperature", _config.temperature, allocator);
647
2
        }
648
2
        if (_config.max_tokens != -1) {
649
2
            options.AddMember("max_token", _config.max_tokens, allocator);
650
2
        }
651
2
        doc.AddMember("options", options, allocator);
652
653
        // prompt section
654
2
        if (_config.endpoint.ends_with("chat")) {
655
1
            rapidjson::Value messages(rapidjson::kArrayType);
656
1
            if (system_prompt && *system_prompt) {
657
1
                rapidjson::Value sys_msg(rapidjson::kObjectType);
658
1
                sys_msg.AddMember("role", "system", allocator);
659
1
                sys_msg.AddMember("content", rapidjson::Value(system_prompt, allocator), allocator);
660
1
                messages.PushBack(sys_msg, allocator);
661
1
            }
662
1
            for (const auto& input : inputs) {
663
1
                rapidjson::Value message(rapidjson::kObjectType);
664
1
                message.AddMember("role", "user", allocator);
665
1
                message.AddMember("content", rapidjson::Value(input.c_str(), allocator), allocator);
666
1
                messages.PushBack(message, allocator);
667
1
            }
668
1
            doc.AddMember("messages", messages, allocator);
669
1
        } else {
670
1
            if (system_prompt && *system_prompt) {
671
1
                doc.AddMember("system", rapidjson::Value(system_prompt, allocator), allocator);
672
1
            }
673
1
            doc.AddMember("prompt", rapidjson::Value(inputs[0].c_str(), allocator), allocator);
674
1
        }
675
676
2
        return Status::OK();
677
2
    }
678
679
    Status build_default_request(rapidjson::Document& doc,
680
                                 rapidjson::Document::AllocatorType& allocator,
681
                                 const std::vector<std::string>& inputs,
682
1
                                 const char* const system_prompt, std::string& request_body) const {
683
        /*
684
        Default format(OpenAI-compatible):
685
        {
686
            "model": <model_name>,
687
            "temperature": <temperature>,
688
            "max_tokens": <max_tokens>,
689
            "messages": [
690
                {"role": "system", "content": <system_prompt>},
691
                {"role": "user", "content": <user_prompt>}
692
            ]
693
        }
694
        */
695
696
1
        doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator), allocator);
697
698
        // If 'temperature' and 'max_tokens' are set, add them to the request body.
699
1
        if (_config.temperature != -1) {
700
1
            doc.AddMember("temperature", _config.temperature, allocator);
701
1
        }
702
1
        if (_config.max_tokens != -1) {
703
1
            doc.AddMember("max_tokens", _config.max_tokens, allocator);
704
1
        }
705
706
1
        rapidjson::Value messages(rapidjson::kArrayType);
707
1
        if (system_prompt && *system_prompt) {
708
1
            rapidjson::Value sys_msg(rapidjson::kObjectType);
709
1
            sys_msg.AddMember("role", "system", allocator);
710
1
            sys_msg.AddMember("content", rapidjson::Value(system_prompt, allocator), allocator);
711
1
            messages.PushBack(sys_msg, allocator);
712
1
        }
713
1
        for (const auto& input : inputs) {
714
1
            rapidjson::Value message(rapidjson::kObjectType);
715
1
            message.AddMember("role", "user", allocator);
716
1
            message.AddMember("content", rapidjson::Value(input.c_str(), allocator), allocator);
717
1
            messages.PushBack(message, allocator);
718
1
        }
719
1
        doc.AddMember("messages", messages, allocator);
720
1
        return Status::OK();
721
1
    }
722
};
723
724
// The OpenAI API format can be reused with some compatible AIs.
725
class OpenAIAdapter : public VoyageAIAdapter {
726
public:
727
13
    Status set_authentication(HttpClient* client) const override {
728
13
        client->set_header(HttpHeaders::AUTHORIZATION, "Bearer " + _config.api_key);
729
13
        client->set_content_type("application/json");
730
731
13
        return Status::OK();
732
13
    }
733
734
    Status build_request_payload(const std::vector<std::string>& inputs,
735
                                 const char* const system_prompt,
736
6
                                 std::string& request_body) const override {
737
6
        rapidjson::Document doc;
738
6
        doc.SetObject();
739
6
        auto& allocator = doc.GetAllocator();
740
741
6
        if (_config.endpoint.ends_with("responses")) {
742
            /*{
743
              "model": "gpt-4.1-mini",
744
              "input": [
745
                {"role": "system", "content": "system_prompt here"},
746
                {"role": "user", "content": "xxx"}
747
              ],
748
              "temperature": 0.7,
749
              "max_output_tokens": 150
750
            }*/
751
3
            doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator),
752
3
                          allocator);
753
754
            // If 'temperature' and 'max_tokens' are set, add them to the request body.
755
3
            if (_config.temperature != -1) {
756
3
                doc.AddMember("temperature", _config.temperature, allocator);
757
3
            }
758
3
            if (_config.max_tokens != -1) {
759
3
                doc.AddMember("max_output_tokens", _config.max_tokens, allocator);
760
3
            }
761
762
            // input
763
3
            rapidjson::Value input(rapidjson::kArrayType);
764
3
            if (system_prompt && *system_prompt) {
765
3
                rapidjson::Value sys_msg(rapidjson::kObjectType);
766
3
                sys_msg.AddMember("role", "system", allocator);
767
3
                sys_msg.AddMember("content", rapidjson::Value(system_prompt, allocator), allocator);
768
3
                input.PushBack(sys_msg, allocator);
769
3
            }
770
3
            for (const auto& msg : inputs) {
771
3
                rapidjson::Value message(rapidjson::kObjectType);
772
3
                message.AddMember("role", "user", allocator);
773
3
                message.AddMember("content", rapidjson::Value(msg.c_str(), allocator), allocator);
774
3
                input.PushBack(message, allocator);
775
3
            }
776
3
            doc.AddMember("input", input, allocator);
777
3
        } else {
778
            /*{
779
              "model": "gpt-4",
780
              "messages": [
781
                {"role": "system", "content": "system_prompt here"},
782
                {"role": "user", "content": "xxx"}
783
              ],
784
              "temperature": x,
785
              "max_tokens": x,
786
            }*/
787
3
            doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator),
788
3
                          allocator);
789
790
            // If 'temperature' and 'max_tokens' are set, add them to the request body.
791
3
            if (_config.temperature != -1) {
792
3
                doc.AddMember("temperature", _config.temperature, allocator);
793
3
            }
794
3
            if (_config.max_tokens != -1) {
795
3
                doc.AddMember("max_tokens", _config.max_tokens, allocator);
796
3
            }
797
798
3
            rapidjson::Value messages(rapidjson::kArrayType);
799
3
            if (system_prompt && *system_prompt) {
800
3
                rapidjson::Value sys_msg(rapidjson::kObjectType);
801
3
                sys_msg.AddMember("role", "system", allocator);
802
3
                sys_msg.AddMember("content", rapidjson::Value(system_prompt, allocator), allocator);
803
3
                messages.PushBack(sys_msg, allocator);
804
3
            }
805
3
            for (const auto& input : inputs) {
806
3
                rapidjson::Value message(rapidjson::kObjectType);
807
3
                message.AddMember("role", "user", allocator);
808
3
                message.AddMember("content", rapidjson::Value(input.c_str(), allocator), allocator);
809
3
                messages.PushBack(message, allocator);
810
3
            }
811
3
            doc.AddMember("messages", messages, allocator);
812
3
        }
813
814
6
        rapidjson::StringBuffer buffer;
815
6
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
816
6
        doc.Accept(writer);
817
6
        request_body = buffer.GetString();
818
819
6
        return Status::OK();
820
6
    }
821
822
    Status parse_response(const std::string& response_body, std::vector<std::string>& results,
823
18
                          bool expand_batch = true) const override {
824
18
        rapidjson::Document doc;
825
18
        doc.Parse(response_body.c_str());
826
827
18
        if (doc.HasParseError() || !doc.IsObject()) {
828
1
            return Status::InternalError("Failed to parse {} response: {}", _config.provider_type,
829
1
                                         response_body);
830
1
        }
831
832
17
        const bool is_responses_response =
833
17
                doc.HasMember("output") ||
834
17
                (doc.HasMember("object") && doc["object"].IsString() &&
835
8
                 std::string_view(doc["object"].GetString(), doc["object"].GetStringLength()) ==
836
0
                         "response");
837
17
        if (is_responses_response) {
838
            /// for responses endpoint
839
            /*{
840
              "output": [
841
                {
842
                  "id": "rs_123",
843
                  "type": "reasoning",
844
                  "content": [],
845
                  "summary": []
846
                },
847
                {
848
                  "id": "msg_123",
849
                  "type": "message",
850
                  "role": "assistant",
851
                  "content": [
852
                    {
853
                      "type": "output_text",
854
                      "text": "result text here"   <- result
855
                    }
856
                  ]
857
                }
858
              ]
859
            }*/
860
9
            if (doc.HasMember("status")) {
861
9
                if (!doc["status"].IsString()) {
862
0
                    return Status::InternalError("Invalid status in {} response: {}",
863
0
                                                 _config.provider_type, response_body);
864
0
                }
865
9
                if (std::string_view(doc["status"].GetString(), doc["status"].GetStringLength()) !=
866
9
                    "completed") {
867
2
                    return Status::InternalError("{} response is not completed: {}",
868
2
                                                 _config.provider_type, response_body);
869
2
                }
870
9
            }
871
872
7
            if (!doc.HasMember("output") || !doc["output"].IsArray()) {
873
0
                return Status::InternalError("Invalid output format in {} response: {}",
874
0
                                             _config.provider_type, response_body);
875
0
            }
876
877
7
            const auto& output = doc["output"];
878
7
            std::string response_text;
879
7
            bool has_output_text = false;
880
881
17
            for (rapidjson::SizeType i = 0; i < output.Size(); i++) {
882
10
                const auto& item = output[i];
883
10
                if (!item.IsObject() || !item.HasMember("type") || !item["type"].IsString()) {
884
0
                    return Status::InternalError("Invalid output format in {} response: {}",
885
0
                                                 _config.provider_type, response_body);
886
0
                }
887
888
                // Responses output is heterogeneous. Reasoning and tool items are not final text.
889
10
                if (std::string_view(item["type"].GetString(), item["type"].GetStringLength()) !=
890
10
                    "message") {
891
3
                    continue;
892
3
                }
893
894
7
                if (!item.HasMember("content") || !item["content"].IsArray()) {
895
0
                    return Status::InternalError("Invalid output format in {} response: {}",
896
0
                                                 _config.provider_type, response_body);
897
0
                }
898
899
7
                const auto& content = item["content"];
900
16
                for (rapidjson::SizeType j = 0; j < content.Size(); j++) {
901
9
                    const auto& part = content[j];
902
9
                    if (!part.IsObject() || !part.HasMember("type") || !part["type"].IsString()) {
903
0
                        return Status::InternalError("Invalid output format in {} response: {}",
904
0
                                                     _config.provider_type, response_body);
905
0
                    }
906
907
9
                    if (std::string_view(part["type"].GetString(),
908
9
                                         part["type"].GetStringLength()) != "output_text") {
909
0
                        continue;
910
0
                    }
911
912
9
                    if (!part.HasMember("text") || !part["text"].IsString()) {
913
0
                        return Status::InternalError("Invalid output format in {} response: {}",
914
0
                                                     _config.provider_type, response_body);
915
0
                    }
916
917
9
                    has_output_text = true;
918
9
                    response_text.append(part["text"].GetString(), part["text"].GetStringLength());
919
9
                }
920
7
            }
921
922
7
            if (!has_output_text) {
923
1
                return Status::InternalError("No output text in {} response: {}",
924
1
                                             _config.provider_type, response_body);
925
1
            }
926
6
            RETURN_IF_ERROR(append_parsed_text_result(response_text, results, expand_batch));
927
8
        } else if (doc.HasMember("choices") && doc["choices"].IsArray()) {
928
            /// for completions endpoint
929
            /*{
930
              "object": "chat.completion",
931
              "model": "gpt-4",
932
              "choices": [
933
                {
934
                  ...
935
                  "message": {
936
                    "role": "assistant",
937
                    "content": "xxx"      <- result
938
                  },
939
                  ...
940
                }
941
              ],
942
              ...
943
            }*/
944
7
            const auto& choices = doc["choices"];
945
7
            results.reserve(choices.Size());
946
947
12
            for (rapidjson::SizeType i = 0; i < choices.Size(); i++) {
948
7
                if (!choices[i].HasMember("message") ||
949
7
                    !choices[i]["message"].HasMember("content") ||
950
7
                    !choices[i]["message"]["content"].IsString()) {
951
2
                    return Status::InternalError("Invalid choice format in {} response: {}",
952
2
                                                 _config.provider_type, response_body);
953
2
                }
954
955
5
                RETURN_IF_ERROR(append_parsed_text_result(
956
5
                        choices[i]["message"]["content"].GetString(), results, expand_batch));
957
5
            }
958
7
        } else {
959
1
            return Status::InternalError("Invalid {} response format: {}", _config.provider_type,
960
1
                                         response_body);
961
1
        }
962
963
11
        return Status::OK();
964
17
    }
965
966
    Status build_multimodal_embedding_request(
967
            const std::vector<MultimodalType>& /*media_types*/,
968
            const std::vector<std::string>& /*media_urls*/,
969
            const std::vector<std::string>& /*media_content_types*/,
970
1
            std::string& /*request_body*/) const override {
971
1
        return Status::NotSupported("{} does not support multimodal Embed feature.",
972
1
                                    _config.provider_type);
973
1
    }
974
975
protected:
976
2
    bool supports_dimension_param(const std::string& model_name) const override {
977
2
        return !(model_name == "text-embedding-ada-002");
978
2
    }
979
980
2
    std::string get_dimension_param_name() const override { return "dimensions"; }
981
};
982
983
class DeepSeekAdapter : public OpenAIAdapter {
984
public:
985
    Status build_embedding_request(const std::vector<std::string>& inputs,
986
1
                                   std::string& request_body) const override {
987
1
        return embed_not_supported_status();
988
1
    }
989
990
    Status parse_embedding_response(const std::string& response_body,
991
1
                                    std::vector<std::vector<float>>& results) const override {
992
1
        return embed_not_supported_status();
993
1
    }
994
};
995
996
class MoonShotAdapter : public OpenAIAdapter {
997
public:
998
    Status build_embedding_request(const std::vector<std::string>& inputs,
999
1
                                   std::string& request_body) const override {
1000
1
        return embed_not_supported_status();
1001
1
    }
1002
1003
    Status parse_embedding_response(const std::string& response_body,
1004
1
                                    std::vector<std::vector<float>>& results) const override {
1005
1
        return embed_not_supported_status();
1006
1
    }
1007
};
1008
1009
class MinimaxAdapter : public OpenAIAdapter {
1010
public:
1011
    Status build_embedding_request(const std::vector<std::string>& inputs,
1012
1
                                   std::string& request_body) const override {
1013
1
        rapidjson::Document doc;
1014
1
        doc.SetObject();
1015
1
        auto& allocator = doc.GetAllocator();
1016
1017
        /*{
1018
          "text": ["xxx", "xxx", ...],
1019
          "model": "embo-1",
1020
          "type": "db"
1021
        }*/
1022
1
        rapidjson::Value texts(rapidjson::kArrayType);
1023
1
        for (const auto& input : inputs) {
1024
1
            texts.PushBack(rapidjson::Value(input.c_str(), allocator), allocator);
1025
1
        }
1026
1
        doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator), allocator);
1027
1
        doc.AddMember("texts", texts, allocator);
1028
1
        doc.AddMember("type", rapidjson::Value("db", allocator), allocator);
1029
1030
1
        rapidjson::StringBuffer buffer;
1031
1
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
1032
1
        doc.Accept(writer);
1033
1
        request_body = buffer.GetString();
1034
1035
1
        return Status::OK();
1036
1
    }
1037
};
1038
1039
class ZhipuAdapter : public OpenAIAdapter {
1040
protected:
1041
2
    bool supports_dimension_param(const std::string& model_name) const override {
1042
2
        return !(model_name == "embedding-2");
1043
2
    }
1044
};
1045
1046
class QwenAdapter : public OpenAIAdapter {
1047
public:
1048
    Status build_multimodal_embedding_request(
1049
            const std::vector<MultimodalType>& media_types,
1050
            const std::vector<std::string>& media_urls,
1051
            const std::vector<std::string>& /*media_content_types*/,
1052
4
            std::string& request_body) const override {
1053
4
        RETURN_IF_ERROR(validate_multimodal_embedding_inputs(
1054
4
                "QWEN", media_types, media_urls, {MultimodalType::IMAGE, MultimodalType::VIDEO}));
1055
1056
3
        rapidjson::Document doc;
1057
3
        doc.SetObject();
1058
3
        auto& allocator = doc.GetAllocator();
1059
1060
        /*{
1061
            "model": "tongyi-embedding-vision-plus",
1062
            "input": {
1063
              "contents": [
1064
                {"image": "<url>"},
1065
                {"video": "<url>"}
1066
              ]
1067
            }
1068
            "parameters": {
1069
              "dimension": 512
1070
            }
1071
        }*/
1072
3
        doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator), allocator);
1073
3
        rapidjson::Value input(rapidjson::kObjectType);
1074
3
        rapidjson::Value contents(rapidjson::kArrayType);
1075
1076
7
        for (size_t i = 0; i < media_urls.size(); ++i) {
1077
4
            rapidjson::Value media_item(rapidjson::kObjectType);
1078
4
            if (media_types[i] == MultimodalType::IMAGE) {
1079
2
                media_item.AddMember("image", rapidjson::Value(media_urls[i].c_str(), allocator),
1080
2
                                     allocator);
1081
2
            } else {
1082
2
                media_item.AddMember("video", rapidjson::Value(media_urls[i].c_str(), allocator),
1083
2
                                     allocator);
1084
2
            }
1085
4
            contents.PushBack(media_item, allocator);
1086
4
        }
1087
1088
3
        input.AddMember("contents", contents, allocator);
1089
3
        doc.AddMember("input", input, allocator);
1090
3
        if (_config.dimensions != -1 && supports_dimension_param(_config.model_name)) {
1091
3
            rapidjson::Value parameters(rapidjson::kObjectType);
1092
3
            std::string param_name = get_dimension_param_name();
1093
3
            rapidjson::Value dimension_name(param_name.c_str(), allocator);
1094
3
            parameters.AddMember(dimension_name, _config.dimensions, allocator);
1095
3
            doc.AddMember("parameters", parameters, allocator);
1096
3
        }
1097
1098
3
        rapidjson::StringBuffer buffer;
1099
3
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
1100
3
        doc.Accept(writer);
1101
3
        request_body = buffer.GetString();
1102
3
        return Status::OK();
1103
4
    }
1104
1105
    Status parse_embedding_response(const std::string& response_body,
1106
0
                                    std::vector<std::vector<float>>& results) const override {
1107
0
        rapidjson::Document doc;
1108
0
        doc.Parse(response_body.c_str());
1109
1110
0
        if (doc.HasParseError() || !doc.IsObject()) [[unlikely]] {
1111
0
            return Status::InternalError("Failed to parse {} response: {}", _config.provider_type,
1112
0
                                         response_body);
1113
0
        }
1114
        // Qwen multimodal embedding usually returns:
1115
        // {
1116
        //   "output": {
1117
        //     "embeddings": [
1118
        //       {"index":0, "embedding":[...], "type":"image|video|text"},
1119
        //       ...
1120
        //     ]
1121
        //   }
1122
        // }
1123
        //
1124
        // In text-only or compatibility endpoints, Qwen may also return OpenAI-style
1125
        // "data":[{"embedding":[...]}]. For compatibility we first parse native
1126
        // output.embeddings and then fallback to OpenAIAdapter parser.
1127
0
        if (doc.HasMember("output") && doc["output"].IsObject() &&
1128
0
            doc["output"].HasMember("embeddings") && doc["output"]["embeddings"].IsArray()) {
1129
0
            const auto& embeddings = doc["output"]["embeddings"];
1130
0
            results.reserve(embeddings.Size());
1131
0
            for (rapidjson::SizeType i = 0; i < embeddings.Size(); i++) {
1132
0
                if (!embeddings[i].HasMember("embedding") ||
1133
0
                    !embeddings[i]["embedding"].IsArray()) {
1134
0
                    return Status::InternalError("Invalid {} response format: {}",
1135
0
                                                 _config.provider_type, response_body);
1136
0
                }
1137
0
                std::transform(embeddings[i]["embedding"].Begin(), embeddings[i]["embedding"].End(),
1138
0
                               std::back_inserter(results.emplace_back()),
1139
0
                               [](const auto& val) { return val.GetFloat(); });
1140
0
            }
1141
0
            return Status::OK();
1142
0
        }
1143
0
        return OpenAIAdapter::parse_embedding_response(response_body, results);
1144
0
    }
1145
1146
protected:
1147
5
    bool supports_dimension_param(const std::string& model_name) const override {
1148
5
        static const std::unordered_set<std::string> no_dimension_models = {
1149
5
                "text-embedding-v1", "text-embedding-v2", "text2vec", "m3e-base", "m3e-small"};
1150
5
        return !no_dimension_models.contains(model_name);
1151
5
    }
1152
1153
4
    std::string get_dimension_param_name() const override { return "dimension"; }
1154
};
1155
1156
class JinaAdapter : public VoyageAIAdapter {
1157
public:
1158
    Status build_multimodal_embedding_request(
1159
            const std::vector<MultimodalType>& media_types,
1160
            const std::vector<std::string>& media_urls,
1161
            const std::vector<std::string>& /*media_content_types*/,
1162
2
            std::string& request_body) const override {
1163
2
        RETURN_IF_ERROR(validate_multimodal_embedding_inputs(
1164
2
                "JINA", media_types, media_urls, {MultimodalType::IMAGE, MultimodalType::VIDEO}));
1165
1166
2
        rapidjson::Document doc;
1167
2
        doc.SetObject();
1168
2
        auto& allocator = doc.GetAllocator();
1169
1170
        /*{
1171
            "model": "jina-embeddings-v4",
1172
            "task": "text-matching",
1173
            "input": [
1174
              {"image": "<url>"},
1175
              {"video": "<url>"}
1176
            ]
1177
        }*/
1178
2
        doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator), allocator);
1179
2
        doc.AddMember("task", "text-matching", allocator);
1180
1181
2
        rapidjson::Value input(rapidjson::kArrayType);
1182
5
        for (size_t i = 0; i < media_urls.size(); ++i) {
1183
3
            rapidjson::Value media_item(rapidjson::kObjectType);
1184
3
            if (media_types[i] == MultimodalType::IMAGE) {
1185
2
                media_item.AddMember("image", rapidjson::Value(media_urls[i].c_str(), allocator),
1186
2
                                     allocator);
1187
2
            } else {
1188
1
                media_item.AddMember("video", rapidjson::Value(media_urls[i].c_str(), allocator),
1189
1
                                     allocator);
1190
1
            }
1191
3
            input.PushBack(media_item, allocator);
1192
3
        }
1193
2
        if (_config.dimensions != -1 && supports_dimension_param(_config.model_name)) {
1194
2
            doc.AddMember("dimensions", _config.dimensions, allocator);
1195
2
        }
1196
2
        doc.AddMember("input", input, allocator);
1197
1198
2
        rapidjson::StringBuffer buffer;
1199
2
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
1200
2
        doc.Accept(writer);
1201
2
        request_body = buffer.GetString();
1202
2
        return Status::OK();
1203
2
    }
1204
};
1205
1206
class BaichuanAdapter : public OpenAIAdapter {
1207
protected:
1208
0
    bool supports_dimension_param(const std::string& model_name) const override { return false; }
1209
};
1210
1211
// Gemini's embedding format is different from VoyageAI, so it requires a separate adapter
1212
class GeminiAdapter : public AIAdapter {
1213
public:
1214
2
    Status set_authentication(HttpClient* client) const override {
1215
2
        client->set_header("x-goog-api-key", _config.api_key);
1216
2
        client->set_content_type("application/json");
1217
2
        return Status::OK();
1218
2
    }
1219
1220
    Status build_request_payload(const std::vector<std::string>& inputs,
1221
                                 const char* const system_prompt,
1222
1
                                 std::string& request_body) const override {
1223
1
        rapidjson::Document doc;
1224
1
        doc.SetObject();
1225
1
        auto& allocator = doc.GetAllocator();
1226
1227
        /*{
1228
          "systemInstruction": {
1229
              "parts": [
1230
                {
1231
                  "text": "system_prompt here"
1232
                }
1233
              ]
1234
            }
1235
          ],
1236
          "contents": [
1237
            {
1238
              "parts": [
1239
                {
1240
                  "text": "xxx"
1241
                }
1242
              ]
1243
            }
1244
          ],
1245
          "generationConfig": {
1246
          "temperature": 0.7,
1247
          "maxOutputTokens": 1024
1248
          }
1249
1250
        }*/
1251
1
        if (system_prompt && *system_prompt) {
1252
1
            rapidjson::Value system_instruction(rapidjson::kObjectType);
1253
1
            rapidjson::Value parts(rapidjson::kArrayType);
1254
1255
1
            rapidjson::Value part(rapidjson::kObjectType);
1256
1
            part.AddMember("text", rapidjson::Value(system_prompt, allocator), allocator);
1257
1
            parts.PushBack(part, allocator);
1258
            // system_instruction.PushBack(content, allocator);
1259
1
            system_instruction.AddMember("parts", parts, allocator);
1260
1
            doc.AddMember("systemInstruction", system_instruction, allocator);
1261
1
        }
1262
1263
1
        rapidjson::Value contents(rapidjson::kArrayType);
1264
1
        for (const auto& input : inputs) {
1265
1
            rapidjson::Value content(rapidjson::kObjectType);
1266
1
            rapidjson::Value parts(rapidjson::kArrayType);
1267
1268
1
            rapidjson::Value part(rapidjson::kObjectType);
1269
1
            part.AddMember("text", rapidjson::Value(input.c_str(), allocator), allocator);
1270
1271
1
            parts.PushBack(part, allocator);
1272
1
            content.AddMember("parts", parts, allocator);
1273
1
            contents.PushBack(content, allocator);
1274
1
        }
1275
1
        doc.AddMember("contents", contents, allocator);
1276
1277
        // If 'temperature' and 'max_tokens' are set, add them to the request body.
1278
1
        rapidjson::Value generationConfig(rapidjson::kObjectType);
1279
1
        if (_config.temperature != -1) {
1280
1
            generationConfig.AddMember("temperature", _config.temperature, allocator);
1281
1
        }
1282
1
        if (_config.max_tokens != -1) {
1283
1
            generationConfig.AddMember("maxOutputTokens", _config.max_tokens, allocator);
1284
1
        }
1285
1
        doc.AddMember("generationConfig", generationConfig, allocator);
1286
1287
1
        rapidjson::StringBuffer buffer;
1288
1
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
1289
1
        doc.Accept(writer);
1290
1
        request_body = buffer.GetString();
1291
1292
1
        return Status::OK();
1293
1
    }
1294
1295
    Status parse_response(const std::string& response_body, std::vector<std::string>& results,
1296
4
                          bool expand_batch = true) const override {
1297
4
        rapidjson::Document doc;
1298
4
        doc.Parse(response_body.c_str());
1299
1300
4
        if (doc.HasParseError() || !doc.IsObject()) {
1301
1
            return Status::InternalError("Failed to parse {} response: {}", _config.provider_type,
1302
1
                                         response_body);
1303
1
        }
1304
3
        if (!doc.HasMember("candidates") || !doc["candidates"].IsArray()) {
1305
1
            return Status::InternalError("Invalid {} response format: {}", _config.provider_type,
1306
1
                                         response_body);
1307
1
        }
1308
1309
        /*{
1310
          "candidates":[
1311
            {
1312
              "content": {
1313
                "parts": [
1314
                  {
1315
                    "text": "xxx"
1316
                  }
1317
                ]
1318
              }
1319
            }
1320
          ]
1321
        }*/
1322
2
        const auto& candidates = doc["candidates"];
1323
2
        results.reserve(candidates.Size());
1324
1325
4
        for (rapidjson::SizeType i = 0; i < candidates.Size(); i++) {
1326
2
            if (!candidates[i].HasMember("content") ||
1327
2
                !candidates[i]["content"].HasMember("parts") ||
1328
2
                !candidates[i]["content"]["parts"].IsArray() ||
1329
2
                candidates[i]["content"]["parts"].Empty() ||
1330
2
                !candidates[i]["content"]["parts"][0].HasMember("text") ||
1331
2
                !candidates[i]["content"]["parts"][0]["text"].IsString()) {
1332
0
                return Status::InternalError("Invalid candidate format in {} response",
1333
0
                                             _config.provider_type);
1334
0
            }
1335
1336
2
            RETURN_IF_ERROR(append_parsed_text_result(
1337
2
                    candidates[i]["content"]["parts"][0]["text"].GetString(), results,
1338
2
                    expand_batch));
1339
2
        }
1340
2
        return Status::OK();
1341
2
    }
1342
1343
    Status build_embedding_request(const std::vector<std::string>& inputs,
1344
2
                                   std::string& request_body) const override {
1345
2
        rapidjson::Document doc;
1346
2
        doc.SetObject();
1347
2
        auto& allocator = doc.GetAllocator();
1348
1349
        /*{
1350
          "requests": [
1351
            {
1352
              "model": "models/gemini-embedding-001",
1353
              "content": {
1354
                "parts": [
1355
                  {
1356
                    "text": "xxx"
1357
                  }
1358
                ]
1359
              },
1360
              "outputDimensionality": 1024
1361
            },
1362
            {
1363
              "model": "models/gemini-embedding-001",
1364
              "content": {
1365
                "parts": [
1366
                  {
1367
                    "text": "yyy"
1368
                  }
1369
                ]
1370
              },
1371
              "outputDimensionality": 1024
1372
            }
1373
          ]
1374
        }*/
1375
1376
        // gemini requires the model format as `models/{model}`
1377
2
        std::string model_name = _config.model_name;
1378
2
        if (!model_name.starts_with("models/")) {
1379
2
            model_name = "models/" + model_name;
1380
2
        }
1381
1382
2
        rapidjson::Value requests(rapidjson::kArrayType);
1383
4
        for (const auto& input : inputs) {
1384
4
            rapidjson::Value request(rapidjson::kObjectType);
1385
4
            request.AddMember("model", rapidjson::Value(model_name.c_str(), allocator), allocator);
1386
4
            add_dimension_params(request, allocator);
1387
1388
4
            rapidjson::Value content(rapidjson::kObjectType);
1389
4
            rapidjson::Value parts(rapidjson::kArrayType);
1390
4
            rapidjson::Value part(rapidjson::kObjectType);
1391
4
            part.AddMember("text", rapidjson::Value(input.c_str(), allocator), allocator);
1392
4
            parts.PushBack(part, allocator);
1393
4
            content.AddMember("parts", parts, allocator);
1394
4
            request.AddMember("content", content, allocator);
1395
4
            requests.PushBack(request, allocator);
1396
4
        }
1397
2
        doc.AddMember("requests", requests, allocator);
1398
1399
2
        rapidjson::StringBuffer buffer;
1400
2
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
1401
2
        doc.Accept(writer);
1402
2
        request_body = buffer.GetString();
1403
1404
2
        return Status::OK();
1405
2
    }
1406
1407
    Status build_multimodal_embedding_request(const std::vector<MultimodalType>& media_types,
1408
                                              const std::vector<std::string>& media_urls,
1409
                                              const std::vector<std::string>& media_content_types,
1410
8
                                              std::string& request_body) const override {
1411
8
        RETURN_IF_ERROR(validate_multimodal_embedding_inputs(
1412
8
                "Gemini", media_types, media_urls,
1413
8
                {MultimodalType::IMAGE, MultimodalType::AUDIO, MultimodalType::VIDEO}));
1414
6
        if (media_content_types.size() != media_urls.size()) {
1415
1
            return Status::InvalidArgument(
1416
1
                    "Gemini multimodal embed input size mismatch, media_content_types={}, "
1417
1
                    "media_urls={}",
1418
1
                    media_content_types.size(), media_urls.size());
1419
1
        }
1420
1421
5
        rapidjson::Document doc;
1422
5
        doc.SetObject();
1423
5
        auto& allocator = doc.GetAllocator();
1424
1425
        /*{
1426
          "requests": [
1427
            {
1428
              "model": "models/gemini-embedding-2-preview",
1429
              "content": {
1430
                "parts": [
1431
                  {"file_data": {"mime_type": "<original content_type>", "file_uri": "<url>"}}
1432
                ]
1433
              },
1434
              "outputDimensionality": 768
1435
            },
1436
            {
1437
              "model": "models/gemini-embedding-2-preview",
1438
              "content": {
1439
                "parts": [
1440
                  {"file_data": {"mime_type": "<original content_type>", "file_uri": "<url>"}}
1441
                ]
1442
              },
1443
              "outputDimensionality": 768
1444
            }
1445
          ]
1446
        }*/
1447
5
        std::string model_name = _config.model_name;
1448
5
        if (!model_name.starts_with("models/")) {
1449
5
            model_name = "models/" + model_name;
1450
5
        }
1451
1452
5
        rapidjson::Value requests(rapidjson::kArrayType);
1453
12
        for (size_t i = 0; i < media_urls.size(); ++i) {
1454
7
            rapidjson::Value request(rapidjson::kObjectType);
1455
7
            request.AddMember("model", rapidjson::Value(model_name.c_str(), allocator), allocator);
1456
7
            add_dimension_params(request, allocator);
1457
1458
7
            rapidjson::Value content(rapidjson::kObjectType);
1459
7
            rapidjson::Value parts(rapidjson::kArrayType);
1460
7
            rapidjson::Value part(rapidjson::kObjectType);
1461
7
            rapidjson::Value file_data(rapidjson::kObjectType);
1462
7
            file_data.AddMember("mime_type",
1463
7
                                rapidjson::Value(media_content_types[i].c_str(), allocator),
1464
7
                                allocator);
1465
7
            file_data.AddMember("file_uri", rapidjson::Value(media_urls[i].c_str(), allocator),
1466
7
                                allocator);
1467
7
            part.AddMember("file_data", file_data, allocator);
1468
7
            parts.PushBack(part, allocator);
1469
7
            content.AddMember("parts", parts, allocator);
1470
7
            request.AddMember("content", content, allocator);
1471
7
            requests.PushBack(request, allocator);
1472
7
        }
1473
5
        doc.AddMember("requests", requests, allocator);
1474
1475
5
        rapidjson::StringBuffer buffer;
1476
5
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
1477
5
        doc.Accept(writer);
1478
5
        request_body = buffer.GetString();
1479
5
        return Status::OK();
1480
6
    }
1481
1482
    Status parse_embedding_response(const std::string& response_body,
1483
3
                                    std::vector<std::vector<float>>& results) const override {
1484
3
        rapidjson::Document doc;
1485
3
        doc.Parse(response_body.c_str());
1486
1487
3
        if (doc.HasParseError() || !doc.IsObject()) {
1488
0
            return Status::InternalError("Failed to parse {} response: {}", _config.provider_type,
1489
0
                                         response_body);
1490
0
        }
1491
3
        if (doc.HasMember("embeddings") && doc["embeddings"].IsArray()) {
1492
            /*{
1493
              "embeddings": [
1494
                {"values": [0.1, 0.2, 0.3]},
1495
                {"values": [0.4, 0.5, 0.6]}
1496
              ]
1497
            }*/
1498
2
            const auto& embeddings = doc["embeddings"];
1499
2
            results.reserve(embeddings.Size());
1500
6
            for (rapidjson::SizeType i = 0; i < embeddings.Size(); i++) {
1501
4
                if (!embeddings[i].HasMember("values") || !embeddings[i]["values"].IsArray()) {
1502
0
                    return Status::InternalError("Invalid {} response format: {}",
1503
0
                                                 _config.provider_type, response_body);
1504
0
                }
1505
4
                std::transform(embeddings[i]["values"].Begin(), embeddings[i]["values"].End(),
1506
4
                               std::back_inserter(results.emplace_back()),
1507
10
                               [](const auto& val) { return val.GetFloat(); });
1508
4
            }
1509
2
            return Status::OK();
1510
2
        }
1511
1
        if (!doc.HasMember("embedding") || !doc["embedding"].IsObject()) {
1512
0
            return Status::InternalError("Invalid {} response format: {}", _config.provider_type,
1513
0
                                         response_body);
1514
0
        }
1515
1516
        /*{
1517
          "embedding":{
1518
            "values": [0.1, 0.2, 0.3]
1519
          }
1520
        }*/
1521
1
        const auto& embedding = doc["embedding"];
1522
1
        if (!embedding.HasMember("values") || !embedding["values"].IsArray()) {
1523
0
            return Status::InternalError("Invalid {} response format: {}", _config.provider_type,
1524
0
                                         response_body);
1525
0
        }
1526
1
        std::transform(embedding["values"].Begin(), embedding["values"].End(),
1527
1
                       std::back_inserter(results.emplace_back()),
1528
3
                       [](const auto& val) { return val.GetFloat(); });
1529
1530
1
        return Status::OK();
1531
1
    }
1532
1533
protected:
1534
11
    bool supports_dimension_param(const std::string& model_name) const override {
1535
11
        static const std::unordered_set<std::string> no_dimension_models = {"models/embedding-001",
1536
11
                                                                            "embedding-001"};
1537
11
        return !no_dimension_models.contains(model_name);
1538
11
    }
1539
1540
9
    std::string get_dimension_param_name() const override { return "outputDimensionality"; }
1541
};
1542
1543
class AnthropicAdapter : public VoyageAIAdapter {
1544
public:
1545
1
    Status set_authentication(HttpClient* client) const override {
1546
1
        client->set_header("x-api-key", _config.api_key);
1547
1
        client->set_header("anthropic-version", _config.anthropic_version);
1548
1
        client->set_content_type("application/json");
1549
1550
1
        return Status::OK();
1551
1
    }
1552
1553
    Status build_request_payload(const std::vector<std::string>& inputs,
1554
                                 const char* const system_prompt,
1555
1
                                 std::string& request_body) const override {
1556
1
        rapidjson::Document doc;
1557
1
        doc.SetObject();
1558
1
        auto& allocator = doc.GetAllocator();
1559
1560
        /*
1561
            "model": "claude-opus-4-1-20250805",
1562
            "max_tokens": 1024,
1563
            "system": "system_prompt here",
1564
            "messages": [
1565
              {"role": "user", "content": "xxx"}
1566
            ],
1567
            "temperature": 0.7
1568
        */
1569
1570
        // If 'temperature' and 'max_tokens' are set, add them to the request body.
1571
1
        doc.AddMember("model", rapidjson::Value(_config.model_name.c_str(), allocator), allocator);
1572
1
        if (_config.temperature != -1) {
1573
1
            doc.AddMember("temperature", _config.temperature, allocator);
1574
1
        }
1575
1
        if (_config.max_tokens != -1) {
1576
1
            doc.AddMember("max_tokens", _config.max_tokens, allocator);
1577
1
        } else {
1578
            // Keep the default value, Anthropic requires this parameter
1579
0
            doc.AddMember("max_tokens", 2048, allocator);
1580
0
        }
1581
1
        if (system_prompt && *system_prompt) {
1582
1
            doc.AddMember("system", rapidjson::Value(system_prompt, allocator), allocator);
1583
1
        }
1584
1585
1
        rapidjson::Value messages(rapidjson::kArrayType);
1586
1
        for (const auto& input : inputs) {
1587
1
            rapidjson::Value message(rapidjson::kObjectType);
1588
1
            message.AddMember("role", "user", allocator);
1589
1
            message.AddMember("content", rapidjson::Value(input.c_str(), allocator), allocator);
1590
1
            messages.PushBack(message, allocator);
1591
1
        }
1592
1
        doc.AddMember("messages", messages, allocator);
1593
1594
1
        rapidjson::StringBuffer buffer;
1595
1
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
1596
1
        doc.Accept(writer);
1597
1
        request_body = buffer.GetString();
1598
1599
1
        return Status::OK();
1600
1
    }
1601
1602
    Status parse_response(const std::string& response_body, std::vector<std::string>& results,
1603
4
                          bool expand_batch = true) const override {
1604
4
        rapidjson::Document doc;
1605
4
        doc.Parse(response_body.c_str());
1606
4
        if (doc.HasParseError() || !doc.IsObject()) {
1607
1
            return Status::InternalError("Failed to parse {} response: {}", _config.provider_type,
1608
1
                                         response_body);
1609
1
        }
1610
3
        if (!doc.HasMember("content") || !doc["content"].IsArray()) {
1611
1
            return Status::InternalError("Invalid {} response format: {}", _config.provider_type,
1612
1
                                         response_body);
1613
1
        }
1614
1615
        /*{
1616
            "content": [
1617
              {
1618
                "text": "xxx",
1619
                "type": "text"
1620
              }
1621
            ]
1622
        }*/
1623
2
        const auto& content = doc["content"];
1624
2
        results.reserve(1);
1625
1626
2
        std::string result;
1627
4
        for (rapidjson::SizeType i = 0; i < content.Size(); i++) {
1628
2
            if (!content[i].HasMember("type") || !content[i]["type"].IsString() ||
1629
2
                !content[i].HasMember("text") || !content[i]["text"].IsString()) {
1630
0
                continue;
1631
0
            }
1632
1633
2
            if (std::string(content[i]["type"].GetString()) == "text") {
1634
2
                if (!result.empty()) {
1635
0
                    result += "\n";
1636
0
                }
1637
2
                result += content[i]["text"].GetString();
1638
2
            }
1639
2
        }
1640
1641
2
        return append_parsed_text_result(result, results, expand_batch);
1642
3
    }
1643
};
1644
1645
// Mock adapter used only for UT to bypass real HTTP calls and return deterministic data.
1646
class MockAdapter : public AIAdapter {
1647
public:
1648
#ifdef BE_TEST
1649
    static void clear_embedding_inputs_for_test() { _embedding_inputs_for_test().clear(); }
1650
1651
    static const std::vector<std::string>& get_embedding_inputs_for_test() {
1652
        return _embedding_inputs_for_test();
1653
    }
1654
#endif
1655
1656
0
    Status set_authentication(HttpClient* client) const override { return Status::OK(); }
1657
1658
    Status build_request_payload(const std::vector<std::string>& inputs,
1659
                                 const char* const system_prompt,
1660
3
                                 std::string& request_body) const override {
1661
3
        return Status::OK();
1662
3
    }
1663
1664
    Status parse_response(const std::string& response_body, std::vector<std::string>& results,
1665
82
                          bool expand_batch = true) const override {
1666
82
        return append_parsed_text_result(response_body, results, expand_batch);
1667
82
    }
1668
1669
    Status build_embedding_request(const std::vector<std::string>& inputs,
1670
6
                                   std::string& request_body) const override {
1671
#ifdef BE_TEST
1672
        auto& embedding_inputs = _embedding_inputs_for_test();
1673
        embedding_inputs.insert(embedding_inputs.end(), inputs.begin(), inputs.end());
1674
#endif
1675
6
        return Status::OK();
1676
6
    }
1677
1678
    Status build_multimodal_embedding_request(
1679
            const std::vector<MultimodalType>& /*media_types*/,
1680
            const std::vector<std::string>& /*media_urls*/,
1681
            const std::vector<std::string>& /*media_content_types*/,
1682
3
            std::string& /*request_body*/) const override {
1683
3
        return Status::OK();
1684
3
    }
1685
1686
    Status parse_embedding_response(const std::string& response_body,
1687
0
                                    std::vector<std::vector<float>>& results) const override {
1688
0
        rapidjson::Document doc;
1689
0
        doc.SetObject();
1690
0
        doc.Parse(response_body.c_str());
1691
0
        if (doc.HasParseError() || !doc.IsObject()) {
1692
0
            return Status::InternalError("Failed to parse embedding response");
1693
0
        }
1694
0
        if (!doc.HasMember("embedding") || !doc["embedding"].IsArray()) {
1695
0
            return Status::InternalError("Invalid embedding response format");
1696
0
        }
1697
1698
0
        results.reserve(1);
1699
0
        std::transform(doc["embedding"].Begin(), doc["embedding"].End(),
1700
0
                       std::back_inserter(results.emplace_back()),
1701
0
                       [](const auto& val) { return val.GetFloat(); });
1702
0
        return Status::OK();
1703
0
    }
1704
1705
private:
1706
#ifdef BE_TEST
1707
    static std::vector<std::string>& _embedding_inputs_for_test() {
1708
        static thread_local std::vector<std::string> embedding_inputs;
1709
        return embedding_inputs;
1710
    }
1711
#endif
1712
};
1713
1714
class AIAdapterFactory {
1715
public:
1716
120
    static std::shared_ptr<AIAdapter> create_adapter(const std::string& provider_type) {
1717
120
        static const std::unordered_map<std::string, std::function<std::shared_ptr<AIAdapter>()>>
1718
120
                adapters = {{"LOCAL", []() { return std::make_shared<LocalAdapter>(); }},
1719
120
                            {"OPENAI", []() { return std::make_shared<OpenAIAdapter>(); }},
1720
120
                            {"MOONSHOT", []() { return std::make_shared<MoonShotAdapter>(); }},
1721
120
                            {"DEEPSEEK", []() { return std::make_shared<DeepSeekAdapter>(); }},
1722
120
                            {"MINIMAX", []() { return std::make_shared<MinimaxAdapter>(); }},
1723
120
                            {"ZHIPU", []() { return std::make_shared<ZhipuAdapter>(); }},
1724
120
                            {"QWEN", []() { return std::make_shared<QwenAdapter>(); }},
1725
120
                            {"JINA", []() { return std::make_shared<JinaAdapter>(); }},
1726
120
                            {"BAICHUAN", []() { return std::make_shared<BaichuanAdapter>(); }},
1727
120
                            {"ANTHROPIC", []() { return std::make_shared<AnthropicAdapter>(); }},
1728
120
                            {"GEMINI", []() { return std::make_shared<GeminiAdapter>(); }},
1729
120
                            {"VOYAGEAI", []() { return std::make_shared<VoyageAIAdapter>(); }},
1730
120
                            {"MOCK", []() { return std::make_shared<MockAdapter>(); }}};
1731
1732
120
        auto it = adapters.find(provider_type);
1733
120
        return (it != adapters.end()) ? it->second() : nullptr;
1734
120
    }
1735
};
1736
1737
} // namespace doris