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
9 changes: 7 additions & 2 deletions go/adk/pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,14 @@ func CreateLLM(ctx context.Context, m adk.Model, log logr.Logger) (adkmodel.LLM,
if modelName == "" {
return nil, fmt.Errorf("bedrock requires a model name (e.g. anthropic.claude-3-sonnet-20240229-v1:0)")
}
// Use Bedrock Converse API for ALL models (including Anthropic)
// Use Bedrock Converse API for ALL models (including Anthropic).
// ReadTimeout maps to the overall HTTP client timeout (whole Converse
// request) and ConnectTimeout to the dialer, mirroring the Python ADK's
// botocore read/connect timeouts so the config is honored on both runtimes.
tc := transportConfigFromBase(m.BaseModel, m.ReadTimeout)
tc.ConnectTimeout = m.ConnectTimeout
cfg := &models.BedrockConfig{
TransportConfig: transportConfigFromBase(m.BaseModel, nil),
TransportConfig: tc,
Model: modelName,
Region: region,
AdditionalModelRequestFields: m.AdditionalModelRequestFields,
Expand Down
54 changes: 54 additions & 0 deletions go/adk/pkg/agent/agent_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package agent

import (
"context"
"encoding/json"
"os"
"path/filepath"
Expand Down Expand Up @@ -286,6 +287,59 @@ func TestConfigDeserialization_Bedrock(t *testing.T) {
}
}

// TestCreateLLM_BedrockTimeouts verifies that the Bedrock read/connect timeouts
// from the model config are wired into the Go ADK's HTTP transport, so the same
// ModelConfig fields are honored by both the Python and Go runtimes.
func TestCreateLLM_BedrockTimeouts(t *testing.T) {
read := 1800
connect := 10
m := &adk.Bedrock{
BaseModel: adk.BaseModel{Type: "bedrock", Model: "anthropic.claude-3-sonnet-20240229-v1:0"},
Region: "us-east-1",
ReadTimeout: &read,
ConnectTimeout: &connect,
}

llm, err := CreateLLM(context.Background(), m, logr.Discard())
if err != nil {
t.Fatalf("CreateLLM: %v", err)
}
bm, ok := llm.(*models.BedrockModel)
if !ok {
t.Fatalf("model is %T, want *models.BedrockModel", llm)
}
if bm.Config.Timeout == nil || *bm.Config.Timeout != read {
t.Errorf("Config.Timeout = %v, want %d", bm.Config.Timeout, read)
}
if bm.Config.ConnectTimeout == nil || *bm.Config.ConnectTimeout != connect {
t.Errorf("Config.ConnectTimeout = %v, want %d", bm.Config.ConnectTimeout, connect)
}
}

// TestCreateLLM_BedrockTimeoutsUnset verifies that omitting the timeouts leaves
// them nil so each runtime keeps its own default behavior.
func TestCreateLLM_BedrockTimeoutsUnset(t *testing.T) {
m := &adk.Bedrock{
BaseModel: adk.BaseModel{Type: "bedrock", Model: "anthropic.claude-3-sonnet-20240229-v1:0"},
Region: "us-east-1",
}

llm, err := CreateLLM(context.Background(), m, logr.Discard())
if err != nil {
t.Fatalf("CreateLLM: %v", err)
}
bm, ok := llm.(*models.BedrockModel)
if !ok {
t.Fatalf("model is %T, want *models.BedrockModel", llm)
}
if bm.Config.Timeout != nil {
t.Errorf("Config.Timeout = %v, want nil", *bm.Config.Timeout)
}
if bm.Config.ConnectTimeout != nil {
t.Errorf("Config.ConnectTimeout = %v, want nil", *bm.Config.ConnectTimeout)
}
}

func TestBuildAgentTools_WiresSkillsToolsFromEnv(t *testing.T) {
skillsDir := t.TempDir()
skillDir := filepath.Join(skillsDir, "csv-to-json")
Expand Down
30 changes: 29 additions & 1 deletion go/adk/pkg/models/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package models

import (
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
Expand All @@ -19,7 +21,8 @@ type TransportConfig struct {
TLSCACertPath *string
TLSDisableSystemCAs *bool
APIKeyPassthrough bool
Timeout *int // seconds; nil = defaultTimeout
Timeout *int // seconds; nil = defaultTimeout. Bounds the whole request (connect + write + read).
ConnectTimeout *int // seconds; nil = Go transport default. Bounds connection establishment only.
}

// BuildHTTPClient creates an http.Client with the full transport stack:
Expand All @@ -35,6 +38,16 @@ func BuildHTTPClient(tc TransportConfig) (*http.Client, error) {
return nil, err
}

// Apply the connect timeout to the transport's dialer. Clone first so we
// never mutate the shared http.DefaultTransport that BuildTLSTransport
// returns when no TLS override is set.
if tc.ConnectTimeout != nil {
transport, err = withConnectTimeout(transport, time.Duration(*tc.ConnectTimeout)*time.Second)
if err != nil {
return nil, err
}
}

if len(tc.Headers) > 0 {
transport = &headerTransport{base: transport, headers: tc.Headers}
}
Expand All @@ -47,6 +60,21 @@ func BuildHTTPClient(tc TransportConfig) (*http.Client, error) {
return &http.Client{Timeout: timeout, Transport: transport}, nil
}

// withConnectTimeout returns a copy of base (which must be an *http.Transport)
// whose DialContext enforces the given connection-establishment timeout. The
// base transport is cloned so callers can safely pass http.DefaultTransport
// without mutating the shared global.
func withConnectTimeout(base http.RoundTripper, connectTimeout time.Duration) (http.RoundTripper, error) {
t, ok := base.(*http.Transport)
if !ok {
return nil, fmt.Errorf("BuildHTTPClient: connect timeout requires an *http.Transport base, got %T", base)
}
cloned := t.Clone()
dialer := &net.Dialer{Timeout: connectTimeout, KeepAlive: 30 * time.Second}
cloned.DialContext = dialer.DialContext
return cloned, nil
}

// BearerTokenKey is the context key for storing the bearer token for API key passthrough
var BearerTokenKey = &contextKey{}

Expand Down
30 changes: 30 additions & 0 deletions go/adk/pkg/models/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package models

import (
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"os"
Expand Down Expand Up @@ -134,6 +135,35 @@ func TestBuildHTTPClient_Timeout(t *testing.T) {
}
}

// Should set a connect timeout on a cloned transport without mutating the
// shared http.DefaultTransport.
func TestBuildHTTPClient_ConnectTimeout(t *testing.T) {
def, ok := http.DefaultTransport.(*http.Transport)
if !ok {
t.Fatalf("http.DefaultTransport is %T, want *http.Transport", http.DefaultTransport)
}
origDial := fmt.Sprintf("%p", def.DialContext)

seconds := 7
client, err := BuildHTTPClient(TransportConfig{ConnectTimeout: &seconds})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
tr, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("expected *http.Transport, got %T", client.Transport)
}
if tr == def {
t.Fatal("expected a cloned transport, got shared http.DefaultTransport")
}
if tr.DialContext == nil {
t.Fatal("expected DialContext to be set for connect timeout")
}
if newDial := fmt.Sprintf("%p", def.DialContext); newDial != origDial {
t.Error("http.DefaultTransport.DialContext was mutated by BuildHTTPClient")
}
}

// Should inject headers if specified
func TestBuildHTTPClient_HeadersInjected(t *testing.T) {
var got string
Expand Down
9 changes: 9 additions & 0 deletions go/api/adk/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,15 @@ 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. Nil keeps
// each runtime's default. Python ADK: overrides botocore's ~60s read timeout,
// which otherwise aborts long completions with a ReadTimeoutError. Go ADK:
// bounds the whole Converse request (overall HTTP client timeout, default 30m).
ReadTimeout *int `json:"read_timeout,omitempty"`
// ConnectTimeout is the Bedrock HTTP client connection-establishment timeout
// in seconds. Nil keeps each runtime's default (Python ADK: botocore; Go ADK:
// net dialer). Bounds connection setup only, not the response read.
ConnectTimeout *int `json:"connect_timeout,omitempty"`
}

func (b *Bedrock) MarshalJSON() ([]byte, error) {
Expand Down
19 changes: 19 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,14 @@ spec:
- 5m
- 1h
type: string
connectTimeout:
description: |-
ConnectTimeout is the Bedrock HTTP client connection-establishment timeout
in seconds, applied by both the Python and Go ADK runtimes. It bounds
connection setup only, not the response read. When unset, each runtime's
default is used (Python ADK: botocore; Go ADK: net dialer).
minimum: 1
type: integer
promptCaching:
default: false
description: |-
Expand All @@ -521,6 +529,17 @@ 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, applied by
both the Python and Go ADK runtimes. Raise this for agents that make long
Converse calls (large tool-augmented turns, extended reasoning). On the
Python ADK it overrides botocore's ~60s read timeout, which otherwise
aborts long completions with a ReadTimeoutError; on the Go ADK it bounds
the whole Converse request (default 30m). When unset, each runtime'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
19 changes: 19 additions & 0 deletions go/api/v1alpha2/modelconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,25 @@ 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, applied by
// both the Python and Go ADK runtimes. Raise this for agents that make long
// Converse calls (large tool-augmented turns, extended reasoning). On the
// Python ADK it overrides botocore's ~60s read timeout, which otherwise
// aborts long completions with a ReadTimeoutError; on the Go ADK it bounds
// the whole Converse request (default 30m). When unset, each runtime's
// default is used.
// +optional
// +kubebuilder:validation:Minimum=1
ReadTimeout *int `json:"readTimeout,omitempty"`

// ConnectTimeout is the Bedrock HTTP client connection-establishment timeout
// in seconds, applied by both the Python and Go ADK runtimes. It bounds
// connection setup only, not the response read. When unset, each runtime's
// default is used (Python ADK: botocore; Go ADK: net dialer).
// +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 @@ -831,6 +831,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
19 changes: 19 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,14 @@ spec:
- 5m
- 1h
type: string
connectTimeout:
description: |-
ConnectTimeout is the Bedrock HTTP client connection-establishment timeout
in seconds, applied by both the Python and Go ADK runtimes. It bounds
connection setup only, not the response read. When unset, each runtime's
default is used (Python ADK: botocore; Go ADK: net dialer).
minimum: 1
type: integer
promptCaching:
default: false
description: |-
Expand All @@ -521,6 +529,17 @@ 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, applied by
both the Python and Go ADK runtimes. Raise this for agents that make long
Converse calls (large tool-augmented turns, extended reasoning). On the
Python ADK it overrides botocore's ~60s read timeout, which otherwise
aborts long completions with a ReadTimeoutError; on the Go ADK it bounds
the whole Converse request (default 30m). When unset, each runtime'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
Loading
Loading