Coverage Report

Created: 2026-10-15 23:39

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