Coverage Report

Created: 2026-09-22 20:38

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