This is an automated email from the ASF dual-hosted git repository.
Mryange pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 8dd6aafade8 [Fix](ai_func) Parse final text from Responses API output
(#67671)
8dd6aafade8 is described below
commit 8dd6aafade836c862730d66449c5e3a70c6f8f69
Author: linrrarity <[email protected]>
AuthorDate: Thu Sep 10 14:39:15 2026 +0800
[Fix](ai_func) Parse final text from Responses API output (#67671)
### What problem does this PR solve?
Issue Number: close #xxx
Related PR: #xxx
Problem Summary:
The Responses API returns heterogeneous items in the `output` array.
Besides the final `message`, it may contain `reasoning` and tool-related
items.
The previous OpenAI adapter treated every output item as response text.
This caused two problems:
1. An OpenAI-type reasoning item with an empty `content` array was
reported as an invalid response.
2. An OpenAI-type `reasoning_text` item was incorrectly included in
batch results, causing errors such as `expected 1 items but got 2`.
This change updates the Responses API parser to:
- Ignore non-`message` output items, including reasoning and tool items.
- Only extract `output_text` parts from message content.
- Preserve validation for malformed message and output-text structures.
- Update the response format comments to match the current API
structure.
for example:
```sql
CREATE RESOURCE 'deepseek-responses'
PROPERTIES (
'type'='ai',
'ai.provider_type'='deepseek',
'ai.endpoint'='https://api.deepseek.com/responses',
'ai.model_name' = 'deepseek-v4-flash',
'ai.api_key' = 'sk-xxx'
);
SELECT id, ai_TRANSLATE('deepseek-responses', str_val, tar_language) AS
Result
FROM ai_test WHERE str_val IS NOT NULL;
```
before:
```text
Doris> SELECT id, ai_TRANSLATE('deepseek-responses', str_val, tar_language)
AS Result
-> FROM ai_test WHERE str_val IS NOT NULL;
ERROR 1105 (HY000): Exception, msg: (127.0.0.1)[RUNTIME_ERROR]Failed to
parse ai_translate batch result, expected 1 items but got 2
```
now
```text
+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id | Result
|
+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 4 | Satz 4, Apache Doris ist ein MPP-basiertes
Echtzeit-Data-Warehouse, das für seine hohe Abfragegeschwindigkeit bekannt ist.
|
| 3 | phrase 3, Apache Doris est un entrepôt de données en temps réel
basé sur MPP, connu pour sa vitesse de requête élevée.
|
| 6 | предложение 6, Apache Doris — это хранилище данных реального
времени на основе MPP, известное высокой скоростью выполнения запросов.
|
| 2 | 句子2:Apache Doris 是一个基于 MPP 的实时数据仓库,以高查询速度著称。
|
| 5 | 文5、Apache DorisはMPPベースのリアルタイムデータウェアハウスであり、高速なクエリ処理で知られています。
|
| 8 | Frase 8, Apache Doris é um data warehouse em tempo real baseado em
MPP, conhecido por sua alta velocidade de consulta.
|
| 9 | 문장 9, Apache Doris는 높은 쿼리 속도로 알려진 MPP 기반 실시간 데이터 웨어하우스입니다.
|
| 1 | sentence 1, Apache Doris is an MPP-based real-time data warehouse
known for its high query speed.
|
| 7 | oración 7, Apache Doris es un almacén de datos en tiempo real
basado en MPP conocido por su alta velocidad de consulta.
|
| 10 | الجملة 10، Apache Doris هو مستودع بيانات في الوقت الفعلي يعتمد على
MPP ويُعرف بسرعته العالية في معالجة الاستعلامات.
|
+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
```
### Release note
Fix parsing of OpenAI-compatible Responses API results containing
reasoning output.
---
be/src/exprs/aggregate/aggregate_function_ai_agg.h | 12 +-
be/src/exprs/function/ai/ai_adapter.h | 141 ++++++++---
be/test/ai/aggregate_function_ai_agg_test.cpp | 45 +++-
be/test/ai/ai_adapter_test.cpp | 263 ++++++++++++++++++++-
4 files changed, 422 insertions(+), 39 deletions(-)
diff --git a/be/src/exprs/aggregate/aggregate_function_ai_agg.h
b/be/src/exprs/aggregate/aggregate_function_ai_agg.h
index cc0d67f9821..d3da899fca8 100644
--- a/be/src/exprs/aggregate/aggregate_function_ai_agg.h
+++ b/be/src/exprs/aggregate/aggregate_function_ai_agg.h
@@ -19,6 +19,7 @@
#include <gen_cpp/PaloInternalService_types.h>
+#include <cstdlib>
#include <memory>
#include "common/status.h"
@@ -118,9 +119,13 @@ public:
THROW_IF_ERROR(
_ai_adapter->build_request_payload(inputs,
system_prompt.c_str(), request_body));
THROW_IF_ERROR(send_request_to_ai(request_body, response));
- THROW_IF_ERROR(_ai_adapter->parse_response(response, results));
+ THROW_IF_ERROR(_ai_adapter->parse_response(response, results, false /*
expand_batch */));
- return results[0];
+ if (results.size() != 1) [[unlikely]] {
+ throw Exception(ErrorCode::INTERNAL_ERROR,
+ "AI aggregate expected one result but got {}",
results.size());
+ }
+ return results.front();
}
// init task and ai related parameters
@@ -159,7 +164,8 @@ private:
Status send_request_to_ai(const std::string& request_body, std::string&
response) const {
// Mock path for testing
#ifdef BE_TEST
- response = "this is a mock response";
+ const char* test_result = std::getenv("AI_TEST_RESULT");
+ response = test_result != nullptr ? test_result : "this is a mock
response";
return Status::OK();
#endif
diff --git a/be/src/exprs/function/ai/ai_adapter.h
b/be/src/exprs/function/ai/ai_adapter.h
index 39f16e73420..b6376c6c004 100644
--- a/be/src/exprs/function/ai/ai_adapter.h
+++ b/be/src/exprs/function/ai/ai_adapter.h
@@ -24,6 +24,7 @@
#include <cctype>
#include <memory>
#include <string>
+#include <string_view>
#include <unordered_map>
#include <vector>
@@ -133,7 +134,8 @@ public:
// Parse response from AI service and extract generated text results
virtual Status parse_response(const std::string& response_body,
- std::vector<std::string>& results) const {
+ std::vector<std::string>& results,
+ bool /* expand_batch */ = true) const {
return Status::NotSupported("{} don't support text generation",
_config.provider_type);
}
@@ -171,8 +173,15 @@ protected:
// Example:
// provider response -> choices[0].message.content = "[\"1\",\"0\",\"1\"]"
// this helper -> appends "1", "0", "1" into `results`
+ // Set expand_batch to false when AI_AGG needs the complete generated text
as one result.
static Status append_parsed_text_result(std::string_view text,
- std::vector<std::string>& results)
{
+ std::vector<std::string>& results,
+ bool expand_batch = true) {
+ if (!expand_batch) {
+ results.emplace_back(text.data(), text.size());
+ return Status::OK();
+ }
+
size_t begin = 0;
size_t end = text.size();
while (begin < end && std::isspace(static_cast<unsigned
char>(text[begin]))) {
@@ -455,8 +464,8 @@ public:
return Status::OK();
}
- Status parse_response(const std::string& response_body,
- std::vector<std::string>& results) const override {
+ Status parse_response(const std::string& response_body,
std::vector<std::string>& results,
+ bool expand_batch = true) const override {
rapidjson::Document doc;
doc.Parse(response_body.c_str());
@@ -475,26 +484,29 @@ public:
if (choices[i].HasMember("message") &&
choices[i]["message"].HasMember("content") &&
choices[i]["message"]["content"].IsString()) {
RETURN_IF_ERROR(append_parsed_text_result(
- choices[i]["message"]["content"].GetString(),
results));
+ choices[i]["message"]["content"].GetString(),
results, expand_batch));
} else if (choices[i].HasMember("text") &&
choices[i]["text"].IsString()) {
// Some local LLMs use a simpler format
- RETURN_IF_ERROR(
-
append_parsed_text_result(choices[i]["text"].GetString(), results));
+
RETURN_IF_ERROR(append_parsed_text_result(choices[i]["text"].GetString(),
+ results,
expand_batch));
}
}
} else if (doc.HasMember("text") && doc["text"].IsString()) {
// Format 2: Simple response with just "text" or "content" field
- RETURN_IF_ERROR(append_parsed_text_result(doc["text"].GetString(),
results));
+ RETURN_IF_ERROR(
+ append_parsed_text_result(doc["text"].GetString(),
results, expand_batch));
} else if (doc.HasMember("content") && doc["content"].IsString()) {
-
RETURN_IF_ERROR(append_parsed_text_result(doc["content"].GetString(), results));
+ RETURN_IF_ERROR(
+ append_parsed_text_result(doc["content"].GetString(),
results, expand_batch));
} else if (doc.HasMember("response") && doc["response"].IsString()) {
// Format 3: Response field (Ollama `generate` format)
-
RETURN_IF_ERROR(append_parsed_text_result(doc["response"].GetString(),
results));
+ RETURN_IF_ERROR(
+ append_parsed_text_result(doc["response"].GetString(),
results, expand_batch));
} else if (doc.HasMember("message") && doc["message"].IsObject() &&
doc["message"].HasMember("content") &&
doc["message"]["content"].IsString()) {
// Format 4: message/content field (Ollama `chat` format)
- RETURN_IF_ERROR(
-
append_parsed_text_result(doc["message"]["content"].GetString(), results));
+
RETURN_IF_ERROR(append_parsed_text_result(doc["message"]["content"].GetString(),
+ results, expand_batch));
} else {
return Status::NotSupported("Unsupported response format from
local AI.");
}
@@ -807,8 +819,8 @@ public:
return Status::OK();
}
- Status parse_response(const std::string& response_body,
- std::vector<std::string>& results) const override {
+ Status parse_response(const std::string& response_body,
std::vector<std::string>& results,
+ bool expand_batch = true) const override {
rapidjson::Document doc;
doc.Parse(response_body.c_str());
@@ -817,37 +829,101 @@ public:
response_body);
}
- if (doc.HasMember("output") && doc["output"].IsArray()) {
+ const bool is_responses_response =
+ doc.HasMember("output") ||
+ (doc.HasMember("object") && doc["object"].IsString() &&
+ std::string_view(doc["object"].GetString(),
doc["object"].GetStringLength()) ==
+ "response");
+ if (is_responses_response) {
/// for responses endpoint
/*{
"output": [
+ {
+ "id": "rs_123",
+ "type": "reasoning",
+ "content": [],
+ "summary": []
+ },
{
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{
- "type": "text",
+ "type": "output_text",
"text": "result text here" <- result
}
]
}
]
}*/
+ if (doc.HasMember("status")) {
+ if (!doc["status"].IsString()) {
+ return Status::InternalError("Invalid status in {}
response: {}",
+ _config.provider_type,
response_body);
+ }
+ if (std::string_view(doc["status"].GetString(),
doc["status"].GetStringLength()) !=
+ "completed") {
+ return Status::InternalError("{} response is not
completed: {}",
+ _config.provider_type,
response_body);
+ }
+ }
+
+ if (!doc.HasMember("output") || !doc["output"].IsArray()) {
+ return Status::InternalError("Invalid output format in {}
response: {}",
+ _config.provider_type,
response_body);
+ }
+
const auto& output = doc["output"];
- results.reserve(output.Size());
+ std::string response_text;
+ bool has_output_text = false;
for (rapidjson::SizeType i = 0; i < output.Size(); i++) {
- if (!output[i].HasMember("content") ||
!output[i]["content"].IsArray() ||
- output[i]["content"].Empty() ||
!output[i]["content"][0].HasMember("text") ||
- !output[i]["content"][0]["text"].IsString()) {
+ const auto& item = output[i];
+ if (!item.IsObject() || !item.HasMember("type") ||
!item["type"].IsString()) {
return Status::InternalError("Invalid output format in {}
response: {}",
_config.provider_type,
response_body);
}
- RETURN_IF_ERROR(append_parsed_text_result(
- output[i]["content"][0]["text"].GetString(), results));
+ // Responses output is heterogeneous. Reasoning and tool items
are not final text.
+ if (std::string_view(item["type"].GetString(),
item["type"].GetStringLength()) !=
+ "message") {
+ continue;
+ }
+
+ if (!item.HasMember("content") || !item["content"].IsArray()) {
+ return Status::InternalError("Invalid output format in {}
response: {}",
+ _config.provider_type,
response_body);
+ }
+
+ const auto& content = item["content"];
+ for (rapidjson::SizeType j = 0; j < content.Size(); j++) {
+ const auto& part = content[j];
+ if (!part.IsObject() || !part.HasMember("type") ||
!part["type"].IsString()) {
+ return Status::InternalError("Invalid output format in
{} response: {}",
+ _config.provider_type,
response_body);
+ }
+
+ if (std::string_view(part["type"].GetString(),
+ part["type"].GetStringLength()) !=
"output_text") {
+ continue;
+ }
+
+ if (!part.HasMember("text") || !part["text"].IsString()) {
+ return Status::InternalError("Invalid output format in
{} response: {}",
+ _config.provider_type,
response_body);
+ }
+
+ has_output_text = true;
+ response_text.append(part["text"].GetString(),
part["text"].GetStringLength());
+ }
+ }
+
+ if (!has_output_text) {
+ return Status::InternalError("No output text in {} response:
{}",
+ _config.provider_type,
response_body);
}
+ RETURN_IF_ERROR(append_parsed_text_result(response_text, results,
expand_batch));
} else if (doc.HasMember("choices") && doc["choices"].IsArray()) {
/// for completions endpoint
/*{
@@ -877,7 +953,7 @@ public:
}
RETURN_IF_ERROR(append_parsed_text_result(
- choices[i]["message"]["content"].GetString(),
results));
+ choices[i]["message"]["content"].GetString(), results,
expand_batch));
}
} else {
return Status::InternalError("Invalid {} response format: {}",
_config.provider_type,
@@ -1216,8 +1292,8 @@ public:
return Status::OK();
}
- Status parse_response(const std::string& response_body,
- std::vector<std::string>& results) const override {
+ Status parse_response(const std::string& response_body,
std::vector<std::string>& results,
+ bool expand_batch = true) const override {
rapidjson::Document doc;
doc.Parse(response_body.c_str());
@@ -1258,7 +1334,8 @@ public:
}
RETURN_IF_ERROR(append_parsed_text_result(
- candidates[i]["content"]["parts"][0]["text"].GetString(),
results));
+ candidates[i]["content"]["parts"][0]["text"].GetString(),
results,
+ expand_batch));
}
return Status::OK();
}
@@ -1522,8 +1599,8 @@ public:
return Status::OK();
}
- Status parse_response(const std::string& response_body,
- std::vector<std::string>& results) const override {
+ Status parse_response(const std::string& response_body,
std::vector<std::string>& results,
+ bool expand_batch = true) const override {
rapidjson::Document doc;
doc.Parse(response_body.c_str());
if (doc.HasParseError() || !doc.IsObject()) {
@@ -1561,7 +1638,7 @@ public:
}
}
- return append_parsed_text_result(result, results);
+ return append_parsed_text_result(result, results, expand_batch);
}
};
@@ -1584,9 +1661,9 @@ public:
return Status::OK();
}
- Status parse_response(const std::string& response_body,
- std::vector<std::string>& results) const override {
- return append_parsed_text_result(response_body, results);
+ Status parse_response(const std::string& response_body,
std::vector<std::string>& results,
+ bool expand_batch = true) const override {
+ return append_parsed_text_result(response_body, results, expand_batch);
}
Status build_embedding_request(const std::vector<std::string>& inputs,
diff --git a/be/test/ai/aggregate_function_ai_agg_test.cpp
b/be/test/ai/aggregate_function_ai_agg_test.cpp
index 4fb9c7969d0..e5cfaa889a2 100644
--- a/be/test/ai/aggregate_function_ai_agg_test.cpp
+++ b/be/test/ai/aggregate_function_ai_agg_test.cpp
@@ -20,8 +20,11 @@
#include <gmock/gmock-matchers.h>
#include <gtest/gtest.h>
+#include <cstdlib>
+#include <map>
#include <memory>
#include <string>
+#include <utility>
#include <vector>
#include "core/arena.h"
@@ -66,7 +69,7 @@ public:
_agg_function->set_query_context(_query_ctx.get());
}
- void TearDown() override {}
+ void TearDown() override { unsetenv("AI_TEST_RESULT"); }
protected:
std::unique_ptr<MockRuntimeState> _runtime_state;
@@ -516,6 +519,46 @@ TEST_F(AggregateFunctionAIAggTest,
mock_resource_send_request_test) {
_agg_function->destroy(place);
}
+TEST_F(AggregateFunctionAIAggTest,
openai_responses_preserves_model_output_as_one_result) {
+ TAIResource ai_resource;
+ ai_resource.provider_type = "OPENAI";
+ ai_resource.model_name = "test_model";
+ ai_resource.endpoint = "https://api.openai.com/v1/responses";
+ _query_ctx->set_ai_resources(
+ std::map<std::string, TAIResource> {{"openai_response",
ai_resource}});
+
+ const std::vector<std::pair<std::string, std::string>> test_cases = {
+
{R"({"object":"response","status":"completed","output":[{"type":"message","content":[{"type":"output_text","text":"[\"north\",\"south\"]"}]}]})",
+ R"(["north","south"])"},
+
{R"({"object":"response","status":"completed","output":[{"type":"message","content":[{"type":"output_text","text":"[]"}]}]})",
+ "[]"}};
+
+ for (const auto& [response, expected] : test_cases) {
+ setenv("AI_TEST_RESULT", response.c_str(), 1);
+
+ auto resource_col = ColumnString::create();
+ auto text_col = ColumnString::create();
+ auto task_col = ColumnString::create();
+ resource_col->insert_data("openai_response", 15);
+ text_col->insert_data("test input", 10);
+ task_col->insert_data("summarize", 9);
+
+ std::unique_ptr<char[]> memory(new
char[_agg_function->size_of_data()]);
+ AggregateDataPtr place = memory.get();
+ _agg_function->create(place);
+
+ const IColumn* columns[3] = {resource_col.get(), text_col.get(),
task_col.get()};
+ _agg_function->add(place, columns, 0, _arena);
+
+ ColumnString result_column;
+ _agg_function->insert_result_into(place, result_column);
+ StringRef result_ref = result_column.get_data_at(0);
+ EXPECT_EQ(std::string(result_ref.data, result_ref.size), expected);
+
+ _agg_function->destroy(place);
+ }
+}
+
TEST_F(AggregateFunctionAIAggTest, missing_ai_resources_metadata_test) {
auto empty_query_ctx = MockQueryContext::create();
_agg_function->set_query_context(empty_query_ctx.get());
diff --git a/be/test/ai/ai_adapter_test.cpp b/be/test/ai/ai_adapter_test.cpp
index 2cfda9d7154..da40ef217dc 100644
--- a/be/test/ai/ai_adapter_test.cpp
+++ b/be/test/ai/ai_adapter_test.cpp
@@ -381,16 +381,273 @@ TEST(AI_ADAPTER_TEST, openai_adatper_responses_request) {
ASSERT_STREQ(input[1]["content"].GetString(), inputs[0].c_str());
}
-TEST(AI_ADAPTER_TEST, openai_adapter_responses_parse_response) {
+TEST(AI_ADAPTER_TEST,
openai_adapter_responses_parse_response_skips_empty_reasoning) {
OpenAIAdapter adapter;
- std::string resp = R"({"output":[{"content":[{"text":"openai response
result"}]}]})";
+ std::string resp = R"({
+ "id": "resp_123",
+ "object": "response",
+ "status": "completed",
+ "output": [
+ {
+ "id": "rs_123",
+ "type": "reasoning",
+ "content": [],
+ "summary": []
+ },
+ {
+ "id": "msg_123",
+ "type": "message",
+ "status": "completed",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "openai response result",
+ "annotations": []
+ }
+ ]
+ }
+ ]
+ })";
std::vector<std::string> results;
Status st = adapter.parse_response(resp, results);
- ASSERT_TRUE(st.ok());
+ ASSERT_TRUE(st.ok()) << st.to_string();
ASSERT_EQ(results.size(), 1);
ASSERT_EQ(results[0], "openai response result");
}
+TEST(AI_ADAPTER_TEST,
openai_adapter_responses_parse_response_skips_reasoning_text) {
+ OpenAIAdapter adapter;
+ std::string resp = R"({
+ "id": "resp_456",
+ "object": "response",
+ "status": "completed",
+ "output": [
+ {
+ "id": "rs_456",
+ "type": "reasoning",
+ "status": "completed",
+ "content": [
+ {
+ "type": "reasoning_text",
+ "text": "The model reasoning must not become a batch
result."
+ }
+ ],
+ "summary": []
+ },
+ {
+ "id": "msg_456",
+ "type": "message",
+ "status": "completed",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "[\"translation one\",\"translation two\"]",
+ "annotations": []
+ }
+ ]
+ }
+ ]
+ })";
+ std::vector<std::string> results;
+ Status st = adapter.parse_response(resp, results);
+ ASSERT_TRUE(st.ok()) << st.to_string();
+ ASSERT_EQ(results.size(), 2);
+ EXPECT_EQ(results[0], "translation one");
+ EXPECT_EQ(results[1], "translation two");
+}
+
+TEST(AI_ADAPTER_TEST,
openai_adapter_responses_parse_response_rejects_incomplete_status) {
+ OpenAIAdapter adapter;
+ std::string resp = R"({
+ "id": "resp_incomplete",
+ "object": "response",
+ "status": "incomplete",
+ "incomplete_details": {
+ "reason": "max_output_tokens"
+ },
+ "output": [
+ {
+ "id": "msg_incomplete",
+ "type": "message",
+ "status": "incomplete",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "[\"truncated result\"",
+ "annotations": []
+ }
+ ]
+ }
+ ]
+ })";
+ std::vector<std::string> results;
+ Status st = adapter.parse_response(resp, results);
+ ASSERT_FALSE(st.ok());
+ EXPECT_THAT(st.to_string(), ::testing::HasSubstr("incomplete"));
+ EXPECT_THAT(st.to_string(), ::testing::HasSubstr("max_output_tokens"));
+ EXPECT_TRUE(results.empty());
+}
+
+TEST(AI_ADAPTER_TEST,
openai_adapter_responses_status_cannot_fall_back_to_choices) {
+ OpenAIAdapter adapter;
+ std::string resp = R"({
+ "object": "response",
+ "status": "incomplete",
+ "incomplete_details": {
+ "reason": "max_output_tokens"
+ },
+ "output": null,
+ "choices": [
+ {
+ "message": {
+ "content": "[\"partial result\"]"
+ }
+ }
+ ]
+ })";
+ std::vector<std::string> results;
+ Status st = adapter.parse_response(resp, results);
+ ASSERT_FALSE(st.ok());
+ EXPECT_THAT(st.to_string(), ::testing::HasSubstr("incomplete"));
+ EXPECT_THAT(st.to_string(), ::testing::HasSubstr("max_output_tokens"));
+ EXPECT_TRUE(results.empty());
+}
+
+TEST(AI_ADAPTER_TEST,
openai_adapter_responses_parse_response_rejects_missing_final_text) {
+ OpenAIAdapter adapter;
+ std::string resp = R"({
+ "id": "resp_no_text",
+ "object": "response",
+ "status": "completed",
+ "output": [
+ {
+ "id": "rs_no_text",
+ "type": "reasoning",
+ "status": "completed",
+ "content": [],
+ "summary": []
+ }
+ ]
+ })";
+ std::vector<std::string> results;
+ Status st = adapter.parse_response(resp, results);
+ ASSERT_FALSE(st.ok());
+ EXPECT_TRUE(results.empty());
+}
+
+TEST(AI_ADAPTER_TEST,
openai_adapter_responses_parse_response_joins_output_text_before_parsing) {
+ OpenAIAdapter adapter;
+ std::string resp = R"({
+ "id": "resp_split_text",
+ "object": "response",
+ "status": "completed",
+ "output": [
+ {
+ "id": "msg_split_text_1",
+ "type": "message",
+ "status": "completed",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "[\"translation",
+ "annotations": []
+ },
+ {
+ "type": "output_text",
+ "text": " one\",",
+ "annotations": []
+ }
+ ]
+ },
+ {
+ "id": "msg_split_text_2",
+ "type": "message",
+ "status": "completed",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "\"translation two\"]",
+ "annotations": []
+ }
+ ]
+ }
+ ]
+ })";
+ std::vector<std::string> results;
+ Status st = adapter.parse_response(resp, results);
+ ASSERT_TRUE(st.ok()) << st.to_string();
+ ASSERT_EQ(results.size(), 2);
+ EXPECT_EQ(results[0], "translation one");
+ EXPECT_EQ(results[1], "translation two");
+}
+
+TEST(AI_ADAPTER_TEST,
openai_adapter_responses_parse_response_keeps_json_array_in_opaque_mode) {
+ OpenAIAdapter adapter;
+ std::string resp = R"({
+ "id": "resp_opaque",
+ "object": "response",
+ "status": "completed",
+ "output": [
+ {
+ "id": "msg_opaque",
+ "type": "message",
+ "status": "completed",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "[\"north\",",
+ "annotations": []
+ },
+ {
+ "type": "output_text",
+ "text": "\"south\"]",
+ "annotations": []
+ }
+ ]
+ }
+ ]
+ })";
+ std::vector<std::string> results;
+ Status st = adapter.parse_response(resp, results, false);
+ ASSERT_TRUE(st.ok()) << st.to_string();
+ ASSERT_EQ(results.size(), 1);
+ EXPECT_EQ(results[0], R"(["north","south"])");
+}
+
+TEST(AI_ADAPTER_TEST, non_openai_text_adapters_keep_json_array_in_opaque_mode)
{
+ auto check_opaque_mode = [](const char* provider, AIAdapter& adapter,
+ const std::string& response) {
+ SCOPED_TRACE(provider);
+ std::vector<std::string> results;
+ Status st = adapter.parse_response(response, results, false);
+ ASSERT_TRUE(st.ok()) << st.to_string();
+ ASSERT_EQ(results.size(), 1);
+ EXPECT_EQ(results[0], R"(["north","south"])");
+ };
+
+ LocalAdapter local_adapter;
+ check_opaque_mode("local", local_adapter,
+
R"({"choices":[{"message":{"content":"[\"north\",\"south\"]"}}]})");
+
+ GeminiAdapter gemini_adapter;
+ check_opaque_mode(
+ "gemini", gemini_adapter,
+
R"({"candidates":[{"content":{"parts":[{"text":"[\"north\",\"south\"]"}]}}]})");
+
+ AnthropicAdapter anthropic_adapter;
+ check_opaque_mode("anthropic", anthropic_adapter,
+
R"({"content":[{"type":"text","text":"[\"north\",\"south\"]"}]})");
+
+ MockAdapter mock_adapter;
+ check_opaque_mode("mock", mock_adapter, R"(["north","south"])");
+}
+
TEST(AI_ADAPTER_TEST, openai_adapter_parse_response_keeps_mask_literals) {
OpenAIAdapter adapter;
std::string resp = R"({"choices":[{"message":{"content":"[MSKED]"}}]})";
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]