diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeChatFormatter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeChatFormatter.java
index b84e739b9..8c244c537 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeChatFormatter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeChatFormatter.java
@@ -16,6 +16,7 @@
package io.agentscope.core.formatter.dashscope;
import io.agentscope.core.formatter.AbstractBaseFormatter;
+import io.agentscope.core.formatter.dashscope.dto.DashScopeContentPart;
import io.agentscope.core.formatter.dashscope.dto.DashScopeInput;
import io.agentscope.core.formatter.dashscope.dto.DashScopeMessage;
import io.agentscope.core.formatter.dashscope.dto.DashScopeParameters;
@@ -173,11 +174,15 @@ public DashScopeRequest buildRequest(
}
/**
- * Apply cache control to DashScope messages.
+ * Apply cache control to DashScope messages at the content block level.
*
- *
Adds cache_control: {"type": "ephemeral"} to all system messages and the last
- * message in the list. Messages that already have cache_control set (e.g., via manual metadata
- * marking) will not be overwritten.
+ *
Per the DashScope API specification, {@code cache_control} must be placed inside content
+ * blocks (within the {@code content} array), not at the message level. This method converts
+ * string content to array format when needed and sets {@code cache_control} on the last content
+ * block of each target message.
+ *
+ *
Target messages: all system messages and the last message in the list. Messages whose last
+ * content block already has {@code cache_control} set will not be overwritten.
*
* @param messages the list of formatted DashScope messages
*/
@@ -186,14 +191,48 @@ public void applyCacheControl(List messages) {
return;
}
for (DashScopeMessage msg : messages) {
- if ("system".equals(msg.getRole()) && msg.getCacheControl() == null) {
- msg.setCacheControl(EPHEMERAL_CACHE_CONTROL);
+ if ("system".equals(msg.getRole())) {
+ applyCacheControlToContentBlock(msg);
}
}
DashScopeMessage lastMsg = messages.get(messages.size() - 1);
- if (lastMsg.getCacheControl() == null) {
- lastMsg.setCacheControl(EPHEMERAL_CACHE_CONTROL);
+ applyCacheControlToContentBlock(lastMsg);
+ }
+
+ /**
+ * Apply ephemeral cache_control to the last content block of the given message.
+ * If content is a plain string, it is first converted to array format.
+ * Skips if the last content block already has cache_control set.
+ */
+ static void applyCacheControlToContentBlock(DashScopeMessage msg) {
+ List parts = ensureContentArray(msg);
+ if (parts.isEmpty()) {
+ return;
+ }
+ DashScopeContentPart lastPart = parts.get(parts.size() - 1);
+ if (lastPart.getCacheControl() == null) {
+ lastPart.setCacheControl(EPHEMERAL_CACHE_CONTROL);
+ }
+ }
+
+ /**
+ * Ensure the message content is in array format ({@code List}).
+ * If content is a plain string, converts it to {@code [{"type":"text","text":"..."}]}.
+ *
+ * @return the content part list (never null, may be empty)
+ */
+ @SuppressWarnings("unchecked")
+ static List ensureContentArray(DashScopeMessage msg) {
+ Object content = msg.getContent();
+ if (content instanceof List) {
+ return (List) content;
+ }
+ List parts = new ArrayList<>();
+ if (content instanceof String text) {
+ parts.add(DashScopeContentPart.text(text));
}
+ msg.setContent(parts);
+ return parts;
}
/**
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMessageConverter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMessageConverter.java
index 31a044c5e..f536f72b6 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMessageConverter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMessageConverter.java
@@ -249,7 +249,9 @@ private String extractTextContent(Msg msg) {
}
/**
- * Apply cache_control from Msg metadata to the converted DashScopeMessage.
+ * Apply cache_control from Msg metadata to the converted DashScopeMessage at content block
+ * level. When the metadata flag is set, this converts the message content to array format
+ * (if needed) and sets cache_control on the last content block.
*
* @param msg the source message with metadata
* @param result the converted DashScope message
@@ -260,7 +262,7 @@ private void applyCacheControlFromMetadata(Msg msg, DashScopeMessage result) {
}
Object cacheFlag = msg.getMetadata().get(MessageMetadataKeys.CACHE_CONTROL);
if (Boolean.TRUE.equals(cacheFlag)) {
- result.setCacheControl(DashScopeChatFormatter.getEphemeralCacheControl());
+ DashScopeChatFormatter.applyCacheControlToContentBlock(result);
}
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMultiAgentFormatter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMultiAgentFormatter.java
index 2df49033e..c4a0b3f9a 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMultiAgentFormatter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMultiAgentFormatter.java
@@ -33,7 +33,6 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
/**
* DashScope formatter for multi-agent conversations.
@@ -365,11 +364,14 @@ private static class MessageGroup {
}
/**
- * Apply cache control to DashScope messages.
+ * Apply cache control to DashScope messages at the content block level.
*
- * Adds cache_control: {"type": "ephemeral"} to all system messages and the last
- * message in the list. Messages that already have cache_control set (e.g., via manual metadata
- * marking) will not be overwritten.
+ *
Per the DashScope API specification, {@code cache_control} must be placed inside content
+ * blocks (within the {@code content} array), not at the message level. This method delegates
+ * to {@link DashScopeChatFormatter#applyCacheControlToContentBlock(DashScopeMessage)} for each
+ * target message.
+ *
+ *
Target messages: all system messages and the last message in the list.
*
* @param messages the list of formatted DashScope messages
*/
@@ -377,15 +379,12 @@ public void applyCacheControl(List messages) {
if (messages == null || messages.isEmpty()) {
return;
}
- Map ephemeral = DashScopeChatFormatter.getEphemeralCacheControl();
for (DashScopeMessage msg : messages) {
- if ("system".equals(msg.getRole()) && msg.getCacheControl() == null) {
- msg.setCacheControl(ephemeral);
+ if ("system".equals(msg.getRole())) {
+ DashScopeChatFormatter.applyCacheControlToContentBlock(msg);
}
}
DashScopeMessage lastMsg = messages.get(messages.size() - 1);
- if (lastMsg.getCacheControl() == null) {
- lastMsg.setCacheControl(ephemeral);
- }
+ DashScopeChatFormatter.applyCacheControlToContentBlock(lastMsg);
}
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeContentPart.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeContentPart.java
index d2c7915a9..42fe4f2a0 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeContentPart.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeContentPart.java
@@ -18,6 +18,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
+import java.util.Map;
/**
* DashScope content part DTO for multimodal messages.
@@ -79,6 +80,10 @@ public class DashScopeContentPart {
@JsonProperty("total_pixels")
private Integer totalPixels;
+ /** Cache control configuration for prompt caching (placed at content block level per DashScope API spec). */
+ @JsonProperty("cache_control")
+ private Map cacheControl;
+
public DashScopeContentPart() {}
public String getText() {
@@ -153,6 +158,14 @@ public void setTotalPixels(Integer totalPixels) {
this.totalPixels = totalPixels;
}
+ public Map getCacheControl() {
+ return cacheControl;
+ }
+
+ public void setCacheControl(Map cacheControl) {
+ this.cacheControl = cacheControl;
+ }
+
/**
* Get video as URL string.
*
@@ -290,6 +303,11 @@ public Builder totalPixels(Integer totalPixels) {
return this;
}
+ public Builder cacheControl(Map cacheControl) {
+ part.setCacheControl(cacheControl);
+ return this;
+ }
+
public DashScopeContentPart build() {
return part;
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIBaseFormatter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIBaseFormatter.java
index bf57dd42b..af1a64cf4 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIBaseFormatter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIBaseFormatter.java
@@ -16,6 +16,7 @@
package io.agentscope.core.formatter.openai;
import io.agentscope.core.formatter.AbstractBaseFormatter;
+import io.agentscope.core.formatter.openai.dto.OpenAIContentPart;
import io.agentscope.core.formatter.openai.dto.OpenAIMessage;
import io.agentscope.core.formatter.openai.dto.OpenAIRequest;
import io.agentscope.core.formatter.openai.dto.OpenAIResponse;
@@ -24,6 +25,7 @@
import io.agentscope.core.model.ToolChoice;
import io.agentscope.core.model.ToolSchema;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -170,11 +172,15 @@ public OpenAIRequest buildRequest(
}
/**
- * Apply cache control to OpenAI messages.
+ * Apply cache control to OpenAI messages at the content block level.
*
- * Adds cache_control: {"type": "ephemeral"} to all system messages and the last
- * message in the list. Messages that already have cache_control set (e.g., via manual metadata
- * marking) will not be overwritten.
+ *
Per the DashScope API specification (which also applies to the OpenAI-compatible protocol),
+ * {@code cache_control} must be placed inside content blocks (within the {@code content} array),
+ * not at the message level. This method converts string content to array format when needed and
+ * sets {@code cache_control} on the last content block of each target message.
+ *
+ *
Target messages: all system messages and the last message in the list. Messages whose last
+ * content block already has {@code cache_control} set will not be overwritten.
*
* @param messages the list of formatted OpenAI messages
*/
@@ -183,14 +189,48 @@ public void applyCacheControl(List messages) {
return;
}
for (OpenAIMessage msg : messages) {
- if ("system".equals(msg.getRole()) && msg.getCacheControl() == null) {
- msg.setCacheControl(EPHEMERAL_CACHE_CONTROL);
+ if ("system".equals(msg.getRole())) {
+ applyCacheControlToContentBlock(msg);
}
}
OpenAIMessage lastMsg = messages.get(messages.size() - 1);
- if (lastMsg.getCacheControl() == null) {
- lastMsg.setCacheControl(EPHEMERAL_CACHE_CONTROL);
+ applyCacheControlToContentBlock(lastMsg);
+ }
+
+ /**
+ * Apply ephemeral cache_control to the last content block of the given message.
+ * If content is a plain string, it is first converted to array format.
+ * Skips if the last content block already has cache_control set.
+ */
+ static void applyCacheControlToContentBlock(OpenAIMessage msg) {
+ List parts = ensureContentArray(msg);
+ if (parts.isEmpty()) {
+ return;
+ }
+ OpenAIContentPart lastPart = parts.get(parts.size() - 1);
+ if (lastPart.getCacheControl() == null) {
+ lastPart.setCacheControl(EPHEMERAL_CACHE_CONTROL);
+ }
+ }
+
+ /**
+ * Ensure the message content is in array format ({@code List}).
+ * If content is a plain string, converts it to {@code [{"type":"text","text":"..."}]}.
+ *
+ * @return the content part list (never null, may be empty)
+ */
+ @SuppressWarnings("unchecked")
+ static List ensureContentArray(OpenAIMessage msg) {
+ Object content = msg.getContent();
+ if (content instanceof List) {
+ return (List) content;
+ }
+ List parts = new ArrayList<>();
+ if (content instanceof String text) {
+ parts.add(OpenAIContentPart.text(text));
}
+ msg.setContent(parts);
+ return parts;
}
/**
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIMessageConverter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIMessageConverter.java
index 47f8584fd..86acd09dd 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIMessageConverter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIMessageConverter.java
@@ -469,7 +469,9 @@ private String detectAudioFormat(String mediaType) {
}
/**
- * Apply cache_control from Msg metadata to the converted OpenAIMessage.
+ * Apply cache_control from Msg metadata to the converted OpenAIMessage at content block level.
+ * When the metadata flag is set, this converts the message content to array format (if needed)
+ * and sets cache_control on the last content block.
*
* @param msg the source message with metadata
* @param result the converted OpenAI message
@@ -480,7 +482,7 @@ private void applyCacheControlFromMetadata(Msg msg, OpenAIMessage result) {
}
Object cacheFlag = msg.getMetadata().get(MessageMetadataKeys.CACHE_CONTROL);
if (Boolean.TRUE.equals(cacheFlag)) {
- result.setCacheControl(OpenAIBaseFormatter.getEphemeralCacheControl());
+ OpenAIBaseFormatter.applyCacheControlToContentBlock(result);
}
}
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIContentPart.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIContentPart.java
index 8f898d4d5..ef0dcd719 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIContentPart.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIContentPart.java
@@ -17,6 +17,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
/**
* OpenAI content part DTO for multimodal messages.
@@ -65,6 +66,10 @@ public class OpenAIContentPart {
@JsonProperty("video_url")
private OpenAIVideoUrl videoUrl;
+ /** Cache control configuration for prompt caching (placed at content block level per API spec). */
+ @JsonProperty("cache_control")
+ private Map cacheControl;
+
public OpenAIContentPart() {}
public String getType() {
@@ -107,6 +112,14 @@ public void setVideoUrl(OpenAIVideoUrl videoUrl) {
this.videoUrl = videoUrl;
}
+ public Map getCacheControl() {
+ return cacheControl;
+ }
+
+ public void setCacheControl(Map cacheControl) {
+ this.cacheControl = cacheControl;
+ }
+
/**
* Create a text content part.
*
@@ -206,6 +219,11 @@ public Builder videoUrl(OpenAIVideoUrl videoUrl) {
return this;
}
+ public Builder cacheControl(Map cacheControl) {
+ part.setCacheControl(cacheControl);
+ return this;
+ }
+
public OpenAIContentPart build() {
OpenAIContentPart result = part;
part = new OpenAIContentPart();
diff --git a/agentscope-core/src/test/java/io/agentscope/core/e2e/CacheControlE2ETest.java b/agentscope-core/src/test/java/io/agentscope/core/e2e/CacheControlE2ETest.java
new file mode 100644
index 000000000..a798b4abc
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/e2e/CacheControlE2ETest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.e2e;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import io.agentscope.core.ReActAgent;
+import io.agentscope.core.agent.test.TestUtils;
+import io.agentscope.core.formatter.dashscope.DashScopeChatFormatter;
+import io.agentscope.core.formatter.openai.OpenAIChatFormatter;
+import io.agentscope.core.memory.InMemoryMemory;
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.model.DashScopeChatModel;
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.OpenAIChatModel;
+import io.agentscope.core.tool.Toolkit;
+import java.time.Duration;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * E2E test for DashScope context caching (cache_control at content block level).
+ *
+ * Verifies that the DashScope API accepts requests with cache_control placed at the content
+ * block level, which is the correct placement per API documentation.
+ *
+ *
Two identical requests are made via a ReActAgent with cacheControl enabled. The first creates
+ * the cache, and the second should hit the cache. Both requests must succeed without API errors.
+ *
+ *
Requirements: DASHSCOPE_API_KEY environment variable must be set. Set
+ * ENABLE_E2E_TESTS=true to run this test.
+ */
+@Tag("e2e")
+@ExtendWith(E2ETestCondition.class)
+@DisplayName("Cache Control E2E Test")
+class CacheControlE2ETest {
+
+ private static final Duration TEST_TIMEOUT = Duration.ofSeconds(120);
+
+ /**
+ * A long system prompt to ensure it meets the minimum token length for caching. DashScope
+ * requires at least 1024 tokens for cache creation.
+ */
+ private static final String LONG_SYSTEM_PROMPT =
+ """
+ You are a highly knowledgeable assistant specializing in computer science and software \
+ engineering. You have deep expertise in the following areas:
+
+ 1. Programming Languages: You are proficient in Java, Python, JavaScript, TypeScript, \
+ C++, Rust, Go, and many other languages. You can explain language features, design \
+ patterns, and best practices for each.
+
+ 2. Data Structures and Algorithms: You have comprehensive knowledge of arrays, linked \
+ lists, trees, graphs, hash tables, heaps, and their various implementations. You can \
+ analyze time and space complexity using Big-O notation.
+
+ 3. System Design: You can design scalable, distributed systems including load balancers, \
+ caches, message queues, databases, and microservices architectures. You understand CAP \
+ theorem, consistency models, and distributed consensus protocols.
+
+ 4. Databases: You are familiar with relational databases (MySQL, PostgreSQL), NoSQL \
+ databases (MongoDB, Redis, Cassandra), and NewSQL databases. You understand indexing, \
+ query optimization, sharding, and replication strategies.
+
+ 5. Cloud Computing: You have expertise in AWS, Azure, and GCP services. You understand \
+ containerization with Docker and Kubernetes, serverless computing, and infrastructure \
+ as code with Terraform and CloudFormation.
+
+ 6. Machine Learning and AI: You understand supervised learning, unsupervised learning, \
+ reinforcement learning, neural networks, transformers, and large language models. You \
+ can explain attention mechanisms, backpropagation, and optimization techniques.
+
+ 7. Security: You are knowledgeable about encryption, authentication, authorization, \
+ OAuth, JWT, XSS, CSRF, SQL injection, and other security topics. You understand \
+ secure coding practices and threat modeling.
+
+ 8. DevOps and CI/CD: You understand continuous integration, continuous deployment, \
+ GitOps, monitoring, logging, and observability. You can set up pipelines using \
+ Jenkins, GitHub Actions, GitLab CI, and ArgoCD.
+
+ 9. Software Engineering Practices: You advocate for clean code, SOLID principles, \
+ test-driven development, code reviews, and documentation. You understand agile \
+ methodologies including Scrum and Kanban.
+
+ 10. Networking: You understand TCP/IP, HTTP/HTTPS, WebSockets, gRPC, DNS, CDN, \
+ and network security. You can explain how data flows through the internet stack.
+
+ When answering questions, be concise but thorough. Provide code examples when \
+ appropriate. Always consider edge cases and potential pitfalls. If you are unsure \
+ about something, say so rather than guessing. Cite relevant documentation or \
+ resources when possible.
+
+ Remember to format your responses clearly with proper headings, bullet points, \
+ and code blocks. Use markdown formatting for better readability.
+
+ Please respond in a professional and helpful manner. Your goal is to help the \
+ user understand complex technical concepts and solve real-world engineering problems.
+ """;
+
+ @Test
+ @DisplayName("DashScope should accept cache_control at content block level")
+ void testDashScopeCacheControlAccepted() {
+ assumeTrue(
+ ProviderFactory.hasDashScopeKey(),
+ "DASHSCOPE_API_KEY not set, skipping cache control E2E test");
+
+ String apiKey = System.getenv("DASHSCOPE_API_KEY");
+ if (apiKey == null || apiKey.isEmpty()) {
+ apiKey = System.getProperty("DASHSCOPE_API_KEY");
+ }
+
+ DashScopeChatModel model =
+ DashScopeChatModel.builder().apiKey(apiKey).modelName("qwen-plus").stream(false)
+ .formatter(new DashScopeChatFormatter())
+ .defaultOptions(GenerateOptions.builder().cacheControl(true).build())
+ .build();
+
+ ReActAgent agent =
+ ReActAgent.builder()
+ .name("CacheControlTestAgent")
+ .model(model)
+ .toolkit(new Toolkit())
+ .sysPrompt(LONG_SYSTEM_PROMPT)
+ .memory(new InMemoryMemory())
+ .build();
+
+ // First call: creates the cache
+ System.out.println("=== Cache Control E2E: First call (cache creation) ===");
+ Msg input1 = TestUtils.createUserMessage("User", "What is a binary search tree?");
+ Msg response1 = agent.call(input1).block(TEST_TIMEOUT);
+
+ assertNotNull(response1, "First response should not be null");
+ String text1 = TestUtils.extractTextContent(response1);
+ assertNotNull(text1, "First response should have text content");
+ assertTrue(!text1.isEmpty(), "First response text should not be empty");
+ System.out.println(
+ "First call succeeded. Response: "
+ + text1.substring(0, Math.min(100, text1.length()))
+ + "...");
+
+ // Reset memory for a clean second call with the same system prompt
+ agent.getMemory().clear();
+
+ // Second call: should hit the cache
+ System.out.println("=== Cache Control E2E: Second call (cache hit expected) ===");
+ Msg input2 = TestUtils.createUserMessage("User", "What is a binary search tree?");
+ Msg response2 = agent.call(input2).block(TEST_TIMEOUT);
+
+ assertNotNull(response2, "Second response should not be null");
+ String text2 = TestUtils.extractTextContent(response2);
+ assertNotNull(text2, "Second response should have text content");
+ assertTrue(!text2.isEmpty(), "Second response text should not be empty");
+ System.out.println(
+ "Second call succeeded. Response: "
+ + text2.substring(0, Math.min(100, text2.length()))
+ + "...");
+
+ System.out.println(
+ "Cache control E2E test passed - DashScope native API accepted cache_control at"
+ + " content block level");
+ }
+
+ @Test
+ @DisplayName(
+ "DashScope OpenAI-compatible endpoint should accept cache_control at content block"
+ + " level")
+ void testOpenAICompatibleCacheControlAccepted() {
+ assumeTrue(
+ ProviderFactory.hasDashScopeKey(),
+ "DASHSCOPE_API_KEY not set, skipping OpenAI-compatible cache control E2E test");
+
+ String apiKey = System.getenv("DASHSCOPE_API_KEY");
+ if (apiKey == null || apiKey.isEmpty()) {
+ apiKey = System.getProperty("DASHSCOPE_API_KEY");
+ }
+
+ OpenAIChatModel model =
+ OpenAIChatModel.builder()
+ .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
+ .apiKey(apiKey)
+ .modelName("qwen-plus")
+ .stream(false)
+ .formatter(new OpenAIChatFormatter())
+ .generateOptions(GenerateOptions.builder().cacheControl(true).build())
+ .build();
+
+ ReActAgent agent =
+ ReActAgent.builder()
+ .name("OpenAICacheControlTestAgent")
+ .model(model)
+ .toolkit(new Toolkit())
+ .sysPrompt(LONG_SYSTEM_PROMPT)
+ .memory(new InMemoryMemory())
+ .build();
+
+ // First call: creates the cache
+ System.out.println(
+ "=== OpenAI-Compatible Cache Control E2E: First call (cache creation) ===");
+ Msg input1 = TestUtils.createUserMessage("User", "What is a binary search tree?");
+ Msg response1 = agent.call(input1).block(TEST_TIMEOUT);
+
+ assertNotNull(response1, "First response should not be null");
+ String text1 = TestUtils.extractTextContent(response1);
+ assertNotNull(text1, "First response should have text content");
+ assertTrue(!text1.isEmpty(), "First response text should not be empty");
+ System.out.println(
+ "First call succeeded. Response: "
+ + text1.substring(0, Math.min(100, text1.length()))
+ + "...");
+
+ // Reset memory for a clean second call with the same system prompt
+ agent.getMemory().clear();
+
+ // Second call: should hit the cache
+ System.out.println(
+ "=== OpenAI-Compatible Cache Control E2E: Second call (cache hit expected) ===");
+ Msg input2 = TestUtils.createUserMessage("User", "What is a binary search tree?");
+ Msg response2 = agent.call(input2).block(TEST_TIMEOUT);
+
+ assertNotNull(response2, "Second response should not be null");
+ String text2 = TestUtils.extractTextContent(response2);
+ assertNotNull(text2, "Second response should have text content");
+ assertTrue(!text2.isEmpty(), "Second response text should not be empty");
+ System.out.println(
+ "Second call succeeded. Response: "
+ + text2.substring(0, Math.min(100, text2.length()))
+ + "...");
+
+ System.out.println(
+ "Cache control E2E test passed - OpenAI-compatible endpoint accepted"
+ + " cache_control at content block level");
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/dashscope/DashScopeCacheControlTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/dashscope/DashScopeCacheControlTest.java
index ab869da37..85f1166d3 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/formatter/dashscope/DashScopeCacheControlTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/dashscope/DashScopeCacheControlTest.java
@@ -16,12 +16,17 @@
package io.agentscope.core.formatter.dashscope;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import io.agentscope.core.formatter.dashscope.dto.DashScopeContentPart;
import io.agentscope.core.formatter.dashscope.dto.DashScopeMessage;
import io.agentscope.core.message.MessageMetadataKeys;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.util.JsonCodec;
+import io.agentscope.core.util.JsonUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -33,6 +38,7 @@
/**
* Tests for cache_control support in DashScope formatter.
+ * Validates that cache_control is placed at content block level per DashScope API spec.
*/
class DashScopeCacheControlTest {
@@ -45,12 +51,31 @@ void setUp() {
formatter = new DashScopeChatFormatter();
}
+ /** Helper: get the last content part's cacheControl from a message. */
+ private Map getLastPartCacheControl(DashScopeMessage msg) {
+ List parts = msg.getContentAsList();
+ assertNotNull(parts, "Content should be array format after applyCacheControl");
+ assertTrue(!parts.isEmpty(), "Content parts should not be empty");
+ return parts.get(parts.size() - 1).getCacheControl();
+ }
+
+ /** Helper: assert no content block in the message has cache_control set. */
+ private void assertNoCacheControlOnParts(DashScopeMessage msg) {
+ List parts = msg.getContentAsList();
+ if (parts == null) {
+ return;
+ }
+ for (DashScopeContentPart part : parts) {
+ assertNull(part.getCacheControl(), "No content block should have cache_control");
+ }
+ }
+
@Nested
- @DisplayName("applyCacheControl - automatic strategy")
+ @DisplayName("applyCacheControl - content block level")
class ApplyCacheControlTest {
@Test
- @DisplayName("should add cache_control to system and last message")
+ @DisplayName("should add cache_control to last content block of system and last message")
void systemAndLastMessage() {
List messages = new ArrayList<>();
messages.add(
@@ -61,10 +86,21 @@ void systemAndLastMessage() {
formatter.applyCacheControl(messages);
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
- assertNull(messages.get(1).getCacheControl());
- assertNull(messages.get(2).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(3).getCacheControl());
+ // system message: content converted to array, last part has cache_control
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
+ assertNull(
+ messages.get(0).getCacheControl(),
+ "Message-level cache_control should be null");
+
+ // middle messages: no cache_control
+ assertNoCacheControlOnParts(messages.get(1));
+ assertNoCacheControlOnParts(messages.get(2));
+
+ // last message: content converted to array, last part has cache_control
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(3)));
+ assertNull(
+ messages.get(3).getCacheControl(),
+ "Message-level cache_control should be null");
}
@Test
@@ -76,8 +112,8 @@ void noSystemMessage() {
formatter.applyCacheControl(messages);
- assertNull(messages.get(0).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(1).getCacheControl());
+ assertNoCacheControlOnParts(messages.get(0));
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(1)));
}
@Test
@@ -85,14 +121,12 @@ void noSystemMessage() {
void emptyList() {
List messages = new ArrayList<>();
formatter.applyCacheControl(messages);
- // No exception thrown
}
@Test
@DisplayName("should handle null list without error")
void nullList() {
formatter.applyCacheControl(null);
- // No exception thrown
}
@Test
@@ -104,51 +138,51 @@ void singleSystemMessage() {
formatter.applyCacheControl(messages);
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
}
@Test
- @DisplayName("should not overwrite manually marked cache_control")
+ @DisplayName("should not overwrite content block with existing cache_control")
void manuallyMarkedNotOverridden() {
Map customCacheControl = Map.of("type", "custom");
- List messages = new ArrayList<>();
- messages.add(
- DashScopeMessage.builder()
- .role("system")
- .content("System")
+ DashScopeContentPart part =
+ DashScopeContentPart.builder()
+ .text("System")
.cacheControl(customCacheControl)
- .build());
+ .build();
+ List messages = new ArrayList<>();
+ messages.add(DashScopeMessage.builder().role("system").content(List.of(part)).build());
messages.add(DashScopeMessage.builder().role("user").content("User").build());
formatter.applyCacheControl(messages);
- // System message keeps its custom cache_control
- assertEquals(customCacheControl, messages.get(0).getCacheControl());
- // Last message gets ephemeral
- assertEquals(EPHEMERAL, messages.get(1).getCacheControl());
+ // System message keeps its custom cache_control on the content block
+ assertEquals(customCacheControl, getLastPartCacheControl(messages.get(0)));
+ // Last message gets ephemeral on content block
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(1)));
}
@Test
- @DisplayName("should not overwrite last message with existing cache_control")
+ @DisplayName("should not overwrite last message content block with existing cache_control")
void lastMessageManuallyMarkedNotOverridden() {
Map customCacheControl = Map.of("type", "custom");
+ DashScopeContentPart part =
+ DashScopeContentPart.builder()
+ .text("User")
+ .cacheControl(customCacheControl)
+ .build();
List messages = new ArrayList<>();
messages.add(DashScopeMessage.builder().role("system").content("System").build());
- messages.add(
- DashScopeMessage.builder()
- .role("user")
- .content("User")
- .cacheControl(customCacheControl)
- .build());
+ messages.add(DashScopeMessage.builder().role("user").content(List.of(part)).build());
formatter.applyCacheControl(messages);
// System message gets ephemeral
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
// Last message keeps its custom cache_control
- assertEquals(customCacheControl, messages.get(1).getCacheControl());
+ assertEquals(customCacheControl, getLastPartCacheControl(messages.get(1)));
}
@Test
@@ -161,9 +195,79 @@ void multipleSystemMessages() {
formatter.applyCacheControl(messages);
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(1).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(2).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(1)));
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(2)));
+ }
+
+ @Test
+ @DisplayName("should convert string content to array format")
+ void stringContentConvertedToArray() {
+ List messages = new ArrayList<>();
+ messages.add(DashScopeMessage.builder().role("system").content("Hello world").build());
+
+ formatter.applyCacheControl(messages);
+
+ // Content should now be array format
+ assertTrue(messages.get(0).isMultimodal(), "Content should be array format");
+ List parts = messages.get(0).getContentAsList();
+ assertNotNull(parts);
+ assertEquals(1, parts.size());
+ assertEquals("Hello world", parts.get(0).getText());
+ assertEquals(EPHEMERAL, parts.get(0).getCacheControl());
+ }
+
+ @Test
+ @DisplayName("should set cache_control on last part when content is already array")
+ void arrayContentLastPartMarked() {
+ List contentParts =
+ List.of(
+ DashScopeContentPart.text("First part"),
+ DashScopeContentPart.text("Second part"),
+ DashScopeContentPart.text("Third part"));
+ List messages = new ArrayList<>();
+ messages.add(
+ DashScopeMessage.builder()
+ .role("system")
+ .content(new ArrayList<>(contentParts))
+ .build());
+
+ formatter.applyCacheControl(messages);
+
+ List parts = messages.get(0).getContentAsList();
+ assertNull(parts.get(0).getCacheControl());
+ assertNull(parts.get(1).getCacheControl());
+ assertEquals(EPHEMERAL, parts.get(2).getCacheControl());
+ }
+ }
+
+ @Nested
+ @DisplayName("JSON serialization verification")
+ class JsonSerializationTest {
+
+ @Test
+ @DisplayName("cache_control should appear inside content block, not at message level")
+ void cacheControlInContentBlock() throws Exception {
+ List messages = new ArrayList<>();
+ messages.add(
+ DashScopeMessage.builder().role("system").content("You are helpful.").build());
+ messages.add(DashScopeMessage.builder().role("user").content("Hello").build());
+
+ formatter.applyCacheControl(messages);
+
+ JsonCodec jsonCodec = JsonUtils.getJsonCodec();
+ String json = jsonCodec.toJson(messages);
+
+ // cache_control should be within content blocks
+ assertTrue(json.contains("\"cache_control\""), "JSON should contain cache_control");
+ assertTrue(json.contains("\"ephemeral\""), "JSON should contain ephemeral");
+
+ // Verify message-level cache_control is NOT present
+ for (DashScopeMessage msg : messages) {
+ assertNull(
+ msg.getCacheControl(),
+ "Message-level cache_control should be null for role: " + msg.getRole());
+ }
}
}
@@ -172,7 +276,7 @@ void multipleSystemMessages() {
class MetadataMarkingTest {
@Test
- @DisplayName("should set cache_control from Msg metadata")
+ @DisplayName("should set cache_control on content block from Msg metadata")
void metadataMarking() {
Map metadata = new HashMap<>();
metadata.put(MessageMetadataKeys.CACHE_CONTROL, true);
@@ -186,7 +290,10 @@ void metadataMarking() {
List result = formatter.format(List.of(msg));
assertEquals(1, result.size());
- assertEquals(EPHEMERAL, result.get(0).getCacheControl());
+ // cache_control should be on content block, not message
+ assertNull(
+ result.get(0).getCacheControl(), "Message-level cache_control should be null");
+ assertEquals(EPHEMERAL, getLastPartCacheControl(result.get(0)));
}
@Test
@@ -219,7 +326,7 @@ void metadataFalse() {
}
@Test
- @DisplayName("should set cache_control on system message via metadata")
+ @DisplayName("should set cache_control on system message content block via metadata")
void systemMessageMetadata() {
Map metadata = new HashMap<>();
metadata.put(MessageMetadataKeys.CACHE_CONTROL, true);
@@ -234,7 +341,10 @@ void systemMessageMetadata() {
List result = formatter.format(List.of(systemMsg, userMsg));
assertEquals(2, result.size());
- assertEquals(EPHEMERAL, result.get(0).getCacheControl());
+ // System message: cache_control on content block
+ assertNull(result.get(0).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(result.get(0)));
+ // User message: no cache_control
assertNull(result.get(1).getCacheControl());
}
}
@@ -244,7 +354,7 @@ void systemMessageMetadata() {
class MultiAgentFormatterTest {
@Test
- @DisplayName("should add cache_control to system and last message")
+ @DisplayName("should add cache_control to content blocks of system and last message")
void applyCacheControl() {
DashScopeMultiAgentFormatter multiFormatter = new DashScopeMultiAgentFormatter();
@@ -255,8 +365,11 @@ void applyCacheControl() {
multiFormatter.applyCacheControl(messages);
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(1).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(1)));
+ // message-level should be null
+ assertNull(messages.get(0).getCacheControl());
+ assertNull(messages.get(1).getCacheControl());
}
}
}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAICacheControlTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAICacheControlTest.java
index 25f8e81cd..9ce89af30 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAICacheControlTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAICacheControlTest.java
@@ -16,8 +16,11 @@
package io.agentscope.core.formatter.openai;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import io.agentscope.core.formatter.openai.dto.OpenAIContentPart;
import io.agentscope.core.formatter.openai.dto.OpenAIMessage;
import io.agentscope.core.message.MessageMetadataKeys;
import io.agentscope.core.message.Msg;
@@ -33,6 +36,7 @@
/**
* Tests for cache_control support in OpenAI formatter.
+ * Validates that cache_control is placed at content block level.
*/
class OpenAICacheControlTest {
@@ -45,12 +49,31 @@ void setUp() {
formatter = new OpenAIChatFormatter();
}
+ /** Helper: get the last content part's cacheControl from a message. */
+ private Map getLastPartCacheControl(OpenAIMessage msg) {
+ List parts = msg.getContentAsList();
+ assertNotNull(parts, "Content should be array format after applyCacheControl");
+ assertTrue(!parts.isEmpty(), "Content parts should not be empty");
+ return parts.get(parts.size() - 1).getCacheControl();
+ }
+
+ /** Helper: assert no content block in the message has cache_control set. */
+ private void assertNoCacheControlOnParts(OpenAIMessage msg) {
+ List parts = msg.getContentAsList();
+ if (parts == null) {
+ return;
+ }
+ for (OpenAIContentPart part : parts) {
+ assertNull(part.getCacheControl(), "No content block should have cache_control");
+ }
+ }
+
@Nested
- @DisplayName("applyCacheControl - automatic strategy")
+ @DisplayName("applyCacheControl - content block level")
class ApplyCacheControlTest {
@Test
- @DisplayName("should add cache_control to system and last message")
+ @DisplayName("should add cache_control to last content block of system and last message")
void systemAndLastMessage() {
List messages = new ArrayList<>();
messages.add(
@@ -61,10 +84,14 @@ void systemAndLastMessage() {
formatter.applyCacheControl(messages);
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
- assertNull(messages.get(1).getCacheControl());
- assertNull(messages.get(2).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(3).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
+ assertNull(messages.get(0).getCacheControl());
+
+ assertNoCacheControlOnParts(messages.get(1));
+ assertNoCacheControlOnParts(messages.get(2));
+
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(3)));
+ assertNull(messages.get(3).getCacheControl());
}
@Test
@@ -76,8 +103,8 @@ void noSystemMessage() {
formatter.applyCacheControl(messages);
- assertNull(messages.get(0).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(1).getCacheControl());
+ assertNoCacheControlOnParts(messages.get(0));
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(1)));
}
@Test
@@ -85,14 +112,12 @@ void noSystemMessage() {
void emptyList() {
List messages = new ArrayList<>();
formatter.applyCacheControl(messages);
- // No exception thrown
}
@Test
@DisplayName("should handle null list without error")
void nullList() {
formatter.applyCacheControl(null);
- // No exception thrown
}
@Test
@@ -104,51 +129,49 @@ void singleSystemMessage() {
formatter.applyCacheControl(messages);
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
}
@Test
- @DisplayName("should not overwrite manually marked cache_control")
+ @DisplayName("should not overwrite content block with existing cache_control")
void manuallyMarkedNotOverridden() {
Map customCacheControl = Map.of("type", "custom");
- List messages = new ArrayList<>();
- messages.add(
- OpenAIMessage.builder()
- .role("system")
- .content("System")
+ OpenAIContentPart part =
+ OpenAIContentPart.builder()
+ .type("text")
+ .text("System")
.cacheControl(customCacheControl)
- .build());
+ .build();
+ List messages = new ArrayList<>();
+ messages.add(OpenAIMessage.builder().role("system").content(List.of(part)).build());
messages.add(OpenAIMessage.builder().role("user").content("User").build());
formatter.applyCacheControl(messages);
- // System message keeps its custom cache_control
- assertEquals(customCacheControl, messages.get(0).getCacheControl());
- // Last message gets ephemeral
- assertEquals(EPHEMERAL, messages.get(1).getCacheControl());
+ assertEquals(customCacheControl, getLastPartCacheControl(messages.get(0)));
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(1)));
}
@Test
- @DisplayName("should not overwrite last message with existing cache_control")
+ @DisplayName("should not overwrite last message content block with existing cache_control")
void lastMessageManuallyMarkedNotOverridden() {
Map customCacheControl = Map.of("type", "custom");
+ OpenAIContentPart part =
+ OpenAIContentPart.builder()
+ .type("text")
+ .text("User")
+ .cacheControl(customCacheControl)
+ .build();
List messages = new ArrayList<>();
messages.add(OpenAIMessage.builder().role("system").content("System").build());
- messages.add(
- OpenAIMessage.builder()
- .role("user")
- .content("User")
- .cacheControl(customCacheControl)
- .build());
+ messages.add(OpenAIMessage.builder().role("user").content(List.of(part)).build());
formatter.applyCacheControl(messages);
- // System message gets ephemeral
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
- // Last message keeps its custom cache_control
- assertEquals(customCacheControl, messages.get(1).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
+ assertEquals(customCacheControl, getLastPartCacheControl(messages.get(1)));
}
@Test
@@ -161,9 +184,26 @@ void multipleSystemMessages() {
formatter.applyCacheControl(messages);
- assertEquals(EPHEMERAL, messages.get(0).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(1).getCacheControl());
- assertEquals(EPHEMERAL, messages.get(2).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(0)));
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(1)));
+ assertEquals(EPHEMERAL, getLastPartCacheControl(messages.get(2)));
+ }
+
+ @Test
+ @DisplayName("should convert string content to array format")
+ void stringContentConvertedToArray() {
+ List messages = new ArrayList<>();
+ messages.add(OpenAIMessage.builder().role("system").content("Hello world").build());
+
+ formatter.applyCacheControl(messages);
+
+ assertTrue(messages.get(0).isMultimodal(), "Content should be array format");
+ List parts = messages.get(0).getContentAsList();
+ assertNotNull(parts);
+ assertEquals(1, parts.size());
+ assertEquals("text", parts.get(0).getType());
+ assertEquals("Hello world", parts.get(0).getText());
+ assertEquals(EPHEMERAL, parts.get(0).getCacheControl());
}
}
@@ -172,7 +212,7 @@ void multipleSystemMessages() {
class MetadataMarkingTest {
@Test
- @DisplayName("should set cache_control from Msg metadata")
+ @DisplayName("should set cache_control on content block from Msg metadata")
void metadataMarking() {
Map metadata = new HashMap<>();
metadata.put(MessageMetadataKeys.CACHE_CONTROL, true);
@@ -186,7 +226,9 @@ void metadataMarking() {
List result = formatter.format(List.of(msg));
assertEquals(1, result.size());
- assertEquals(EPHEMERAL, result.get(0).getCacheControl());
+ assertNull(
+ result.get(0).getCacheControl(), "Message-level cache_control should be null");
+ assertEquals(EPHEMERAL, getLastPartCacheControl(result.get(0)));
}
@Test
@@ -219,7 +261,7 @@ void metadataFalse() {
}
@Test
- @DisplayName("should set cache_control on system message via metadata")
+ @DisplayName("should set cache_control on system message content block via metadata")
void systemMessageMetadata() {
Map metadata = new HashMap<>();
metadata.put(MessageMetadataKeys.CACHE_CONTROL, true);
@@ -234,7 +276,8 @@ void systemMessageMetadata() {
List result = formatter.format(List.of(systemMsg, userMsg));
assertEquals(2, result.size());
- assertEquals(EPHEMERAL, result.get(0).getCacheControl());
+ assertNull(result.get(0).getCacheControl());
+ assertEquals(EPHEMERAL, getLastPartCacheControl(result.get(0)));
assertNull(result.get(1).getCacheControl());
}
}