Coverage Report

Created: 2026-09-20 17:15

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