feat(ai-proxy): add Bearer Token authentication support for Bedrock p… (#3305)

This commit is contained in:
CZJCC
2026-01-07 19:39:20 +08:00
committed by jingze
parent 357418853f
commit fc600f204a
7 changed files with 635 additions and 29 deletions

View File

@@ -322,23 +322,31 @@ Google Vertex AI 所对应的 type 为 vertex。它特有的配置字段如下
#### AWS Bedrock
AWS Bedrock 所对应的 type 为 bedrock。它特有的配置字段如下
AWS Bedrock 所对应的 type 为 bedrock。它支持两种认证方式
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|---------------------------|--------|------|-----|------------------------------|
| `modelVersion` | string | 非必填 | - | 用于指定 Triton Server 中 model version |
| `tritonDomain` | string | 非必填 | - | Triton Server 部署的指定请求 Domain |
1. **AWS Signature V4 认证**:使用 `awsAccessKey``awsSecretKey` 进行 AWS 标准签名认证
2. **Bearer Token 认证**:使用 `apiTokens` 配置 AWS Bearer Token适用于 IAM Identity Center 等场景)
**注意**:两种认证方式二选一,如果同时配置了 `apiTokens`,将优先使用 Bearer Token 认证方式。
它特有的配置字段如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|---------------------------|---------------|-------------------|-------|---------------------------------------------------|
| `apiTokens` | array of string | 与 ak/sk 二选一 | - | AWS Bearer Token用于 Bearer Token 认证方式 |
| `awsAccessKey` | string | 与 apiTokens 二选一 | - | AWS Access Key用于 AWS Signature V4 认证 |
| `awsSecretKey` | string | 与 apiTokens 二选一 | - | AWS Secret Access Key用于 AWS Signature V4 认证 |
| `awsRegion` | string | 必填 | - | AWS 区域例如us-east-1 |
| `bedrockAdditionalFields` | map | 非必填 | - | Bedrock 额外模型请求参数 |
#### NVIDIA Triton Interference Server
NVIDIA Triton Interference Server 所对应的 type 为 triton。它特有的配置字段如下
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|---------------------------|--------|------|-----|------------------------------|
| `awsAccessKey` | string | 必填 | - | AWS Access Key用于身份认证 |
| `awsSecretKey` | string | 必填 | - | AWS Secret Access Key用于身份认证 |
| `awsRegion` | string | 必填 | - | AWS 区域例如us-east-1 |
| `bedrockAdditionalFields` | map | 非必填 | - | Bedrock 额外模型请求参数 |
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|----------------------|--------|--------|-------|------------------------------------------|
| `tritonModelVersion` | string | 必填 | - | 用于指定 Triton Server 中 model version |
| `tritonDomain` | string | 必填 | - | Triton Server 部署的指定请求 Domain |
## 用法示例
@@ -2011,6 +2019,10 @@ provider:
### 使用 OpenAI 协议代理 AWS Bedrock 服务
AWS Bedrock 支持两种认证方式:
#### 方式一:使用 AWS Access Key/Secret Key 认证AWS Signature V4
**配置信息**
```yaml
@@ -2018,7 +2030,21 @@ provider:
type: bedrock
awsAccessKey: "YOUR_AWS_ACCESS_KEY_ID"
awsSecretKey: "YOUR_AWS_SECRET_ACCESS_KEY"
awsRegion: "YOUR_AWS_REGION"
awsRegion: "us-east-1"
bedrockAdditionalFields:
top_k: 200
```
#### 方式二:使用 Bearer Token 认证(适用于 IAM Identity Center 等场景)
**配置信息**
```yaml
provider:
type: bedrock
apiTokens:
- "YOUR_AWS_BEARER_TOKEN"
awsRegion: "us-east-1"
bedrockAdditionalFields:
top_k: 200
```
@@ -2027,7 +2053,7 @@ provider:
```json
{
"model": "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-3-5-haiku-20241022-v1:0",
"model": "us.anthropic.claude-3-5-haiku-20241022-v1:0",
"messages": [
{
"role": "user",

View File

@@ -268,14 +268,22 @@ For Vertex, the corresponding `type` is `vertex`. Its unique configuration field
#### AWS Bedrock
For AWS Bedrock, the corresponding `type` is `bedrock`. Its unique configuration field is:
For AWS Bedrock, the corresponding `type` is `bedrock`. It supports two authentication methods:
| Name | Data Type | Requirement | Default | Description |
|---------------------------|-----------|-------------|---------|---------------------------------------------------------|
| `awsAccessKey` | string | Required | - | AWS Access Key used for authentication |
| `awsSecretKey` | string | Required | - | AWS Secret Access Key used for authentication |
| `awsRegion` | string | Required | - | AWS region, e.g., us-east-1 |
| `bedrockAdditionalFields` | map | Optional | - | Additional inference parameters that the model supports |
1. **AWS Signature V4 Authentication**: Uses `awsAccessKey` and `awsSecretKey` for standard AWS signature authentication
2. **Bearer Token Authentication**: Uses `apiTokens` to configure AWS Bearer Token (suitable for IAM Identity Center and similar scenarios)
**Note**: Choose one of the two authentication methods. If `apiTokens` is configured, Bearer Token authentication will be used preferentially.
Its unique configuration fields are:
| Name | Data Type | Requirement | Default | Description |
|---------------------------|-----------------|--------------------------|---------|-------------------------------------------------------------------|
| `apiTokens` | array of string | Either this or ak/sk | - | AWS Bearer Token for Bearer Token authentication |
| `awsAccessKey` | string | Either this or apiTokens | - | AWS Access Key for AWS Signature V4 authentication |
| `awsSecretKey` | string | Either this or apiTokens | - | AWS Secret Access Key for AWS Signature V4 authentication |
| `awsRegion` | string | Required | - | AWS region, e.g., us-east-1 |
| `bedrockAdditionalFields` | map | Optional | - | Additional inference parameters that the model supports |
## Usage Examples
@@ -1779,13 +1787,31 @@ provider:
```
### Utilizing OpenAI Protocol Proxy for AWS Bedrock Services
AWS Bedrock supports two authentication methods:
#### Method 1: Using AWS Access Key/Secret Key Authentication (AWS Signature V4)
**Configuration Information**
```yaml
provider:
type: bedrock
awsAccessKey: "YOUR_AWS_ACCESS_KEY_ID"
awsSecretKey: "YOUR_AWS_SECRET_ACCESS_KEY"
awsRegion: "YOUR_AWS_REGION"
awsRegion: "us-east-1"
bedrockAdditionalFields:
top_k: 200
```
#### Method 2: Using Bearer Token Authentication (suitable for IAM Identity Center and similar scenarios)
**Configuration Information**
```yaml
provider:
type: bedrock
apiTokens:
- "YOUR_AWS_BEARER_TOKEN"
awsRegion: "us-east-1"
bedrockAdditionalFields:
top_k: 200
```
@@ -1793,7 +1819,7 @@ provider:
**Request Example**
```json
{
"model": "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-3-5-haiku-20241022-v1:0",
"model": "us.anthropic.claude-3-5-haiku-20241022-v1:0",
"messages": [
{
"role": "user",

View File

@@ -8,7 +8,7 @@ toolchain go1.24.4
require (
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2
github.com/higress-group/wasm-go v1.0.7-0.20251209122854-7e766df5675c
github.com/higress-group/wasm-go v1.0.9-0.20251226032831-95da539a1ec7
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.18.0
)

View File

@@ -6,6 +6,8 @@ github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2 h1
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
github.com/higress-group/wasm-go v1.0.7-0.20251209122854-7e766df5675c h1:DdVPyaMHSYBqO5jwB9Wl3PqsBGIf4u29BHMI0uIVB1Y=
github.com/higress-group/wasm-go v1.0.7-0.20251209122854-7e766df5675c/go.mod h1:uKVYICbRaxTlKqdm8E0dpjbysxM8uCPb9LV26hF3Km8=
github.com/higress-group/wasm-go v1.0.9-0.20251226032831-95da539a1ec7 h1:ddAkPFIIf6isVQNymS6+X6QO51/WV0Af4Afb9a2z9TE=
github.com/higress-group/wasm-go v1.0.9-0.20251226032831-95da539a1ec7/go.mod h1:uKVYICbRaxTlKqdm8E0dpjbysxM8uCPb9LV26hF3Km8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=

View File

@@ -128,3 +128,11 @@ func TestGeneric(t *testing.T) {
test.RunGenericOnHttpRequestHeadersTests(t)
test.RunGenericOnHttpRequestBodyTests(t)
}
func TestBedrock(t *testing.T) {
test.RunBedrockParseConfigTests(t)
test.RunBedrockOnHttpRequestHeadersTests(t)
test.RunBedrockOnHttpRequestBodyTests(t)
test.RunBedrockOnHttpResponseHeadersTests(t)
test.RunBedrockOnHttpResponseBodyTests(t)
}

View File

@@ -43,8 +43,11 @@ const (
type bedrockProviderInitializer struct{}
func (b *bedrockProviderInitializer) ValidateConfig(config *ProviderConfig) error {
if len(config.awsAccessKey) == 0 || len(config.awsSecretKey) == 0 {
return errors.New("missing bedrock access authentication parameters")
hasAkSk := len(config.awsAccessKey) > 0 && len(config.awsSecretKey) > 0
hasApiToken := len(config.apiTokens) > 0
if !hasAkSk && !hasApiToken {
return errors.New("missing bedrock access authentication parameters: either apiTokens or (awsAccessKey + awsSecretKey) is required")
}
if len(config.awsRegion) == 0 {
return errors.New("missing bedrock region parameters")
@@ -634,6 +637,13 @@ func (b *bedrockProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiN
func (b *bedrockProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
util.OverwriteRequestHostHeader(headers, fmt.Sprintf(bedrockDefaultDomain, b.config.awsRegion))
// If apiTokens is configured, set Bearer token authentication here
// This follows the same pattern as other providers (qwen, zhipuai, etc.)
// AWS SigV4 authentication is handled in setAuthHeaders because it requires the request body
if len(b.config.apiTokens) > 0 {
util.OverwriteRequestAuthorizationHeader(headers, "Bearer "+b.config.GetApiTokenInUse(ctx))
}
}
func (b *bedrockProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
@@ -659,18 +669,18 @@ func (b *bedrockProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName
case ApiNameChatCompletion:
return b.onChatCompletionResponseBody(ctx, body)
case ApiNameImageGeneration:
return b.onImageGenerationResponseBody(ctx, body)
return b.onImageGenerationResponseBody(body)
}
return nil, errUnsupportedApiName
}
func (b *bedrockProvider) onImageGenerationResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
func (b *bedrockProvider) onImageGenerationResponseBody(body []byte) ([]byte, error) {
bedrockResponse := &bedrockImageGenerationResponse{}
if err := json.Unmarshal(body, bedrockResponse); err != nil {
log.Errorf("unable to unmarshal bedrock image gerneration response: %v", err)
return nil, fmt.Errorf("unable to unmarshal bedrock image generation response: %v", err)
}
response := b.buildBedrockImageGenerationResponse(ctx, bedrockResponse)
response := b.buildBedrockImageGenerationResponse(bedrockResponse)
return json.Marshal(response)
}
@@ -710,7 +720,7 @@ func (b *bedrockProvider) buildBedrockImageGenerationRequest(origRequest *imageG
return requestBytes, err
}
func (b *bedrockProvider) buildBedrockImageGenerationResponse(ctx wrapper.HttpContext, bedrockResponse *bedrockImageGenerationResponse) *imageGenerationResponse {
func (b *bedrockProvider) buildBedrockImageGenerationResponse(bedrockResponse *bedrockImageGenerationResponse) *imageGenerationResponse {
data := make([]imageGenerationData, len(bedrockResponse.Images))
for i, image := range bedrockResponse.Images {
data[i] = imageGenerationData{
@@ -1138,6 +1148,13 @@ func chatMessage2BedrockMessage(chatMessage chatMessage) bedrockMessage {
}
func (b *bedrockProvider) setAuthHeaders(body []byte, headers http.Header) {
// Bearer token authentication is already set in TransformRequestHeaders
// This function only handles AWS SigV4 authentication which requires the request body
if len(b.config.apiTokens) > 0 {
return
}
// Use AWS Signature V4 authentication
t := time.Now().UTC()
amzDate := t.Format("20060102T150405Z")
dateStamp := t.Format("20060102")

View File

@@ -0,0 +1,527 @@
package test
import (
"encoding/json"
"testing"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/test"
"github.com/stretchr/testify/require"
)
// Test config: Basic Bedrock config with AWS Access Key/Secret Key (AWS Signature V4)
var basicBedrockConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"provider": map[string]interface{}{
"type": "bedrock",
"awsAccessKey": "test-ak-for-unit-test",
"awsSecretKey": "test-sk-for-unit-test",
"awsRegion": "us-east-1",
"modelMapping": map[string]string{
"*": "anthropic.claude-3-5-haiku-20241022-v1:0",
},
},
})
return data
}()
// Test config: Bedrock config with Bearer Token authentication
var bedrockApiTokenConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"provider": map[string]interface{}{
"type": "bedrock",
"apiTokens": []string{
"test-token-for-unit-test",
},
"awsRegion": "us-east-1",
"modelMapping": map[string]string{
"*": "anthropic.claude-3-5-haiku-20241022-v1:0",
},
},
})
return data
}()
// Test config: Bedrock config with multiple Bearer Tokens
var bedrockMultiTokenConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"provider": map[string]interface{}{
"type": "bedrock",
"apiTokens": []string{
"test-token-1-for-unit-test",
"test-token-2-for-unit-test",
},
"awsRegion": "us-west-2",
"modelMapping": map[string]string{
"gpt-4": "anthropic.claude-3-opus-20240229-v1:0",
"*": "anthropic.claude-3-haiku-20240307-v1:0",
},
},
})
return data
}()
// Test config: Bedrock config with additional fields
var bedrockWithAdditionalFieldsConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"provider": map[string]interface{}{
"type": "bedrock",
"awsAccessKey": "test-ak-for-unit-test",
"awsSecretKey": "test-sk-for-unit-test",
"awsRegion": "us-east-1",
"bedrockAdditionalFields": map[string]interface{}{
"top_k": 200,
},
"modelMapping": map[string]string{
"*": "anthropic.claude-3-5-haiku-20241022-v1:0",
},
},
})
return data
}()
// Test config: Invalid config - missing both apiTokens and ak/sk
var bedrockInvalidConfigMissingAuth = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"provider": map[string]interface{}{
"type": "bedrock",
"awsRegion": "us-east-1",
"modelMapping": map[string]string{
"*": "anthropic.claude-3-5-haiku-20241022-v1:0",
},
},
})
return data
}()
// Test config: Invalid config - missing region
var bedrockInvalidConfigMissingRegion = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"provider": map[string]interface{}{
"type": "bedrock",
"apiTokens": []string{
"test-token-for-unit-test",
},
"modelMapping": map[string]string{
"*": "anthropic.claude-3-5-haiku-20241022-v1:0",
},
},
})
return data
}()
// Test config: Invalid config - only has access key without secret key
var bedrockInvalidConfigPartialAkSk = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"provider": map[string]interface{}{
"type": "bedrock",
"awsAccessKey": "test-ak-for-unit-test",
"awsRegion": "us-east-1",
"modelMapping": map[string]string{
"*": "anthropic.claude-3-5-haiku-20241022-v1:0",
},
},
})
return data
}()
func RunBedrockParseConfigTests(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
// Test basic Bedrock config with AWS Signature V4 authentication
t.Run("basic bedrock config with ak/sk", func(t *testing.T) {
host, status := test.NewTestHost(basicBedrockConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
})
// Test Bedrock config with Bearer Token authentication
t.Run("bedrock config with api token", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
})
// Test Bedrock config with multiple tokens
t.Run("bedrock config with multiple tokens", func(t *testing.T) {
host, status := test.NewTestHost(bedrockMultiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
})
// Test Bedrock config with additional fields
t.Run("bedrock config with additional fields", func(t *testing.T) {
host, status := test.NewTestHost(bedrockWithAdditionalFieldsConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
})
// Test invalid config - missing authentication
t.Run("bedrock invalid config missing auth", func(t *testing.T) {
host, status := test.NewTestHost(bedrockInvalidConfigMissingAuth)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusFailed, status)
})
// Test invalid config - missing region
t.Run("bedrock invalid config missing region", func(t *testing.T) {
host, status := test.NewTestHost(bedrockInvalidConfigMissingRegion)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusFailed, status)
})
// Test invalid config - partial ak/sk (only access key, no secret key)
t.Run("bedrock invalid config partial ak/sk", func(t *testing.T) {
host, status := test.NewTestHost(bedrockInvalidConfigPartialAkSk)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusFailed, status)
})
})
}
func RunBedrockOnHttpRequestHeadersTests(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// Test Bedrock request headers processing with AWS Signature V4
t.Run("bedrock chat completion request headers with ak/sk", func(t *testing.T) {
host, status := test.NewTestHost(basicBedrockConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// Set request headers
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
// Verify request headers
requestHeaders := host.GetRequestHeaders()
require.NotNil(t, requestHeaders)
// Verify Host is changed to Bedrock service domain
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
require.True(t, hasHost, "Host header should exist")
require.Contains(t, hostValue, "bedrock-runtime.us-east-1.amazonaws.com", "Host should be changed to Bedrock service domain")
})
// Test Bedrock request headers processing with Bearer Token
t.Run("bedrock chat completion request headers with api token", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// Set request headers
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
// Verify request headers
requestHeaders := host.GetRequestHeaders()
require.NotNil(t, requestHeaders)
// Verify Host is changed to Bedrock service domain
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
require.True(t, hasHost, "Host header should exist")
require.Contains(t, hostValue, "bedrock-runtime.us-east-1.amazonaws.com", "Host should be changed to Bedrock service domain")
})
})
}
func RunBedrockOnHttpRequestBodyTests(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// Test Bedrock request body processing with Bearer Token authentication
t.Run("bedrock chat completion request body with api token", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// Set request headers
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
// Set request body
requestBody := `{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "Hello, how are you?"
}
],
"temperature": 0.7
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
// Verify request headers for Bearer Token authentication
requestHeaders := host.GetRequestHeaders()
require.NotNil(t, requestHeaders)
// Verify Authorization header uses Bearer token
authValue, hasAuth := test.GetHeaderValue(requestHeaders, "Authorization")
require.True(t, hasAuth, "Authorization header should exist")
require.Contains(t, authValue, "Bearer ", "Authorization should use Bearer token")
require.Contains(t, authValue, "test-token-for-unit-test", "Authorization should contain the configured token")
// Verify path is transformed to Bedrock format
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Contains(t, pathValue, "/model/", "Path should contain Bedrock model path")
require.Contains(t, pathValue, "/converse", "Path should contain converse endpoint")
})
// Test Bedrock request body processing with AWS Signature V4 authentication
t.Run("bedrock chat completion request body with ak/sk", func(t *testing.T) {
host, status := test.NewTestHost(basicBedrockConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// Set request headers
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
// Set request body
requestBody := `{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "Hello, how are you?"
}
],
"temperature": 0.7
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
// Verify request headers for AWS Signature V4 authentication
requestHeaders := host.GetRequestHeaders()
require.NotNil(t, requestHeaders)
// Verify Authorization header uses AWS Signature
authValue, hasAuth := test.GetHeaderValue(requestHeaders, "Authorization")
require.True(t, hasAuth, "Authorization header should exist")
require.Contains(t, authValue, "AWS4-HMAC-SHA256", "Authorization should use AWS4-HMAC-SHA256 signature")
require.Contains(t, authValue, "Credential=", "Authorization should contain Credential")
require.Contains(t, authValue, "Signature=", "Authorization should contain Signature")
// Verify X-Amz-Date header exists
dateValue, hasDate := test.GetHeaderValue(requestHeaders, "X-Amz-Date")
require.True(t, hasDate, "X-Amz-Date header should exist for AWS Signature V4")
require.NotEmpty(t, dateValue, "X-Amz-Date should not be empty")
// Verify path is transformed to Bedrock format
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Contains(t, pathValue, "/model/", "Path should contain Bedrock model path")
require.Contains(t, pathValue, "/converse", "Path should contain converse endpoint")
})
// Test Bedrock streaming request
t.Run("bedrock streaming request", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// Set request headers
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
// Set streaming request body
requestBody := `{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "Hello"
}
],
"stream": true
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
// Verify path is transformed to Bedrock streaming format
requestHeaders := host.GetRequestHeaders()
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
require.True(t, hasPath, "Path header should exist")
require.Contains(t, pathValue, "/model/", "Path should contain Bedrock model path")
require.Contains(t, pathValue, "/converse-stream", "Path should contain converse-stream endpoint for streaming")
})
})
}
func RunBedrockOnHttpResponseHeadersTests(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// Test Bedrock response headers processing
t.Run("bedrock response headers", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// Set request headers
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
// Set request body
requestBody := `{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "Hello"
}
]
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
// Process response headers
action = host.CallOnHttpResponseHeaders([][2]string{
{":status", "200"},
{"Content-Type", "application/json"},
{"X-Amzn-Requestid", "test-request-id-12345"},
})
require.Equal(t, types.ActionContinue, action)
// Verify response headers
responseHeaders := host.GetResponseHeaders()
require.NotNil(t, responseHeaders)
// Verify status code
statusValue, hasStatus := test.GetHeaderValue(responseHeaders, ":status")
require.True(t, hasStatus, "Status header should exist")
require.Equal(t, "200", statusValue, "Status should be 200")
})
})
}
func RunBedrockOnHttpResponseBodyTests(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// Test Bedrock response body processing
t.Run("bedrock response body", func(t *testing.T) {
host, status := test.NewTestHost(bedrockApiTokenConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// Set request headers
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
// Set request body
requestBody := `{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "Hello"
}
]
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
// Set response property to ensure IsResponseFromUpstream() returns true
host.SetProperty([]string{"response", "code_details"}, []byte("via_upstream"))
// Process response headers (must include :status 200 for body processing)
action = host.CallOnHttpResponseHeaders([][2]string{
{":status", "200"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.ActionContinue, action)
// Process response body (Bedrock format)
responseBody := `{
"output": {
"message": {
"role": "assistant",
"content": [
{
"text": "Hello! How can I help you today?"
}
]
}
},
"stopReason": "end_turn",
"usage": {
"inputTokens": 10,
"outputTokens": 15,
"totalTokens": 25
}
}`
action = host.CallOnHttpResponseBody([]byte(responseBody))
require.Equal(t, types.ActionContinue, action)
// Verify response body is transformed to OpenAI format
transformedResponseBody := host.GetResponseBody()
require.NotNil(t, transformedResponseBody)
var responseMap map[string]interface{}
err := json.Unmarshal(transformedResponseBody, &responseMap)
require.NoError(t, err)
// Verify choices exist in transformed response
choices, exists := responseMap["choices"]
require.True(t, exists, "Choices should exist in response body")
require.NotNil(t, choices, "Choices should not be nil")
// Verify usage exists
usage, exists := responseMap["usage"]
require.True(t, exists, "Usage should exist in response body")
require.NotNil(t, usage, "Usage should not be nil")
})
})
}