Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions go/api/adk/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ type Bedrock struct {
// "5m" (default) or "1h". See the v1alpha2.BedrockConfig CRD doc for the
// cost/compatibility trade-offs of "1h".
CacheTTL string `json:"cache_ttl,omitempty"`
// ReadTimeout is the Bedrock HTTP client read timeout in seconds. Overrides
// botocore's ~60s default, which otherwise aborts long completions with a
// ReadTimeoutError. Nil keeps botocore's default.
ReadTimeout *int `json:"read_timeout,omitempty"`
// ConnectTimeout is the Bedrock HTTP client connect timeout in seconds. Nil
// keeps botocore's default.
ConnectTimeout *int `json:"connect_timeout,omitempty"`
}

func (b *Bedrock) MarshalJSON() ([]byte, error) {
Expand Down
15 changes: 15 additions & 0 deletions go/api/config/crd/bases/kagent.dev_modelconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,12 @@ spec:
- 5m
- 1h
type: string
connectTimeout:
description: |-
ConnectTimeout is the Bedrock HTTP client connect timeout in seconds.
When unset, botocore's default is used.
minimum: 1
type: integer
promptCaching:
default: false
description: |-
Expand All @@ -521,6 +527,15 @@ spec:
See https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
for the current list of supported models and minimum prefix sizes.
type: boolean
readTimeout:
description: |-
ReadTimeout is the Bedrock HTTP client read timeout in seconds. The
underlying botocore client defaults to ~60s, which aborts long
completions (large tool-augmented turns, extended reasoning) with a
ReadTimeoutError. Raise this for agents that make long Converse calls.
When unset, botocore's default is used.
minimum: 1
type: integer
region:
description: AWS region where the Bedrock model is available (e.g.,
us-east-1, us-west-2)
Expand Down
15 changes: 15 additions & 0 deletions go/api/v1alpha2/modelconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,21 @@ type BedrockConfig struct {
// +kubebuilder:validation:Enum="5m";"1h"
// +kubebuilder:default="5m"
CacheTTL string `json:"cacheTTL,omitempty"`

// ReadTimeout is the Bedrock HTTP client read timeout in seconds. The
// underlying botocore client defaults to ~60s, which aborts long
// completions (large tool-augmented turns, extended reasoning) with a
// ReadTimeoutError. Raise this for agents that make long Converse calls.
// When unset, botocore's default is used.
// +optional
// +kubebuilder:validation:Minimum=1
ReadTimeout *int `json:"readTimeout,omitempty"`

// ConnectTimeout is the Bedrock HTTP client connect timeout in seconds.
// When unset, botocore's default is used.
// +optional
// +kubebuilder:validation:Minimum=1
ConnectTimeout *int `json:"connectTimeout,omitempty"`
}

// SAPAICoreConfig contains SAP AI Core-specific configuration options.
Expand Down
10 changes: 10 additions & 0 deletions go/api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,8 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC
AdditionalModelRequestFields: additionalFields,
PromptCaching: model.Spec.Bedrock.PromptCaching,
CacheTTL: model.Spec.Bedrock.CacheTTL,
ReadTimeout: model.Spec.Bedrock.ReadTimeout,
ConnectTimeout: model.Spec.Bedrock.ConnectTimeout,
}

// Populate TLS fields in BaseModel
Expand Down
15 changes: 15 additions & 0 deletions helm/kagent-crds/templates/kagent.dev_modelconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,12 @@ spec:
- 5m
- 1h
type: string
connectTimeout:
description: |-
ConnectTimeout is the Bedrock HTTP client connect timeout in seconds.
When unset, botocore's default is used.
minimum: 1
type: integer
promptCaching:
default: false
description: |-
Expand All @@ -521,6 +527,15 @@ spec:
See https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
for the current list of supported models and minimum prefix sizes.
type: boolean
readTimeout:
description: |-
ReadTimeout is the Bedrock HTTP client read timeout in seconds. The
underlying botocore client defaults to ~60s, which aborts long
completions (large tool-augmented turns, extended reasoning) with a
ReadTimeoutError. Raise this for agents that make long Converse calls.
When unset, botocore's default is used.
minimum: 1
type: integer
region:
description: AWS region where the Bedrock model is available (e.g.,
us-east-1, us-west-2)
Expand Down
24 changes: 24 additions & 0 deletions python/packages/kagent-adk/src/kagent/adk/models/_bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
from typing import TYPE_CHECKING, Any, AsyncGenerator, Optional

import boto3
from botocore.config import Config as BotocoreConfig
from google.adk.models import BaseLlm
from pydantic import Field
from google.adk.models.llm_response import LlmResponse
from google.genai import types

Expand Down Expand Up @@ -85,10 +87,24 @@ def _get_bedrock_client(
tls_disable_verify: Optional[bool] = None,
tls_ca_cert_path: Optional[str] = None,
tls_disable_system_cas: Optional[bool] = None,
read_timeout: Optional[int] = None,
connect_timeout: Optional[int] = None,
):
region = os.environ.get("AWS_DEFAULT_REGION") or os.environ.get("AWS_REGION") or "us-east-1"
kwargs: dict[str, Any] = {"region_name": region}

