mirror of
https://github.com/alibaba/higress.git
synced 2026-05-21 11:17:28 +08:00
Signed-off-by: Betula-L <6059935+Betula-L@users.noreply.github.com> Co-authored-by: Betula-L <6059935+Betula-L@users.noreply.github.com>
570 lines
21 KiB
Go
570 lines
21 KiB
Go
package provider
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestBedrockResponsePreservesClaudeNativeThinkingSignature(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
ctx.SetContext("needClaudeResponseConversion", true)
|
|
|
|
response := provider.buildChatCompletionResponse(ctx, &bedrockConverseResponse{
|
|
Output: converseOutputMemberMessage{Message: message{
|
|
Role: roleAssistant,
|
|
Content: []contentBlock{
|
|
{ReasoningContent: &reasoningContent{ReasoningText: &reasoningText{Text: "reasoning", Signature: "sig"}}},
|
|
{Text: "answer"},
|
|
},
|
|
}},
|
|
StopReason: "end_turn",
|
|
})
|
|
body, err := json.Marshal(response)
|
|
require.NoError(t, err)
|
|
|
|
converted, err := (&ClaudeToOpenAIConverter{}).ConvertOpenAIResponseToClaude(ctx, body)
|
|
require.NoError(t, err)
|
|
|
|
var claudeResponse claudeTextGenResponse
|
|
require.NoError(t, json.Unmarshal(converted, &claudeResponse))
|
|
require.Len(t, claudeResponse.Content, 2)
|
|
assert.Equal(t, "thinking", claudeResponse.Content[0].Type)
|
|
require.NotNil(t, claudeResponse.Content[0].Thinking)
|
|
require.NotNil(t, claudeResponse.Content[0].Signature)
|
|
assert.Equal(t, "reasoning", *claudeResponse.Content[0].Thinking)
|
|
assert.Equal(t, "sig", *claudeResponse.Content[0].Signature)
|
|
assert.Equal(t, "text", claudeResponse.Content[1].Type)
|
|
require.NotNil(t, claudeResponse.Content[1].Text)
|
|
assert.Equal(t, "answer", *claudeResponse.Content[1].Text)
|
|
}
|
|
|
|
func TestBedrockStreamPreservesClaudeNativeThinkingSignature(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
ctx.SetContext("needClaudeResponseConversion", true)
|
|
converter := &ClaudeToOpenAIConverter{}
|
|
|
|
textChunk, err := provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 0,
|
|
Delta: &converseStreamEventContentBlockDelta{
|
|
ReasoningContent: &reasoningContentDelta{Text: "reasoning"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = converter.ConvertOpenAIStreamResponseToClaude(ctx, textChunk)
|
|
require.NoError(t, err)
|
|
|
|
signatureChunk, err := provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 0,
|
|
Delta: &converseStreamEventContentBlockDelta{
|
|
ReasoningContent: &reasoningContentDelta{Signature: "sig"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
converted, err := converter.ConvertOpenAIStreamResponseToClaude(ctx, signatureChunk)
|
|
require.NoError(t, err)
|
|
|
|
events := parseClaudeSSEEvents(t, converted)
|
|
require.Len(t, events, 1)
|
|
assert.Equal(t, "content_block_delta", events[0].Name)
|
|
require.NotNil(t, events[0].Payload.Delta)
|
|
assert.Equal(t, "signature_delta", events[0].Payload.Delta.Type)
|
|
assert.Equal(t, "sig", events[0].Payload.Delta.Signature)
|
|
}
|
|
|
|
func TestBedrockStreamPreservesClaudeNativeIndexesAndStops(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
ctx.SetContext("needClaudeResponseConversion", true)
|
|
converter := &ClaudeToOpenAIConverter{}
|
|
|
|
chunk, err := provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 2,
|
|
Delta: &converseStreamEventContentBlockDelta{
|
|
ReasoningContent: &reasoningContentDelta{Text: "reasoning"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
converted, err := converter.ConvertOpenAIStreamResponseToClaude(ctx, chunk)
|
|
require.NoError(t, err)
|
|
events := parseClaudeSSEEvents(t, converted)
|
|
require.Len(t, events, 2)
|
|
assert.Equal(t, "content_block_start", events[0].Name)
|
|
require.NotNil(t, events[0].Payload.Index)
|
|
assert.Equal(t, 2, *events[0].Payload.Index)
|
|
assert.Equal(t, "content_block_delta", events[1].Name)
|
|
require.NotNil(t, events[1].Payload.Index)
|
|
assert.Equal(t, 2, *events[1].Payload.Index)
|
|
|
|
chunk, err = provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 2,
|
|
ContentBlockStop: &contentBlockStop{ContentBlockIndex: 2},
|
|
})
|
|
require.NoError(t, err)
|
|
converted, err = converter.ConvertOpenAIStreamResponseToClaude(ctx, chunk)
|
|
require.NoError(t, err)
|
|
events = parseClaudeSSEEvents(t, converted)
|
|
require.Len(t, events, 1)
|
|
assert.Equal(t, "content_block_stop", events[0].Name)
|
|
require.NotNil(t, events[0].Payload.Index)
|
|
assert.Equal(t, 2, *events[0].Payload.Index)
|
|
}
|
|
|
|
func TestBedrockResponsePreservesClaudeNativeRedactedThinking(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
ctx.SetContext("needClaudeResponseConversion", true)
|
|
|
|
response := provider.buildChatCompletionResponse(ctx, &bedrockConverseResponse{
|
|
Output: converseOutputMemberMessage{Message: message{
|
|
Role: roleAssistant,
|
|
Content: []contentBlock{
|
|
{ReasoningContent: &reasoningContent{RedactedContent: "opaque-base64"}},
|
|
{Text: "answer"},
|
|
},
|
|
}},
|
|
StopReason: "end_turn",
|
|
})
|
|
body, err := json.Marshal(response)
|
|
require.NoError(t, err)
|
|
|
|
converted, err := (&ClaudeToOpenAIConverter{}).ConvertOpenAIResponseToClaude(ctx, body)
|
|
require.NoError(t, err)
|
|
|
|
var claudeResponse claudeTextGenResponse
|
|
require.NoError(t, json.Unmarshal(converted, &claudeResponse))
|
|
require.Len(t, claudeResponse.Content, 2)
|
|
assert.Equal(t, "redacted_thinking", claudeResponse.Content[0].Type)
|
|
assert.Equal(t, "opaque-base64", claudeResponse.Content[0].Data)
|
|
}
|
|
|
|
func TestBedrockResponsePreservesClaudeNativeToolUseWithThinking(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
ctx.SetContext("needClaudeResponseConversion", true)
|
|
|
|
response := provider.buildChatCompletionResponse(ctx, &bedrockConverseResponse{
|
|
Output: converseOutputMemberMessage{Message: message{
|
|
Role: roleAssistant,
|
|
Content: []contentBlock{
|
|
{ReasoningContent: &reasoningContent{ReasoningText: &reasoningText{Text: "reasoning", Signature: "sig"}}},
|
|
{ToolUse: &bedrockToolUse{ToolUseId: "toolu_1", Name: "lookup", Input: map[string]interface{}{"query": "q"}}},
|
|
},
|
|
}},
|
|
StopReason: "tool_use",
|
|
})
|
|
body, err := json.Marshal(response)
|
|
require.NoError(t, err)
|
|
|
|
converted, err := (&ClaudeToOpenAIConverter{}).ConvertOpenAIResponseToClaude(ctx, body)
|
|
require.NoError(t, err)
|
|
|
|
var claudeResponse claudeTextGenResponse
|
|
require.NoError(t, json.Unmarshal(converted, &claudeResponse))
|
|
require.Len(t, claudeResponse.Content, 2)
|
|
assert.Equal(t, "thinking", claudeResponse.Content[0].Type)
|
|
assert.Equal(t, "tool_use", claudeResponse.Content[1].Type)
|
|
assert.Equal(t, "toolu_1", claudeResponse.Content[1].Id)
|
|
assert.Equal(t, "lookup", claudeResponse.Content[1].Name)
|
|
}
|
|
|
|
func TestBedrockStreamRedactedThinkingStopsOnce(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
ctx.SetContext("needClaudeResponseConversion", true)
|
|
converter := &ClaudeToOpenAIConverter{}
|
|
|
|
chunk, err := provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 1,
|
|
Delta: &converseStreamEventContentBlockDelta{
|
|
ReasoningContent: &reasoningContentDelta{RedactedContent: "opaque-base64"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
converted, err := converter.ConvertOpenAIStreamResponseToClaude(ctx, chunk)
|
|
require.NoError(t, err)
|
|
events := parseClaudeSSEEvents(t, converted)
|
|
require.Len(t, events, 1)
|
|
assert.Equal(t, "content_block_start", events[0].Name)
|
|
assert.Equal(t, "redacted_thinking", events[0].Payload.ContentBlock.Type)
|
|
|
|
chunk, err = provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 1,
|
|
ContentBlockStop: &contentBlockStop{ContentBlockIndex: 1},
|
|
})
|
|
require.NoError(t, err)
|
|
converted, err = converter.ConvertOpenAIStreamResponseToClaude(ctx, chunk)
|
|
require.NoError(t, err)
|
|
events = parseClaudeSSEEvents(t, converted)
|
|
require.Len(t, events, 1)
|
|
assert.Equal(t, "content_block_stop", events[0].Name)
|
|
}
|
|
|
|
func TestBedrockRequestPreservesClaudeNativeThinkingAndToolResult(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"system":"system prompt",
|
|
"messages":[{
|
|
"role":"assistant",
|
|
"content":[
|
|
{"type":"thinking","thinking":"reasoning","signature":"sig"},
|
|
{"type":"tool_use","id":"toolu_1","name":"lookup","input":{"query":"q"}}
|
|
]
|
|
},{
|
|
"role":"user",
|
|
"content":[{
|
|
"type":"tool_result",
|
|
"tool_use_id":"toolu_1",
|
|
"is_error":true,
|
|
"content":[{"type":"text","text":"failed"}]
|
|
}]
|
|
}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
var request bedrockTextGenRequest
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
require.Len(t, request.System, 1)
|
|
assert.Equal(t, "system prompt", request.System[0].Text)
|
|
require.Len(t, request.Messages, 2)
|
|
require.Len(t, request.Messages[0].Content, 2)
|
|
require.NotNil(t, request.Messages[0].Content[0].ReasoningContent)
|
|
require.NotNil(t, request.Messages[0].Content[0].ReasoningContent.ReasoningText)
|
|
assert.Equal(t, "reasoning", request.Messages[0].Content[0].ReasoningContent.ReasoningText.Text)
|
|
assert.Equal(t, "sig", request.Messages[0].Content[0].ReasoningContent.ReasoningText.Signature)
|
|
require.NotNil(t, request.Messages[0].Content[1].ToolUse)
|
|
assert.Equal(t, "toolu_1", request.Messages[0].Content[1].ToolUse.ToolUseId)
|
|
require.NotNil(t, request.Messages[1].Content[0].ToolResult)
|
|
assert.Equal(t, "error", request.Messages[1].Content[0].ToolResult.Status)
|
|
require.NotNil(t, request.Messages[1].Content[0].ToolResult.Content[0].Text)
|
|
assert.Equal(t, "failed", *request.Messages[1].Content[0].ToolResult.Content[0].Text)
|
|
}
|
|
|
|
func TestBedrockRequestPreservesClaudeNoArgToolUseInput(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"messages":[{
|
|
"role":"assistant",
|
|
"content":[
|
|
{"type":"thinking","thinking":"reasoning","signature":"sig"},
|
|
{"type":"tool_use","id":"toolu_1","name":"list_items","input":{}}
|
|
]
|
|
}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, string(body), `"input":{}`)
|
|
var request bedrockTextGenRequest
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
require.Len(t, request.Messages, 1)
|
|
require.Len(t, request.Messages[0].Content, 2)
|
|
require.NotNil(t, request.Messages[0].Content[1].ToolUse)
|
|
assert.Empty(t, request.Messages[0].Content[1].ToolUse.Input)
|
|
}
|
|
|
|
func TestBedrockRequestToolResultWithTrailingTextDoesNotDuplicateToolResult(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"messages":[{
|
|
"role":"user",
|
|
"content":[
|
|
{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"},
|
|
{"type":"text","text":"continue"}
|
|
]
|
|
}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
require.Len(t, openaiRequest.Messages, 2)
|
|
assert.Empty(t, openaiRequest.Messages[1].ClaudeContentBlocks)
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
var request bedrockTextGenRequest
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
require.Len(t, request.Messages, 2)
|
|
require.Len(t, request.Messages[0].Content, 1)
|
|
require.NotNil(t, request.Messages[0].Content[0].ToolResult)
|
|
require.Len(t, request.Messages[1].Content, 1)
|
|
assert.Nil(t, request.Messages[1].Content[0].ToolResult)
|
|
assert.Equal(t, "continue", request.Messages[1].Content[0].Text)
|
|
}
|
|
|
|
func TestBedrockRequestRedactedThinkingUsesSingleUnionArm(t *testing.T) {
|
|
contents := claudeContentBlocksToBedrockContents([]claudeChatMessageContent{
|
|
{Type: "redacted_thinking", Data: "opaque-base64"},
|
|
})
|
|
|
|
body, err := json.Marshal(contents[0])
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{"reasoningContent":{"redactedContent":"opaque-base64"}}`, string(body))
|
|
assert.NotContains(t, string(body), "reasoningText")
|
|
}
|
|
|
|
func TestBedrockRequestToolResultDefaultsEmptyContent(t *testing.T) {
|
|
result := claudeToolResultBlockToBedrock(claudeChatMessageContent{
|
|
Type: "tool_result",
|
|
ToolUseId: "toolu_1",
|
|
})
|
|
|
|
require.Len(t, result.Content, 1)
|
|
require.NotNil(t, result.Content[0].Text)
|
|
assert.Equal(t, "", *result.Content[0].Text)
|
|
body, err := json.Marshal(result)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(body), `"text":""`)
|
|
assert.NotContains(t, string(body), `[{}]`)
|
|
}
|
|
|
|
func TestBedrockRequestPreservesEmptyThinkingTextWithSignature(t *testing.T) {
|
|
contents := claudeContentBlocksToBedrockContents([]claudeChatMessageContent{
|
|
{Type: "thinking", Thinking: "", Signature: "sig"},
|
|
})
|
|
|
|
body, err := json.Marshal(contents[0])
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{"reasoningContent":{"reasoningText":{"text":"","signature":"sig"}}}`, string(body))
|
|
}
|
|
|
|
func TestBedrockRequestPreservesClaudeNativeThinkingBudget(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"max_tokens":32000,
|
|
"thinking":{"type":"enabled","budget_tokens":8192},
|
|
"messages":[{"role":"user","content":"hello"}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
var request bedrockTextGenRequest
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
assert.Equal(t, float64(8192), request.AdditionalModelRequestFields["thinking"].(map[string]interface{})["budget_tokens"])
|
|
}
|
|
|
|
func TestBedrockRequestMapsAdaptiveOutputEffortIntoThinking(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"thinking":{"type":"adaptive"},
|
|
"output_config":{"effort":"high"},
|
|
"messages":[{"role":"user","content":"hello"}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
var request bedrockTextGenRequest
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
thinking := request.AdditionalModelRequestFields["thinking"].(map[string]interface{})
|
|
assert.Equal(t, "adaptive", thinking["type"])
|
|
assert.Equal(t, "high", thinking["effort"])
|
|
assert.NotContains(t, request.AdditionalModelRequestFields, "output_config")
|
|
assert.NotContains(t, request.AdditionalModelRequestFields, "anthropic_beta")
|
|
}
|
|
|
|
func TestBedrockRequestDropsOutputEffortWithoutAdaptiveThinking(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"output_config":{"effort":"high"},
|
|
"messages":[{"role":"user","content":"hello"}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
var request bedrockTextGenRequest
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
assert.NotContains(t, request.AdditionalModelRequestFields, "output_config")
|
|
assert.NotContains(t, request.AdditionalModelRequestFields, "thinking")
|
|
}
|
|
|
|
func TestBedrockRequestDropsUnsupportedAdaptiveOutputEffort(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"thinking":{"type":"adaptive"},
|
|
"output_config":{"effort":"xhigh"},
|
|
"messages":[{"role":"user","content":"hello"}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
var request bedrockTextGenRequest
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
thinking := request.AdditionalModelRequestFields["thinking"].(map[string]interface{})
|
|
assert.Equal(t, "adaptive", thinking["type"])
|
|
assert.NotContains(t, thinking, "effort")
|
|
assert.NotContains(t, request.AdditionalModelRequestFields, "output_config")
|
|
}
|
|
|
|
func TestBedrockRequestMapsClaudeOutputFormatToTextFormat(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"output_config":{
|
|
"format":{
|
|
"type":"json_schema",
|
|
"schema":{"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"]}
|
|
}
|
|
},
|
|
"messages":[{"role":"user","content":"hello"}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
var request map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
outputConfig := request["outputConfig"].(map[string]interface{})
|
|
textFormat := outputConfig["textFormat"].(map[string]interface{})
|
|
assert.Equal(t, "json_schema", textFormat["type"])
|
|
}
|
|
|
|
func TestBedrockRequestDowngradesForcedToolChoiceWhenThinkingEnabled(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
openaiBody, err := (&ClaudeToOpenAIConverter{}).ConvertClaudeRequestToOpenAI([]byte(`{
|
|
"model":"claude",
|
|
"thinking":{"type":"enabled","budget_tokens":8192},
|
|
"tools":[{"name":"lookup","input_schema":{"type":"object"}}],
|
|
"tool_choice":{"type":"any"},
|
|
"messages":[{"role":"user","content":"hello"}]
|
|
}`))
|
|
require.NoError(t, err)
|
|
var openaiRequest chatCompletionRequest
|
|
require.NoError(t, json.Unmarshal(openaiBody, &openaiRequest))
|
|
|
|
body, err := provider.buildBedrockTextGenerationRequest(&openaiRequest, nil)
|
|
require.NoError(t, err)
|
|
|
|
var request bedrockTextGenRequest
|
|
require.NoError(t, json.Unmarshal(body, &request))
|
|
require.NotNil(t, request.ToolConfig)
|
|
assert.NotNil(t, request.ToolConfig.ToolChoice.Auto)
|
|
assert.Nil(t, request.ToolConfig.ToolChoice.Any)
|
|
assert.Nil(t, request.ToolConfig.ToolChoice.Tool)
|
|
}
|
|
|
|
func TestBedrockStreamSkipsOrphanClaudeContentBlockStop(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
ctx.SetContext("needClaudeResponseConversion", true)
|
|
converter := &ClaudeToOpenAIConverter{}
|
|
|
|
chunk, err := provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 3,
|
|
ContentBlockStop: &contentBlockStop{ContentBlockIndex: 3},
|
|
})
|
|
require.NoError(t, err)
|
|
converted, err := converter.ConvertOpenAIStreamResponseToClaude(ctx, chunk)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, parseClaudeSSEEvents(t, converted))
|
|
}
|
|
|
|
func TestBedrockStreamBatchedEventsKeepClaudeMessageStartFirst(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
ctx.SetContext("needClaudeResponseConversion", true)
|
|
converter := &ClaudeToOpenAIConverter{}
|
|
role := roleAssistant
|
|
|
|
roleChunk, err := provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{Role: &role})
|
|
require.NoError(t, err)
|
|
reasoningChunk, err := provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 0,
|
|
Delta: &converseStreamEventContentBlockDelta{
|
|
ReasoningContent: &reasoningContentDelta{Text: "reasoning"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
converted, err := converter.ConvertOpenAIStreamResponseToClaude(ctx, append(roleChunk, reasoningChunk...))
|
|
require.NoError(t, err)
|
|
events := parseClaudeSSEEvents(t, converted)
|
|
require.Len(t, events, 3)
|
|
assert.Equal(t, "message_start", events[0].Name)
|
|
assert.Equal(t, "content_block_start", events[1].Name)
|
|
assert.Equal(t, "content_block_delta", events[2].Name)
|
|
}
|
|
|
|
func TestBedrockResponseUsesReasoningContentInsteadOfThinkTags(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
|
|
response := provider.buildChatCompletionResponse(newMockMultipartHttpContext(), &bedrockConverseResponse{
|
|
Output: converseOutputMemberMessage{Message: message{
|
|
Role: roleAssistant,
|
|
Content: []contentBlock{
|
|
{ReasoningContent: &reasoningContent{ReasoningText: &reasoningText{Text: "reasoning"}}},
|
|
{Text: "answer"},
|
|
},
|
|
}},
|
|
StopReason: "end_turn",
|
|
})
|
|
|
|
require.Len(t, response.Choices, 1)
|
|
require.NotNil(t, response.Choices[0].Message)
|
|
assert.Equal(t, "reasoning", response.Choices[0].Message.ReasoningContent)
|
|
assert.Equal(t, "answer", response.Choices[0].Message.Content)
|
|
assert.NotContains(t, response.Choices[0].Message.StringContent(), "<think>")
|
|
}
|
|
|
|
func TestBedrockStreamUsesReasoningContentInsteadOfThinkTags(t *testing.T) {
|
|
provider := &bedrockProvider{}
|
|
ctx := newMockMultipartHttpContext()
|
|
|
|
chunk, err := provider.convertEventFromBedrockToOpenAI(ctx, ConverseStreamEvent{
|
|
ContentBlockIndex: 0,
|
|
Delta: &converseStreamEventContentBlockDelta{
|
|
ReasoningContent: &reasoningContentDelta{Text: "reasoning"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
body := strings.TrimPrefix(strings.TrimSpace(string(chunk)), ssePrefix)
|
|
var response chatCompletionResponse
|
|
require.NoError(t, json.Unmarshal([]byte(body), &response))
|
|
require.Len(t, response.Choices, 1)
|
|
require.NotNil(t, response.Choices[0].Delta)
|
|
assert.Equal(t, "reasoning", response.Choices[0].Delta.ReasoningContent)
|
|
assert.Nil(t, response.Choices[0].Delta.Content)
|
|
}
|