Coverage Report

Created: 2026-09-10 01:11

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