# botocore defaults to a 60s read timeout, which aborts long Bedrock
# completions (large tool-augmented turns, extended reasoning) with a
# ReadTimeoutError. Allow callers to raise it. Only build a Config when an
# override is set so we keep botocore's defaults otherwise.
timeout_config: dict[str, Any] = {}
if read_timeout is not None:
timeout_config["read_timeout"] = read_timeout
if connect_timeout is not None:
timeout_config["connect_timeout"] = connect_timeout
if timeout_config:
kwargs["config"] = BotocoreConfig(**timeout_config)

if extra_headers:
# boto3 doesn't support custom headers natively; log and ignore
logger.warning("extra_headers are not supported for Bedrock models and will be ignored.")
Expand Down Expand Up @@ -279,6 +295,12 @@ class KAgentBedrockLlm(KAgentTLSMixin, BaseLlm):
# "5m"/None (Bedrock's default 5-minute cache) or "1h" (extended-TTL,
# narrower model support and higher cache-write cost). See _cache_point_block.
cache_ttl: Optional[str] = None
# Bedrock HTTP client timeouts in seconds. read_timeout overrides botocore's
# ~60s default, which otherwise aborts long completions with a
# ReadTimeoutError. None keeps botocore's defaults. Constrained to >= 1 so
# invalid values can't reach the boto3 client (matches the CRD contract).
read_timeout: Optional[int] = Field(default=None, ge=1)
connect_timeout: Optional[int] = Field(default=None, ge=1)

model_config = {"arbitrary_types_allowed": True}

Expand All @@ -289,6 +311,8 @@ def _client(self):
tls_disable_verify=self.tls_disable_verify,
tls_ca_cert_path=self.tls_ca_cert_path,
tls_disable_system_cas=self.tls_disable_system_cas,
read_timeout=self.read_timeout,
connect_timeout=self.connect_timeout,
)

@classmethod
Expand Down
8 changes: 8 additions & 0 deletions python/packages/kagent-adk/src/kagent/adk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,12 @@ class Bedrock(BaseLLM):
# extended-TTL caching. "1h" is supported on fewer models and billed at a
# higher cache-write rate, so it is not strictly better than "5m".
cache_ttl: Literal["5m", "1h"] | None = None
# Bedrock HTTP client timeouts in seconds. read_timeout overrides botocore's
# ~60s default, which otherwise aborts long completions with a
# ReadTimeoutError. None keeps botocore's defaults. Constrained to >= 1 to
# match the ModelConfig CRD (readTimeout/connectTimeout minimum: 1).
read_timeout: int | None = Field(default=None, ge=1)
connect_timeout: int | None = Field(default=None, ge=1)
type: Literal["bedrock"]


Expand Down Expand Up @@ -709,6 +715,8 @@ def _create_llm_from_model_config(model_config: ModelUnion):
additional_model_request_fields=model_config.additional_model_request_fields,
prompt_caching=model_config.prompt_caching,
cache_ttl=model_config.cache_ttl,
read_timeout=model_config.read_timeout,
connect_timeout=model_config.connect_timeout,
**_transport_kwargs(model_config),
)
if model_config.type == "sap_ai_core":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,29 @@ def test_defaults_to_us_east_1(self):
_get_bedrock_client()
assert mock_boto.call_args.kwargs["region_name"] == "us-east-1"

def test_no_config_when_timeouts_unset(self):
with mock.patch("kagent.adk.models._bedrock.boto3.client") as mock_boto:
_get_bedrock_client()
assert "config" not in mock_boto.call_args.kwargs

def test_read_timeout_sets_botocore_config(self):
from botocore.config import Config as BotocoreConfig

with mock.patch("kagent.adk.models._bedrock.boto3.client") as mock_boto:
_get_bedrock_client(read_timeout=1800)
config = mock_boto.call_args.kwargs["config"]
assert config.read_timeout == 1800
# connect_timeout untouched -> botocore keeps its own default,
# whatever that is for the installed version (don't hard-code it).
assert config.connect_timeout == BotocoreConfig().connect_timeout

def test_read_and_connect_timeout(self):
with mock.patch("kagent.adk.models._bedrock.boto3.client") as mock_boto:
_get_bedrock_client(read_timeout=900, connect_timeout=10)
config = mock_boto.call_args.kwargs["config"]
assert config.read_timeout == 900
assert config.connect_timeout == 10


class TestKAgentBedrockLlm:
def test_default_construction(self):
Expand All @@ -182,6 +205,19 @@ def test_non_anthropic_model_accepted(self):
llm = KAgentBedrockLlm(model="meta.llama3-8b-instruct-v1:0")
assert llm.model == "meta.llama3-8b-instruct-v1:0"

def test_positive_timeouts_accepted(self):
llm = KAgentBedrockLlm(model="meta.llama3-8b-instruct-v1:0", read_timeout=1800, connect_timeout=10)
assert llm.read_timeout == 1800
assert llm.connect_timeout == 10

@pytest.mark.parametrize("field", ["read_timeout", "connect_timeout"])
@pytest.mark.parametrize("value", [0, -5])
def test_non_positive_timeouts_rejected(self, field, value):
from pydantic import ValidationError

with pytest.raises(ValidationError):
KAgentBedrockLlm(model="meta.llama3-8b-instruct-v1:0", **{field: value})

@pytest.mark.asyncio
async def test_generate_calls_converse(self):
llm = KAgentBedrockLlm(model="us.anthropic.claude-sonnet-4-20250514-v1:0")
Expand Down
Loading