mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-03-30 02:27:11 +00:00
262 lines
9.5 KiB
Go
262 lines
9.5 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"hash/crc32"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestExtractBedrockChunkData(t *testing.T) {
|
|
t.Run("valid base64 payload", func(t *testing.T) {
|
|
original := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}`
|
|
b64 := base64.StdEncoding.EncodeToString([]byte(original))
|
|
payload := []byte(`{"bytes":"` + b64 + `"}`)
|
|
|
|
result := extractBedrockChunkData(payload)
|
|
require.NotNil(t, result)
|
|
assert.JSONEq(t, original, string(result))
|
|
})
|
|
|
|
t.Run("empty bytes field", func(t *testing.T) {
|
|
result := extractBedrockChunkData([]byte(`{"bytes":""}`))
|
|
assert.Nil(t, result)
|
|
})
|
|
|
|
t.Run("no bytes field", func(t *testing.T) {
|
|
result := extractBedrockChunkData([]byte(`{"other":"value"}`))
|
|
assert.Nil(t, result)
|
|
})
|
|
|
|
t.Run("invalid base64", func(t *testing.T) {
|
|
result := extractBedrockChunkData([]byte(`{"bytes":"not-valid-base64!!!"}`))
|
|
assert.Nil(t, result)
|
|
})
|
|
}
|
|
|
|
func TestTransformBedrockInvocationMetrics(t *testing.T) {
|
|
t.Run("converts metrics to usage", func(t *testing.T) {
|
|
input := `{"type":"message_delta","delta":{"stop_reason":"end_turn"},"amazon-bedrock-invocationMetrics":{"inputTokenCount":150,"outputTokenCount":42}}`
|
|
result := transformBedrockInvocationMetrics([]byte(input))
|
|
|
|
// amazon-bedrock-invocationMetrics should be removed
|
|
assert.False(t, gjson.GetBytes(result, "amazon-bedrock-invocationMetrics").Exists())
|
|
// usage should be set
|
|
assert.Equal(t, int64(150), gjson.GetBytes(result, "usage.input_tokens").Int())
|
|
assert.Equal(t, int64(42), gjson.GetBytes(result, "usage.output_tokens").Int())
|
|
// original fields preserved
|
|
assert.Equal(t, "message_delta", gjson.GetBytes(result, "type").String())
|
|
assert.Equal(t, "end_turn", gjson.GetBytes(result, "delta.stop_reason").String())
|
|
})
|
|
|
|
t.Run("no metrics present", func(t *testing.T) {
|
|
input := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"}}`
|
|
result := transformBedrockInvocationMetrics([]byte(input))
|
|
assert.JSONEq(t, input, string(result))
|
|
})
|
|
|
|
t.Run("does not overwrite existing usage", func(t *testing.T) {
|
|
input := `{"type":"message_delta","usage":{"output_tokens":100},"amazon-bedrock-invocationMetrics":{"inputTokenCount":150,"outputTokenCount":42}}`
|
|
result := transformBedrockInvocationMetrics([]byte(input))
|
|
|
|
// metrics removed but existing usage preserved
|
|
assert.False(t, gjson.GetBytes(result, "amazon-bedrock-invocationMetrics").Exists())
|
|
assert.Equal(t, int64(100), gjson.GetBytes(result, "usage.output_tokens").Int())
|
|
})
|
|
}
|
|
|
|
func TestExtractEventStreamHeaderValue(t *testing.T) {
|
|
// Build a header with :event-type = "chunk" (string type = 7)
|
|
buildStringHeader := func(name, value string) []byte {
|
|
var buf bytes.Buffer
|
|
// name length (1 byte)
|
|
buf.WriteByte(byte(len(name)))
|
|
// name
|
|
buf.WriteString(name)
|
|
// value type (7 = string)
|
|
buf.WriteByte(7)
|
|
// value length (2 bytes, big-endian)
|
|
_ = binary.Write(&buf, binary.BigEndian, uint16(len(value)))
|
|
// value
|
|
buf.WriteString(value)
|
|
return buf.Bytes()
|
|
}
|
|
|
|
t.Run("find string header", func(t *testing.T) {
|
|
headers := buildStringHeader(":event-type", "chunk")
|
|
assert.Equal(t, "chunk", extractEventStreamHeaderValue(headers, ":event-type"))
|
|
})
|
|
|
|
t.Run("header not found", func(t *testing.T) {
|
|
headers := buildStringHeader(":event-type", "chunk")
|
|
assert.Equal(t, "", extractEventStreamHeaderValue(headers, ":message-type"))
|
|
})
|
|
|
|
t.Run("multiple headers", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
buf.Write(buildStringHeader(":content-type", "application/json"))
|
|
buf.Write(buildStringHeader(":event-type", "chunk"))
|
|
buf.Write(buildStringHeader(":message-type", "event"))
|
|
|
|
headers := buf.Bytes()
|
|
assert.Equal(t, "chunk", extractEventStreamHeaderValue(headers, ":event-type"))
|
|
assert.Equal(t, "application/json", extractEventStreamHeaderValue(headers, ":content-type"))
|
|
assert.Equal(t, "event", extractEventStreamHeaderValue(headers, ":message-type"))
|
|
})
|
|
|
|
t.Run("empty headers", func(t *testing.T) {
|
|
assert.Equal(t, "", extractEventStreamHeaderValue([]byte{}, ":event-type"))
|
|
})
|
|
}
|
|
|
|
func TestBedrockEventStreamDecoder(t *testing.T) {
|
|
crc32IeeeTab := crc32.MakeTable(crc32.IEEE)
|
|
|
|
// Build a valid EventStream frame with correct CRC32/IEEE checksums.
|
|
buildFrame := func(eventType string, payload []byte) []byte {
|
|
// Build headers
|
|
var headersBuf bytes.Buffer
|
|
// :event-type header
|
|
headersBuf.WriteByte(byte(len(":event-type")))
|
|
headersBuf.WriteString(":event-type")
|
|
headersBuf.WriteByte(7) // string type
|
|
_ = binary.Write(&headersBuf, binary.BigEndian, uint16(len(eventType)))
|
|
headersBuf.WriteString(eventType)
|
|
// :message-type header
|
|
headersBuf.WriteByte(byte(len(":message-type")))
|
|
headersBuf.WriteString(":message-type")
|
|
headersBuf.WriteByte(7)
|
|
_ = binary.Write(&headersBuf, binary.BigEndian, uint16(len("event")))
|
|
headersBuf.WriteString("event")
|
|
|
|
headers := headersBuf.Bytes()
|
|
headersLen := uint32(len(headers))
|
|
// total = 12 (prelude) + headers + payload + 4 (message_crc)
|
|
totalLen := uint32(12 + len(headers) + len(payload) + 4)
|
|
|
|
// Prelude: total_length(4) + headers_length(4)
|
|
var preludeBuf bytes.Buffer
|
|
_ = binary.Write(&preludeBuf, binary.BigEndian, totalLen)
|
|
_ = binary.Write(&preludeBuf, binary.BigEndian, headersLen)
|
|
preludeBytes := preludeBuf.Bytes()
|
|
preludeCRC := crc32.Checksum(preludeBytes, crc32IeeeTab)
|
|
|
|
// Build frame: prelude + prelude_crc + headers + payload
|
|
var frame bytes.Buffer
|
|
frame.Write(preludeBytes)
|
|
_ = binary.Write(&frame, binary.BigEndian, preludeCRC)
|
|
frame.Write(headers)
|
|
frame.Write(payload)
|
|
|
|
// Message CRC covers everything before itself
|
|
messageCRC := crc32.Checksum(frame.Bytes(), crc32IeeeTab)
|
|
_ = binary.Write(&frame, binary.BigEndian, messageCRC)
|
|
return frame.Bytes()
|
|
}
|
|
|
|
t.Run("decode chunk event", func(t *testing.T) {
|
|
payload := []byte(`{"bytes":"dGVzdA=="}`) // base64("test")
|
|
frame := buildFrame("chunk", payload)
|
|
|
|
decoder := newBedrockEventStreamDecoder(bytes.NewReader(frame))
|
|
result, err := decoder.Decode()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, payload, result)
|
|
})
|
|
|
|
t.Run("skip non-chunk events", func(t *testing.T) {
|
|
// Write initial-response followed by chunk
|
|
var buf bytes.Buffer
|
|
buf.Write(buildFrame("initial-response", []byte(`{}`)))
|
|
chunkPayload := []byte(`{"bytes":"aGVsbG8="}`)
|
|
buf.Write(buildFrame("chunk", chunkPayload))
|
|
|
|
decoder := newBedrockEventStreamDecoder(&buf)
|
|
result, err := decoder.Decode()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, chunkPayload, result)
|
|
})
|
|
|
|
t.Run("EOF on empty input", func(t *testing.T) {
|
|
decoder := newBedrockEventStreamDecoder(bytes.NewReader(nil))
|
|
_, err := decoder.Decode()
|
|
assert.Equal(t, io.EOF, err)
|
|
})
|
|
|
|
t.Run("corrupted prelude CRC", func(t *testing.T) {
|
|
frame := buildFrame("chunk", []byte(`{"bytes":"dGVzdA=="}`))
|
|
// Corrupt the prelude CRC (bytes 8-11)
|
|
frame[8] ^= 0xFF
|
|
decoder := newBedrockEventStreamDecoder(bytes.NewReader(frame))
|
|
_, err := decoder.Decode()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "prelude CRC mismatch")
|
|
})
|
|
|
|
t.Run("corrupted message CRC", func(t *testing.T) {
|
|
frame := buildFrame("chunk", []byte(`{"bytes":"dGVzdA=="}`))
|
|
// Corrupt the message CRC (last 4 bytes)
|
|
frame[len(frame)-1] ^= 0xFF
|
|
decoder := newBedrockEventStreamDecoder(bytes.NewReader(frame))
|
|
_, err := decoder.Decode()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "message CRC mismatch")
|
|
})
|
|
|
|
t.Run("castagnoli encoded frame is rejected", func(t *testing.T) {
|
|
castagnoliTab := crc32.MakeTable(crc32.Castagnoli)
|
|
payload := []byte(`{"bytes":"dGVzdA=="}`)
|
|
|
|
var headersBuf bytes.Buffer
|
|
headersBuf.WriteByte(byte(len(":event-type")))
|
|
headersBuf.WriteString(":event-type")
|
|
headersBuf.WriteByte(7)
|
|
_ = binary.Write(&headersBuf, binary.BigEndian, uint16(len("chunk")))
|
|
headersBuf.WriteString("chunk")
|
|
|
|
headers := headersBuf.Bytes()
|
|
headersLen := uint32(len(headers))
|
|
totalLen := uint32(12 + len(headers) + len(payload) + 4)
|
|
|
|
var preludeBuf bytes.Buffer
|
|
_ = binary.Write(&preludeBuf, binary.BigEndian, totalLen)
|
|
_ = binary.Write(&preludeBuf, binary.BigEndian, headersLen)
|
|
preludeBytes := preludeBuf.Bytes()
|
|
|
|
var frame bytes.Buffer
|
|
frame.Write(preludeBytes)
|
|
_ = binary.Write(&frame, binary.BigEndian, crc32.Checksum(preludeBytes, castagnoliTab))
|
|
frame.Write(headers)
|
|
frame.Write(payload)
|
|
_ = binary.Write(&frame, binary.BigEndian, crc32.Checksum(frame.Bytes(), castagnoliTab))
|
|
|
|
decoder := newBedrockEventStreamDecoder(bytes.NewReader(frame.Bytes()))
|
|
_, err := decoder.Decode()
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "prelude CRC mismatch")
|
|
})
|
|
}
|
|
|
|
func TestBuildBedrockURL(t *testing.T) {
|
|
t.Run("stream URL with colon in model ID", func(t *testing.T) {
|
|
url := BuildBedrockURL("us-east-1", "us.anthropic.claude-opus-4-5-20251101-v1:0", true)
|
|
assert.Equal(t, "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-opus-4-5-20251101-v1%3A0/invoke-with-response-stream", url)
|
|
})
|
|
|
|
t.Run("non-stream URL with colon in model ID", func(t *testing.T) {
|
|
url := BuildBedrockURL("eu-west-1", "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", false)
|
|
assert.Equal(t, "https://bedrock-runtime.eu-west-1.amazonaws.com/model/eu.anthropic.claude-sonnet-4-5-20250929-v1%3A0/invoke", url)
|
|
})
|
|
|
|
t.Run("model ID without colon", func(t *testing.T) {
|
|
url := BuildBedrockURL("us-east-1", "us.anthropic.claude-sonnet-4-6", true)
|
|
assert.Equal(t, "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-sonnet-4-6/invoke-with-response-stream", url)
|
|
})
|
|
}
|