This is an automated email from the ASF dual-hosted git repository.

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new 4d119211 [integrations][bedrock] Apply Bedrock native structured 
output (#1097)
4d119211 is described below

commit 4d119211e9a91c200ad23739ec898df6eb32a4ea
Author: Weiqing Yang <[email protected]>
AuthorDate: Mon Sep 14 19:38:42 2026 -0700

    [integrations][bedrock] Apply Bedrock native structured output (#1097)
    
    Generated-by: Claude Code 2.1.270 (Claude Opus 5)
---
 dist/src/main/resources/META-INF/NOTICE            |  75 +--
 integrations/chat-models/bedrock/pom.xml           |  11 +
 .../bedrock/BedrockChatModelConnection.java        | 264 ++++++++++-
 .../bedrock/BedrockChatModelConnectionTest.java    | 520 +++++++++++++++++++++
 integrations/pom.xml                               |   2 +-
 5 files changed, 823 insertions(+), 49 deletions(-)

diff --git a/dist/src/main/resources/META-INF/NOTICE 
b/dist/src/main/resources/META-INF/NOTICE
index aa53540a..39b97f2d 100644
--- a/dist/src/main/resources/META-INF/NOTICE
+++ b/dist/src/main/resources/META-INF/NOTICE
@@ -51,16 +51,16 @@ This project bundles the following dependencies under the 
Apache Software Licens
 - org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10
 - org.jetbrains.kotlin:kotlin-reflect:1.8.10
 - org.yaml:snakeyaml:2.3
-- io.netty:netty-handler:4.1.118.Final
-- io.netty:netty-resolver:4.1.118.Final
-- io.netty:netty-transport:4.1.118.Final
-- io.netty:netty-buffer:4.1.118.Final
-- io.netty:netty-codec:4.1.118.Final
-- io.netty:netty-codec-http:4.1.118.Final
-- io.netty:netty-codec-http2:4.1.118.Final
-- io.netty:netty-transport-native-unix-common:4.1.118.Final
-- io.netty:netty-transport-classes-epoll:4.1.118.Final
-- io.netty:netty-common:4.1.118.Final
+- io.netty:netty-handler:4.1.130.Final
+- io.netty:netty-resolver:4.1.130.Final
+- io.netty:netty-transport:4.1.130.Final
+- io.netty:netty-buffer:4.1.130.Final
+- io.netty:netty-codec:4.1.130.Final
+- io.netty:netty-codec-http:4.1.130.Final
+- io.netty:netty-codec-http2:4.1.130.Final
+- io.netty:netty-transport-native-unix-common:4.1.130.Final
+- io.netty:netty-transport-classes-epoll:4.1.130.Final
+- io.netty:netty-common:4.1.130.Final
 - io.projectreactor:reactor-core:3.7.0
 - org.elasticsearch.client:elasticsearch-rest-client:8.19.0
 - org.apache.httpcomponents:httpclient:4.5.13
@@ -88,33 +88,34 @@ This project bundles the following dependencies under the 
Apache Software Licens
 - io.prometheus:simpleclient_tracer_common:0.16.0
 - io.prometheus:simpleclient_tracer_otel:0.16.0
 - io.prometheus:simpleclient_tracer_otel_agent:0.16.0
-- software.amazon.awssdk:bedrockruntime:2.32.16
-- software.amazon.awssdk:s3vectors:2.32.16
-- software.amazon.awssdk:sdk-core:2.32.16
-- software.amazon.awssdk:aws-core:2.32.16
-- software.amazon.awssdk:auth:2.32.16
-- software.amazon.awssdk:regions:2.32.16
-- software.amazon.awssdk:profiles:2.32.16
-- software.amazon.awssdk:utils:2.32.16
-- software.amazon.awssdk:annotations:2.32.16
-- software.amazon.awssdk:apache-client:2.32.16
-- software.amazon.awssdk:netty-nio-client:2.32.16
-- software.amazon.awssdk:http-client-spi:2.32.16
-- software.amazon.awssdk:http-auth:2.32.16
-- software.amazon.awssdk:http-auth-aws:2.32.16
-- software.amazon.awssdk:http-auth-aws-eventstream:2.32.16
-- software.amazon.awssdk:http-auth-spi:2.32.16
-- software.amazon.awssdk:identity-spi:2.32.16
-- software.amazon.awssdk:metrics-spi:2.32.16
-- software.amazon.awssdk:endpoints-spi:2.32.16
-- software.amazon.awssdk:checksums:2.32.16
-- software.amazon.awssdk:checksums-spi:2.32.16
-- software.amazon.awssdk:retries:2.32.16
-- software.amazon.awssdk:retries-spi:2.32.16
-- software.amazon.awssdk:aws-json-protocol:2.32.16
-- software.amazon.awssdk:protocol-core:2.32.16
-- software.amazon.awssdk:json-utils:2.32.16
-- software.amazon.awssdk:third-party-jackson-core:2.32.16
+- software.amazon.awssdk:bedrockruntime:2.41.22
+- software.amazon.awssdk:s3vectors:2.41.22
+- software.amazon.awssdk:sdk-core:2.41.22
+- software.amazon.awssdk:aws-core:2.41.22
+- software.amazon.awssdk:auth:2.41.22
+- software.amazon.awssdk:regions:2.41.22
+- software.amazon.awssdk:profiles:2.41.22
+- software.amazon.awssdk:utils:2.41.22
+- software.amazon.awssdk:utils-lite:2.41.22
+- software.amazon.awssdk:annotations:2.41.22
+- software.amazon.awssdk:apache-client:2.41.22
+- software.amazon.awssdk:netty-nio-client:2.41.22
+- software.amazon.awssdk:http-client-spi:2.41.22
+- software.amazon.awssdk:http-auth:2.41.22
+- software.amazon.awssdk:http-auth-aws:2.41.22
+- software.amazon.awssdk:http-auth-aws-eventstream:2.41.22
+- software.amazon.awssdk:http-auth-spi:2.41.22
+- software.amazon.awssdk:identity-spi:2.41.22
+- software.amazon.awssdk:metrics-spi:2.41.22
+- software.amazon.awssdk:endpoints-spi:2.41.22
+- software.amazon.awssdk:checksums:2.41.22
+- software.amazon.awssdk:checksums-spi:2.41.22
+- software.amazon.awssdk:retries:2.41.22
+- software.amazon.awssdk:retries-spi:2.41.22
+- software.amazon.awssdk:aws-json-protocol:2.41.22
+- software.amazon.awssdk:protocol-core:2.41.22
+- software.amazon.awssdk:json-utils:2.41.22
+- software.amazon.awssdk:third-party-jackson-core:2.41.22
 - software.amazon.eventstream:eventstream:1.0.1
 - com.google.genai:google-genai:1.56.0
 - com.google.auto.value:auto-value-annotations:1.11.0
diff --git a/integrations/chat-models/bedrock/pom.xml 
b/integrations/chat-models/bedrock/pom.xml
index b2fb4488..eb88c88a 100644
--- a/integrations/chat-models/bedrock/pom.xml
+++ b/integrations/chat-models/bedrock/pom.xml
@@ -43,6 +43,17 @@ under the License.
             <artifactId>bedrockruntime</artifactId>
             <version>${aws.sdk.version}</version>
         </dependency>
+
+        <!-- Versions managed by the victools BOM imported in the root pom. -->
+        <dependency>
+            <groupId>com.github.victools</groupId>
+            <artifactId>jsonschema-generator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.victools</groupId>
+            <artifactId>jsonschema-module-jackson</artifactId>
+        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/integrations/chat-models/bedrock/src/main/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelConnection.java
 
b/integrations/chat-models/bedrock/src/main/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelConnection.java
index 86105572..62cc250c 100644
--- 
a/integrations/chat-models/bedrock/src/main/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelConnection.java
+++ 
b/integrations/chat-models/bedrock/src/main/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelConnection.java
@@ -20,7 +20,14 @@ package 
org.apache.flink.agents.integrations.chatmodels.bedrock;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.victools.jsonschema.generator.OptionPreset;
+import com.github.victools.jsonschema.generator.SchemaGenerator;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import com.github.victools.jsonschema.generator.SchemaVersion;
+import com.github.victools.jsonschema.module.jackson.JacksonModule;
+import com.github.victools.jsonschema.module.jackson.JacksonOption;
 import org.apache.flink.agents.api.RetryExecutor;
 import org.apache.flink.agents.api.chat.messages.ChatMessage;
 import org.apache.flink.agents.api.chat.messages.MessageRole;
@@ -39,7 +46,12 @@ import 
software.amazon.awssdk.services.bedrockruntime.model.ConversationRole;
 import software.amazon.awssdk.services.bedrockruntime.model.ConverseRequest;
 import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse;
 import 
software.amazon.awssdk.services.bedrockruntime.model.InferenceConfiguration;
+import 
software.amazon.awssdk.services.bedrockruntime.model.JsonSchemaDefinition;
 import software.amazon.awssdk.services.bedrockruntime.model.Message;
+import software.amazon.awssdk.services.bedrockruntime.model.OutputConfig;
+import software.amazon.awssdk.services.bedrockruntime.model.OutputFormat;
+import 
software.amazon.awssdk.services.bedrockruntime.model.OutputFormatStructure;
+import software.amazon.awssdk.services.bedrockruntime.model.OutputFormatType;
 import software.amazon.awssdk.services.bedrockruntime.model.SystemContentBlock;
 import software.amazon.awssdk.services.bedrockruntime.model.ToolConfiguration;
 import software.amazon.awssdk.services.bedrockruntime.model.ToolInputSchema;
@@ -54,6 +66,10 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 /**
@@ -88,6 +104,80 @@ import java.util.stream.Collectors;
 public class BedrockChatModelConnection extends BaseChatModelConnection {
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    // Models AWS documents structured-output support for on the 
bedrock-runtime endpoint. There is
+    // no single list page: the feature page delegates the per-model answer to 
the individual model
+    // cards, where each card carries it as a "Structured outputs" bullet in 
the Supported or Not
+    // Supported column of its "Features supported using bedrock-runtime 
endpoint" table.
+    //
+    // The ids are the Model ID column of each card's Programmatic Access 
table, read from the
+    // bedrock-runtime row. A card commonly prints a different id for 
bedrock-mantle and can carry
+    // opposite verdicts for the two, so the endpoint an id was read from is 
part of what makes the
+    // entry correct. This connection calls Converse on bedrock-runtime.
+    //
+    // Matching is exact, never by prefix. A Bedrock id already pins the 
vendor, the snapshot date
+    // and the version in one string, so there is no alias for a prefix to 
cover, and a prefix would
+    // over-capture: "qwen.qwen3" admits qwen.qwen3-vl-235b-a22b, which AWS 
documents as not
+    // supported, and "anthropic.claude-sonnet-4" admits 
anthropic.claude-sonnet-4-20250514-v1:0,
+    // whose card carries no answer at all. Exact matching also keeps 
irregular id shapes correct
+    // with no normalisation rule: mistral.mistral-large-3-675b-instruct 
carries no version suffix,
+    // openai.gpt-oss-120b-1:0 carries "-1:0" rather than "-v1:0".
+    //
+    // A card whose capability table carries the bullet in neither column is 
undocumented rather
+    // than negative, and is absent from this set for that reason.
+    private static final Set<String> NATIVE_STRUCTURED_OUTPUT_MODELS =
+            Set.of(
+                    "anthropic.claude-sonnet-4-5-20250929-v1:0",
+                    "anthropic.claude-opus-4-5-20251101-v1:0",
+                    "anthropic.claude-haiku-4-5-20251001-v1:0",
+                    "anthropic.claude-opus-4-6-v1",
+                    "anthropic.claude-sonnet-4-6",
+                    "deepseek.v3-v1:0",
+                    "deepseek.v3.2",
+                    "google.gemma-3-12b-it",
+                    "google.gemma-3-27b-it",
+                    "minimax.minimax-m2",
+                    "minimax.minimax-m2.1",
+                    "minimax.minimax-m2.5",
+                    "mistral.mistral-large-3-675b-instruct",
+                    "mistral.devstral-2-123b",
+                    "mistral.magistral-small-2509",
+                    "mistral.ministral-3-14b-instruct",
+                    "mistral.ministral-3-3b-instruct",
+                    "mistral.ministral-3-8b-instruct",
+                    "mistral.voxtral-mini-3b-2507",
+                    "mistral.voxtral-small-24b-2507",
+                    "moonshot.kimi-k2-thinking",
+                    "moonshotai.kimi-k2.5",
+                    "nvidia.nemotron-nano-12b-v2",
+                    "nvidia.nemotron-nano-3-30b",
+                    "nvidia.nemotron-nano-9b-v2",
+                    "nvidia.nemotron-super-3-120b",
+                    "openai.gpt-oss-120b-1:0",
+                    "openai.gpt-oss-20b-1:0",
+                    "openai.gpt-5.6-luna",
+                    "openai.gpt-oss-safeguard-120b",
+                    "openai.gpt-oss-safeguard-20b",
+                    "qwen.qwen3-235b-a22b-2507-v1:0",
+                    "qwen.qwen3-32b-v1:0",
+                    "qwen.qwen3-coder-30b-a3b-v1:0",
+                    "qwen.qwen3-coder-480b-a35b-v1:0",
+                    "qwen.qwen3-coder-next",
+                    "qwen.qwen3-next-80b-a3b",
+                    "writer.palmyra-vision-7b",
+                    "zai.glm-4.7",
+                    "zai.glm-4.7-flash",
+                    "zai.glm-5");
+
+    // A cross-Region inference profile id is a model id behind a geographic 
or global prefix, and
+    // AWS documents structured output as working through cross-Region 
inference. The prefix set is
+    // open-ended — the documentation names members by example and states that 
new profiles may
+    // be created — so a leading segment is matched by shape rather than 
against a fixed list,
+    // which would already have missed the documented us-gov. profiles. The 
charset excludes ":"
+    // and "/", so no ARN can be shortened this way. The strip is attempted 
only after the id itself
+    // fails to match, so a listed model id always matches as itself.
+    private static final Pattern INFERENCE_PROFILE_PREFIX = 
Pattern.compile("^[a-z0-9-]+\\.(.+)$");
+
     private final BedrockRuntimeClient client;
     private final String defaultModel;
     private final RetryExecutor retryExecutor;
@@ -117,9 +207,102 @@ public class BedrockChatModelConnection extends 
BaseChatModelConnection {
                         .build();
     }
 
+    /**
+     * Whether AWS documents structured-output support for {@code 
effectiveModel}.
+     *
+     * <p>See the allowlist above for the source of truth, for why the match 
is exact, and for why a
+     * geographic or global inference-profile prefix is stripped before it.
+     *
+     * <p>Every ARN reports {@code false}. A provisioned-throughput, 
imported-model,
+     * custom-model-deployment, application-inference-profile or 
marketplace-endpoint ARN identifies
+     * a resource without naming the model behind it, and a prompt-router ARN 
names a set whose
+     * member is chosen per request, so for none of them is an answer 
derivable from the identifier
+     * the request carries. An unrecognized identifier reports {@code false} 
so that it degrades to
+     * the prompt-engineering fallback rather than failing at the provider.
+     *
+     * <p>A null or blank model reports {@code false} rather than throwing: 
{@code resolveModel}
+     * rejects one before a request is built, but this method is part of the 
connection contract and
+     * answers for whatever it is given. Only the null case needs a guard of 
its own, because the
+     * allowlist is an immutable Set whose {@code contains(null)} throws; a 
blank model is merely
+     * absent from it.
+     *
+     * <p>Reads no instance state, so capability stays answerable 
independently of how the
+     * connection was configured.
+     */
+    @Override
+    protected boolean supportsNativeStructuredOutput(String effectiveModel) {
+        // Load-bearing: the allowlist is an immutable Set, whose 
contains(null) throws rather than
+        // reporting absence.
+        if (effectiveModel == null || effectiveModel.isBlank()) {
+            return false;
+        }
+        if (NATIVE_STRUCTURED_OUTPUT_MODELS.contains(effectiveModel)) {
+            return true;
+        }
+        Matcher profile = INFERENCE_PROFILE_PREFIX.matcher(effectiveModel);
+        return profile.matches() && 
NATIVE_STRUCTURED_OUTPUT_MODELS.contains(profile.group(1));
+    }
+
     @Override
     public ChatMessage chat(
             List<ChatMessage> messages, List<Tool> tools, Map<String, Object> 
modelParams) {
+        return chat(messages, tools, modelParams, null);
+    }
+
+    /**
+     * Translates {@code outputSchema} into Converse's native {@code 
outputConfig} when it is a POJO
+     * {@link Class} and the effective model is one AWS documents as 
supporting it. Any other schema
+     * form — notably a {@code RowTypeInfo} wrapped in {@code OutputSchema} — 
and any other model
+     * leave the request unconstrained, so that the caller keeps the 
prompt-engineering fallback.
+     */
+    @Override
+    public ChatMessage chat(
+            List<ChatMessage> messages,
+            List<Tool> tools,
+            Map<String, Object> modelParams,
+            Object outputSchema) {
+        ConverseRequest request = buildRequest(messages, tools, modelParams, 
outputSchema);
+        String modelId = request.modelId();
+
+        ConverseResponse response =
+                retryExecutor.execute(() -> client.converse(request), 
"BedrockConverse");
+
+        ChatMessage result = convertResponse(response);
+        if (response.usage() != null) {
+            result.getExtraArgs().put("model_name", modelId);
+            result.getExtraArgs().put("promptTokens", 
response.usage().inputTokens().longValue());
+            result.getExtraArgs()
+                    .put("completionTokens", 
response.usage().outputTokens().longValue());
+        }
+        return result;
+    }
+
+    /**
+     * Translate the flink-agents call arguments into a Converse request: the 
effective model id,
+     * the SYSTEM/conversation message split, the tool configuration, the 
inference configuration,
+     * and the native output configuration when the schema and the model both 
admit one.
+     *
+     * <p>Package-private so a test can assert the request body without 
issuing a live call through
+     * the Bedrock runtime client.
+     *
+     * <p>Resolving the model is the first step, so an absent model id is 
rejected before any
+     * request state is built.
+     *
+     * @param messages the conversation, SYSTEM messages included; must not be 
null
+     * @param tools the tools to advertise, or {@code null} / empty for none
+     * @param modelParams per-call parameters; {@code model}, {@code 
temperature} and {@code
+     *     max_tokens} are read, and {@code null} is accepted
+     * @param outputSchema the schema the response should conform to, or 
{@code null} for an
+     *     unconstrained response; applied natively only for a POJO {@link 
Class} on a model that
+     *     supports it, and otherwise left to the caller's prompt-engineering 
fallback
+     * @return the request to send to Converse
+     * @throws IllegalArgumentException if neither the call nor the connection 
supplies a model id
+     */
+    ConverseRequest buildRequest(
+            List<ChatMessage> messages,
+            List<Tool> tools,
+            Map<String, Object> modelParams,
+            Object outputSchema) {
         String modelId = resolveModel(modelParams);
 
         List<ChatMessage> systemMsgs =
@@ -173,19 +356,78 @@ public class BedrockChatModelConnection extends 
BaseChatModelConnection {
             }
         }
 
-        ConverseRequest request = requestBuilder.build();
+        if (outputSchema instanceof Class && 
supportsNativeStructuredOutput(modelId)) {
+            requestBuilder.outputConfig(nativeOutputConfig((Class<?>) 
outputSchema));
+        }
 
-        ConverseResponse response =
-                retryExecutor.execute(() -> client.converse(request), 
"BedrockConverse");
+        return requestBuilder.build();
+    }
 
-        ChatMessage result = convertResponse(response);
-        if (response.usage() != null) {
-            result.getExtraArgs().put("model_name", modelId);
-            result.getExtraArgs().put("promptTokens", 
response.usage().inputTokens().longValue());
-            result.getExtraArgs()
-                    .put("completionTokens", 
response.usage().outputTokens().longValue());
-        }
-        return result;
+    /**
+     * Wraps the schema derived from {@code schemaClass} in the request 
element Converse reads it
+     * from.
+     *
+     * <p>Converse takes the schema as serialized text rather than as a 
document, unlike the tool
+     * input schema on the same request, so the derived schema is written out 
here.
+     */
+    private static OutputConfig nativeOutputConfig(Class<?> schemaClass) {
+        return OutputConfig.builder()
+                .textFormat(
+                        OutputFormat.builder()
+                                .type(OutputFormatType.JSON_SCHEMA)
+                                .structure(
+                                        OutputFormatStructure.builder()
+                                                .jsonSchema(
+                                                        
JsonSchemaDefinition.builder()
+                                                                .schema(
+                                                                        
toNativeSchema(schemaClass)
+                                                                               
 .toString())
+                                                                .build())
+                                                .build())
+                                .build())
+                .build();
+    }
+
+    // Derives the JSON schema from a POJO class. Every setting below 
addresses a concrete way the
+    // generated schema otherwise fails to constrain generation:
+    //
+    //   - DRAFT_2020_12 is the dialect Bedrock validates a schema against, so 
the schema
+    //     declares it rather than the generator's older default.
+    //   - The PLAIN_JSON preset keeps generation to fields. Without a preset, 
getters surface as
+    //     properties of their own, named after the accessor call, e.g. 
"getSummary()".
+    //   - The required check marks every field required except an Optional 
one. The default marks
+    //     nothing required, which lets a model omit fields at will, while 
marking everything
+    //     required would force the fields a caller declared omissible.
+    //   - The Jackson module makes the schema name properties the way Jackson 
names them, and
+    //     list enum constants mapped by @JsonProperty or by a @JsonValue 
method under the values
+    //     Jackson reads. The response is read back into the same class with 
an ObjectMapper, so a
+    //     property that @JsonProperty renames or @JsonIgnore drops, and a 
mapped enum constant,
+    //     have to appear in the schema as the mapper reads them, or a 
response that satisfies the
+    //     schema still fails to deserialize. An enum annotating only some 
constants falls back to
+    //     Java names for all of them, so its annotated constants do not read 
back. The two enum
+    //     options change only the listed values: the required set stays the 
one configured above.
+    //
+    // A Map's value schema is deliberately left underived. Bedrock accepts 
additionalProperties
+    // only as false, and rejects a schema that carries it as a subschema, so 
typing map values
+    // would trade an unconstrained map for a rejected request. A Map field 
reaches the model as a
+    // bare object.
+    //
+    // A self-referencing class derives its own field as a reference back to 
the schema root,
+    // whatever the required check says. Bedrock does not accept a recursive 
schema and rejects the
+    // request before the model runs, so declaring the field Optional does not 
rescue it; only
+    // flattening the recursion does.
+    private static JsonNode toNativeSchema(Class<?> schemaClass) {
+        SchemaGeneratorConfigBuilder configBuilder =
+                new SchemaGeneratorConfigBuilder(
+                                SchemaVersion.DRAFT_2020_12, 
OptionPreset.PLAIN_JSON)
+                        .with(
+                                new JacksonModule(
+                                        
JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY,
+                                        
JacksonOption.FLATTENED_ENUMS_FROM_JSONVALUE));
+        configBuilder
+                .forFields()
+                .withRequiredCheck(field -> 
!Optional.class.equals(field.getRawMember().getType()));
+        return new 
SchemaGenerator(configBuilder.build()).generateSchema(schemaClass);
     }
 
     private static boolean isRetryable(Exception e) {
diff --git 
a/integrations/chat-models/bedrock/src/test/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelConnectionTest.java
 
b/integrations/chat-models/bedrock/src/test/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelConnectionTest.java
index 5e570f59..59214174 100644
--- 
a/integrations/chat-models/bedrock/src/test/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelConnectionTest.java
+++ 
b/integrations/chat-models/bedrock/src/test/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelConnectionTest.java
@@ -18,15 +18,36 @@
 
 package org.apache.flink.agents.integrations.chatmodels.bedrock;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.flink.agents.api.chat.messages.ChatMessage;
 import org.apache.flink.agents.api.chat.messages.MessageRole;
 import org.apache.flink.agents.api.chat.model.BaseChatModelConnection;
 import org.apache.flink.agents.api.resource.ResourceContext;
 import org.apache.flink.agents.api.resource.ResourceDescriptor;
+import org.apache.flink.agents.api.tools.Tool;
+import org.apache.flink.agents.api.tools.ToolMetadata;
+import org.apache.flink.agents.api.tools.ToolParameters;
+import org.apache.flink.agents.api.tools.ToolResponse;
+import org.apache.flink.agents.api.tools.ToolType;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import software.amazon.awssdk.core.document.Document;
+import software.amazon.awssdk.services.bedrockruntime.model.ConversationRole;
+import software.amazon.awssdk.services.bedrockruntime.model.ConverseRequest;
+import software.amazon.awssdk.services.bedrockruntime.model.Message;
+import software.amazon.awssdk.services.bedrockruntime.model.OutputFormatType;
 
 import java.util.*;
+import java.util.stream.Stream;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -45,6 +66,34 @@ class BedrockChatModelConnectionTest {
         return b.build();
     }
 
+    private static BedrockChatModelConnection connection() {
+        return new BedrockChatModelConnection(
+                descriptor("us-east-1", 
"us.anthropic.claude-sonnet-4-20250514-v1:0"), NOOP);
+    }
+
+    /** Minimal tool carrying only metadata; never invoked in these tests. */
+    private static final class SchemaOnlyTool extends Tool {
+        SchemaOnlyTool(String inputSchema) {
+            super(new ToolMetadata("add", "Add two numbers.", inputSchema));
+        }
+
+        @Override
+        public ToolType getToolType() {
+            return ToolType.FUNCTION;
+        }
+
+        @Override
+        public ToolResponse call(ToolParameters parameters) {
+            throw new UnsupportedOperationException("not invoked in this 
test");
+        }
+    }
+
+    private static ChatMessage toolMessage(String externalId, String content) {
+        Map<String, Object> extraArgs = new HashMap<>();
+        extraArgs.put("externalId", externalId);
+        return new ChatMessage(MessageRole.TOOL, content, extraArgs);
+    }
+
     @Test
     @DisplayName("Constructor creates client with default region")
     void testConstructorDefaultRegion() {
@@ -82,6 +131,22 @@ class BedrockChatModelConnectionTest {
                 .isInstanceOf(RuntimeException.class);
     }
 
+    @Test
+    @DisplayName("chat accepts a POJO output schema instead of rejecting it as 
untranslatable")
+    void testChatAcceptsPojoOutputSchema() {
+        // A connection that does not translate schemas itself inherits a 
four-argument chat that
+        // refuses every non-null schema outright, so the feature can be 
removed at the call
+        // boundary while buildRequest still wires outputConfig correctly. 
Configuring no model
+        // makes the overridden path fail while resolving the model, which 
reports that the schema
+        // was accepted and a request was being built, without any call 
reaching the provider.
+        BedrockChatModelConnection conn =
+                new BedrockChatModelConnection(descriptor("us-east-1", null), 
NOOP);
+        List<ChatMessage> msgs = List.of(new ChatMessage(MessageRole.USER, 
"hello"));
+        assertThatThrownBy(() -> conn.chat(msgs, null, Collections.emptyMap(), 
Profile.class))
+                .isNotInstanceOf(UnsupportedOperationException.class)
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
     @Test
     @DisplayName("stripMarkdownFences: normal text with braces is not 
modified")
     void testStripMarkdownFencesPreservesTextWithBraces() {
@@ -119,4 +184,459 @@ class BedrockChatModelConnectionTest {
     void testStripMarkdownFencesNull() {
         
assertThat(BedrockChatModelConnection.stripMarkdownFences(null)).isNull();
     }
+
+    @Test
+    @DisplayName("buildRequest: the effective model id lands in the request")
+    void testBuildRequestResolvesModelId() {
+        List<ChatMessage> messages = List.of(ChatMessage.user("hello"));
+
+        ConverseRequest fromConnection = connection().buildRequest(messages, 
null, Map.of(), null);
+        assertThat(fromConnection.modelId())
+                .isEqualTo("us.anthropic.claude-sonnet-4-20250514-v1:0");
+
+        ConverseRequest fromCall =
+                connection().buildRequest(messages, null, Map.of("model", 
"per-call-model"), null);
+        assertThat(fromCall.modelId()).isEqualTo("per-call-model");
+    }
+
+    @Test
+    @DisplayName("buildRequest: tools land in toolConfig")
+    void testBuildRequestPreservesToolConfig() {
+        ConverseRequest request =
+                connection()
+                        .buildRequest(
+                                List.of(ChatMessage.user("hello")),
+                                List.of(new SchemaOnlyTool("{\"type\": 
\"object\"}")),
+                                Map.of(),
+                                null);
+
+        assertThat(request.toolConfig()).isNotNull();
+        assertThat(request.toolConfig().tools()).hasSize(1);
+        
assertThat(request.toolConfig().tools().get(0).toolSpec().name()).isEqualTo("add");
+        
assertThat(request.toolConfig().tools().get(0).toolSpec().description())
+                .isEqualTo("Add two numbers.");
+        
assertThat(request.toolConfig().tools().get(0).toolSpec().inputSchema().json().asMap())
+                .containsEntry("type", Document.fromString("object"));
+    }
+
+    @Test
+    @DisplayName("buildRequest: system messages land in system, the rest in 
messages")
+    void testBuildRequestPreservesSystemMessages() {
+        ConverseRequest request =
+                connection()
+                        .buildRequest(
+                                List.of(ChatMessage.system("be terse"), 
ChatMessage.user("hello")),
+                                null,
+                                Map.of(),
+                                null);
+
+        assertThat(request.system()).hasSize(1);
+        assertThat(request.system().get(0).text()).isEqualTo("be terse");
+        assertThat(request.messages()).hasSize(1);
+        
assertThat(request.messages().get(0).role()).isEqualTo(ConversationRole.USER);
+        
assertThat(request.messages().get(0).content().get(0).text()).isEqualTo("hello");
+    }
+
+    @Test
+    @DisplayName("buildRequest: temperature and max_tokens land in 
inferenceConfig")
+    void testBuildRequestPreservesInferenceConfig() {
+        List<ChatMessage> messages = List.of(ChatMessage.user("hello"));
+
+        ConverseRequest configured =
+                connection()
+                        .buildRequest(
+                                messages, null, Map.of("temperature", 0.7, 
"max_tokens", 64), null);
+        assertThat(configured.inferenceConfig()).isNotNull();
+        assertThat(configured.inferenceConfig().temperature()).isEqualTo(0.7f);
+        assertThat(configured.inferenceConfig().maxTokens()).isEqualTo(64);
+
+        ConverseRequest bare = connection().buildRequest(messages, null, 
Map.of(), null);
+        assertThat(bare.inferenceConfig()).isNull();
+    }
+
+    @Test
+    @DisplayName("buildRequest: consecutive tool messages merge into one user 
message")
+    void testBuildRequestMergesConsecutiveToolMessages() {
+        ConverseRequest request =
+                connection()
+                        .buildRequest(
+                                List.of(
+                                        ChatMessage.user("hello"),
+                                        toolMessage("call-1", "first result"),
+                                        toolMessage("call-2", "second 
result")),
+                                null,
+                                Map.of(),
+                                null);
+
+        assertThat(request.messages()).hasSize(2);
+        Message merged = request.messages().get(1);
+        assertThat(merged.role()).isEqualTo(ConversationRole.USER);
+        assertThat(merged.content()).hasSize(2);
+        assertThat(merged.content())
+                .extracting(block -> block.toolResult().toolUseId())
+                .containsExactly("call-1", "call-2");
+    }
+
+    /** Documented on its AWS model card as supporting structured output. */
+    private static final String CAPABLE_MODEL = 
"anthropic.claude-sonnet-4-5-20250929-v1:0";
+
+    /** Documented on its AWS model card as not supporting structured output. 
*/
+    private static final String INCAPABLE_MODEL = "amazon.nova-micro-v1:0";
+
+    private static final ObjectMapper SCHEMA_MAPPER = new ObjectMapper();
+
+    /**
+     * Output schema fixture shaped to expose Jackson's property model.
+     *
+     * <p>{@code name} is deserialized from {@code full_name} rather than from 
the Java field name,
+     * and {@code secret} is not deserialized at all.
+     */
+    public static class Profile {
+        @JsonProperty("full_name")
+        public String name;
+
+        @JsonIgnore public String secret;
+
+        public int age;
+    }
+
+    /**
+     * Output schema fixture shaped to expose what the derived schema 
constrains: which fields a
+     * response must carry, and how a map field is rendered.
+     */
+    public static class Reading {
+        public int score;
+
+        public Optional<String> note;
+
+        public Map<String, Integer> counts;
+    }
+
+    /**
+     * Output schema fixture whose enum constants are deserialized from values 
other than their
+     * names, one through {@code @JsonProperty} on the constants and one 
through a
+     * {@code @JsonValue} method.
+     */
+    public static class Ticket {
+        public Status status;
+
+        public Phase phase;
+    }
+
+    public enum Status {
+        @JsonProperty("in-progress")
+        IN_PROGRESS,
+        @JsonProperty("done")
+        DONE
+    }
+
+    public enum Phase {
+        STARTED("started"),
+        FINISHED("finished");
+
+        private final String wire;
+
+        Phase(String wire) {
+            this.wire = wire;
+        }
+
+        @JsonValue
+        public String wire() {
+            return wire;
+        }
+    }
+
+    /**
+     * Every model id the connection reports capable.
+     *
+     * <p>The list is the whole allowlist, so an entry dropped or mistyped 
fails here rather than
+     * narrowing capability silently. The differing suffix shapes are part of 
what is under test:
+     * {@code mistral.mistral-large-3-675b-instruct} carries no version 
suffix, {@code
+     * openai.gpt-oss-120b-1:0} carries {@code -1:0}, {@code
+     * anthropic.claude-sonnet-4-5-20250929-v1:0} carries {@code -v1:0}, and 
{@code
+     * anthropic.claude-opus-4-6-v1} carries {@code -v1} with no {@code :0}, 
so a rule that assumes
+     * one shape fails on the others.
+     */
+    private static Stream<String> capableModels() {
+        return Stream.of(
+                "anthropic.claude-sonnet-4-5-20250929-v1:0",
+                "anthropic.claude-opus-4-5-20251101-v1:0",
+                "anthropic.claude-haiku-4-5-20251001-v1:0",
+                "anthropic.claude-opus-4-6-v1",
+                "anthropic.claude-sonnet-4-6",
+                "deepseek.v3-v1:0",
+                "deepseek.v3.2",
+                "google.gemma-3-12b-it",
+                "google.gemma-3-27b-it",
+                "minimax.minimax-m2",
+                "minimax.minimax-m2.1",
+                "minimax.minimax-m2.5",
+                "mistral.mistral-large-3-675b-instruct",
+                "mistral.devstral-2-123b",
+                "mistral.magistral-small-2509",
+                "mistral.ministral-3-14b-instruct",
+                "mistral.ministral-3-3b-instruct",
+                "mistral.ministral-3-8b-instruct",
+                "mistral.voxtral-mini-3b-2507",
+                "mistral.voxtral-small-24b-2507",
+                "moonshot.kimi-k2-thinking",
+                "moonshotai.kimi-k2.5",
+                "nvidia.nemotron-nano-12b-v2",
+                "nvidia.nemotron-nano-3-30b",
+                "nvidia.nemotron-nano-9b-v2",
+                "nvidia.nemotron-super-3-120b",
+                "openai.gpt-oss-120b-1:0",
+                "openai.gpt-oss-20b-1:0",
+                "openai.gpt-5.6-luna",
+                "openai.gpt-oss-safeguard-120b",
+                "openai.gpt-oss-safeguard-20b",
+                "qwen.qwen3-235b-a22b-2507-v1:0",
+                "qwen.qwen3-32b-v1:0",
+                "qwen.qwen3-coder-30b-a3b-v1:0",
+                "qwen.qwen3-coder-480b-a35b-v1:0",
+                "qwen.qwen3-coder-next",
+                "qwen.qwen3-next-80b-a3b",
+                "writer.palmyra-vision-7b",
+                "zai.glm-4.7",
+                "zai.glm-4.7-flash",
+                "zai.glm-5");
+    }
+
+    private static Map<String, Object> params(String model) {
+        Map<String, Object> params = new HashMap<>();
+        params.put("model", model);
+        return params;
+    }
+
+    /**
+     * Reads the schema the request carries.
+     *
+     * <p>Walks the typed accessors because {@code ConverseRequest.toString()} 
prints the structure
+     * as redacted sensitive data.
+     */
+    private static JsonNode nativeSchema(ConverseRequest request) throws 
Exception {
+        return SCHEMA_MAPPER.readTree(
+                
request.outputConfig().textFormat().structure().jsonSchema().schema());
+    }
+
+    @ParameterizedTest
+    @MethodSource("capableModels")
+    @DisplayName("every documented model reports capable")
+    void testCapableModelsReportCapable(String model) {
+        // connection() is configured with a model that is not on the list, so 
a predicate reading
+        // the configured model rather than its argument disagrees with itself 
here.
+        
assertThat(connection().supportsNativeStructuredOutput(model)).isTrue();
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"us.", "eu.", "apac.", "au.", "jp.", "global."})
+    @DisplayName("a geographic inference-profile prefix resolves to the model 
it fronts")
+    void testGeoPrefixResolvesToTheModelItFronts(String prefix) {
+        // A cross-Region inference profile id is a model id behind a leading 
segment, and the model
+        // behind it is the one whose capability the request gets.
+        String profile = prefix + "anthropic.claude-opus-4-6-v1";
+        
assertThat(connection().supportsNativeStructuredOutput(profile)).isTrue();
+    }
+
+    @Test
+    @DisplayName("the us-gov inference-profile prefix resolves to the model it 
fronts")
+    void testHyphenatedPrefixResolvesToTheModelItFronts() {
+        // us-gov. is a documented prefix that no other documented prefix 
resembles, so a rule
+        // written as a fixed set of prefixes tends to omit it while a 
leading-segment strip covers
+        // it without being told.
+        
assertThat(connection().supportsNativeStructuredOutput("us-gov.openai.gpt-oss-120b-1:0"))
+                .isTrue();
+    }
+
+    @Test
+    @DisplayName("a model documented as unsupported reports not capable")
+    void testDocumentedUnsupportedModelReportsNotCapable() {
+        // AWS documents this model as not supporting structured output, and 
it extends the
+        // qwen.qwen3- prefix that several capable entries share. Any prefix 
match claims a
+        // capability the provider denies.
+        
assertThat(connection().supportsNativeStructuredOutput("qwen.qwen3-vl-235b-a22b"))
+                .isFalse();
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                INCAPABLE_MODEL,
+                "anthropic.claude-sonnet-4-20250514-v1:0",
+                "mistral.mistral-large-2402-v1:0",
+                "us.anthropic.claude-sonnet-4-20250514-v1:0"
+            })
+    @DisplayName("a model with no documented answer reports not capable")
+    void testUndocumentedModelsReportNotCapable(String model) {
+        // An absent answer is not a positive one. The middle two each extend 
a capable entry
+        // truncated at a version boundary; the last is the id this module's 
own example uses, so
+        // its behavior is pinned here rather than discovered at the provider.
+        
assertThat(connection().supportsNativeStructuredOutput(model)).isFalse();
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0",
+                
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/anthropic.claude-sonnet-4-5-20250929-v1:0"
+            })
+    @DisplayName("an ARN reports not capable even when it spells out a capable 
model")
+    void testArnFormsReportNotCapable(String model) {
+        // An ARN names a resource rather than a model, and an application 
inference profile's
+        // trailing segment can spell an id it does not front. A substring 
match reports both of
+        // these capable.
+        
assertThat(connection().supportsNativeStructuredOutput(model)).isFalse();
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = {"", " "})
+    @DisplayName("a null or blank model reports not capable")
+    void testNullOrBlankModelReportsNotCapable(String model) {
+        // The guard is load-bearing rather than defensive: the allowlist is 
an immutable Set, whose
+        // contains(null) throws instead of reporting absence.
+        
assertThat(connection().supportsNativeStructuredOutput(model)).isFalse();
+    }
+
+    @Test
+    @DisplayName("the derived schema names properties the way Jackson 
deserializes them")
+    void testDerivedSchemaFollowsJacksonPropertyNames() throws Exception {
+        ConverseRequest request =
+                connection()
+                        .buildRequest(
+                                List.of(ChatMessage.user("hello")),
+                                null,
+                                params(CAPABLE_MODEL),
+                                Profile.class);
+
+        // The response is read back into the same class, so a property that 
@JsonProperty renames
+        // or @JsonIgnore drops has to be stated under the name the mapper 
reads. Derived without
+        // Jackson's property model the schema instead names "name" and 
demands the ignored
+        // "secret", constraining the model to a document the mapper then 
refuses.
+        assertThat(nativeSchema(request).path("properties").fieldNames())
+                .toIterable()
+                .containsExactlyInAnyOrder("full_name", "age");
+    }
+
+    @Test
+    @DisplayName("the derived schema lists enum constants the way Jackson 
deserializes them")
+    void testDerivedSchemaFollowsJacksonEnumValues() throws Exception {
+        ConverseRequest request =
+                connection()
+                        .buildRequest(
+                                List.of(ChatMessage.user("hello")),
+                                null,
+                                params(CAPABLE_MODEL),
+                                Ticket.class);
+        JsonNode properties = nativeSchema(request).path("properties");
+
+        // Every listed value is one the model may emit, so each has to 
deserialize into the enum.
+        // Listed by constant name instead, the mapper refuses every value the 
schema allows.
+        List<Status> statuses = new ArrayList<>();
+        for (JsonNode value : properties.path("status").path("enum")) {
+            statuses.add(SCHEMA_MAPPER.treeToValue(value, Status.class));
+        }
+        assertThat(statuses).containsExactlyInAnyOrder(Status.values());
+
+        List<Phase> phases = new ArrayList<>();
+        for (JsonNode value : properties.path("phase").path("enum")) {
+            phases.add(SCHEMA_MAPPER.treeToValue(value, Phase.class));
+        }
+        assertThat(phases).containsExactlyInAnyOrder(Phase.values());
+    }
+
+    @Test
+    @DisplayName("the derived schema declares its dialect, requires fields, 
and leaves maps bare")
+    void testDerivedSchemaConstrainsTheResponse() throws Exception {
+        ConverseRequest request =
+                connection()
+                        .buildRequest(
+                                List.of(ChatMessage.user("hello")),
+                                null,
+                                params(CAPABLE_MODEL),
+                                Reading.class);
+        JsonNode schema = nativeSchema(request);
+
+        // Left to itself the generator marks nothing required, so an empty 
document satisfies a
+        // schema whose whole purpose is to constrain the response. An 
Optional field is the one a
+        // caller declared omissible, so it stays out of the required set.
+        List<String> required = new ArrayList<>();
+        schema.path("required").forEach(entry -> required.add(entry.asText()));
+        assertThat(required).containsExactlyInAnyOrder("score", "counts");
+
+        // A map derives as a bare object. Typing its values renders them under
+        // additionalProperties, which Bedrock accepts only as false and 
rejects as a subschema, so
+        // the value type is left off rather than putting the request outside 
the accepted subset.
+        
assertThat(schema.path("properties").path("counts").path("type").asText())
+                .isEqualTo("object");
+        assertThat(schema.findValues("additionalProperties")).isEmpty();
+
+        // The dialect is stated rather than left on the generator's older 
default.
+        assertThat(schema.path("$schema").asText())
+                .isEqualTo("https://json-schema.org/draft/2020-12/schema";);
+    }
+
+    @Test
+    @DisplayName("tool definitions and an output schema ride the same request")
+    void testToolConfigAndOutputConfigCoexist() {
+        ConverseRequest request =
+                connection()
+                        .buildRequest(
+                                List.of(ChatMessage.user("hello")),
+                                List.of(new SchemaOnlyTool("{\"type\": 
\"object\"}")),
+                                params(CAPABLE_MODEL),
+                                Profile.class);
+
+        // Converse carries tool definitions and an output schema together, so 
neither branch may
+        // suppress the other.
+        assertThat(request.toolConfig()).isNotNull();
+        assertThat(request.toolConfig().tools()).hasSize(1);
+        assertThat(request.outputConfig()).isNotNull();
+        assertThat(request.outputConfig().textFormat().type())
+                .isEqualTo(OutputFormatType.JSON_SCHEMA);
+    }
+
+    @Test
+    @DisplayName("the native path applies for a POJO class schema on a capable 
model")
+    void testNativeSchemaAppliedWhenGateHolds() throws Exception {
+        ConverseRequest applied =
+                connection()
+                        .buildRequest(
+                                List.of(ChatMessage.user("hello")),
+                                null,
+                                params(CAPABLE_MODEL),
+                                Profile.class);
+
+        assertThat(applied.outputConfig().textFormat().type())
+                .isEqualTo(OutputFormatType.JSON_SCHEMA);
+        // Bedrock takes the schema as serialized text rather than as a typed 
object, so what
+        // arrives has to parse back into the schema document itself.
+        
assertThat(nativeSchema(applied).path("type").asText()).isEqualTo("object");
+    }
+
+    private static Stream<Arguments> gateFailures() {
+        return Stream.of(
+                Arguments.of(INCAPABLE_MODEL, Profile.class),
+                Arguments.of(CAPABLE_MODEL, null),
+                // A RowTypeInfo schema arrives wrapped rather than as a bare 
Class and has no
+                // native translation here, so it degrades to the fallback 
rather than failing.
+                // RowTypeInfo itself is not on this module's test classpath; 
any non-Class object
+                // exercises the same gate.
+                Arguments.of(CAPABLE_MODEL, "row<name STRING>"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("gateFailures")
+    @DisplayName("the native path is skipped for an incapable model or a 
non-POJO schema")
+    void testNativeSchemaSkippedWhenGateFails(String model, Object 
outputSchema) {
+        assertThat(
+                        connection()
+                                .buildRequest(
+                                        List.of(ChatMessage.user("hello")),
+                                        null,
+                                        params(model),
+                                        outputSchema)
+                                .outputConfig())
+                .isNull();
+    }
 }
diff --git a/integrations/pom.xml b/integrations/pom.xml
index 8c7b4cba..624fef8d 100644
--- a/integrations/pom.xml
+++ b/integrations/pom.xml
@@ -36,7 +36,7 @@ under the License.
         <milvus.version>2.6.18</milvus.version>
         <openai.version>4.8.0</openai.version>
         <anthropic.version>2.12.0</anthropic.version>
-        <aws.sdk.version>2.32.16</aws.sdk.version>
+        <aws.sdk.version>2.41.22</aws.sdk.version>
         <google.genai.version>1.56.0</google.genai.version>
     </properties>
 

Reply via email to