Skip to content

Commit 178f1d1

Browse files
feat: add bedrock provider
1 parent ee2c4b4 commit 178f1d1

21 files changed

Lines changed: 1643 additions & 13 deletions

api.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
ProviderAnthropic = config.ProviderAnthropic
2020
ProviderOpenAI = config.ProviderOpenAI
2121
ProviderCopilot = config.ProviderCopilot
22+
ProviderBedrock = config.ProviderBedrock
2223
)
2324

2425
type (
@@ -37,6 +38,7 @@ type (
3738

3839
AnthropicConfig = config.Anthropic
3940
AWSBedrockConfig = config.AWSBedrock
41+
BedrockConfig = config.Bedrock
4042
OpenAIConfig = config.OpenAI
4143
CopilotConfig = config.Copilot
4244
)
@@ -57,6 +59,10 @@ func NewCopilotProvider(cfg config.Copilot) provider.Provider {
5759
return provider.NewCopilot(cfg)
5860
}
5961

62+
func NewBedrockProvider(cfg config.Bedrock) provider.Provider {
63+
return provider.NewBedrock(cfg)
64+
}
65+
6066
func NewMetrics(reg prometheus.Registerer) *metrics.Metrics {
6167
return metrics.NewMetrics(reg)
6268
}

config/config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const (
66
ProviderAnthropic = "anthropic"
77
ProviderOpenAI = "openai"
88
ProviderCopilot = "copilot"
9+
ProviderBedrock = "bedrock"
910
)
1011

1112
type Anthropic struct {
@@ -34,6 +35,17 @@ type AWSBedrock struct {
3435
BaseURL string
3536
}
3637

38+
// Bedrock is a standalone Bedrock provider configuration. It acts as a
39+
// SigV4-signing reverse proxy, forwarding native Bedrock API requests
40+
// to AWS and adding centralized AWS credentials.
41+
type Bedrock struct {
42+
// Name is the provider instance name. If empty, defaults to "bedrock".
43+
Name string
44+
APIDumpDir string
45+
CircuitBreaker *CircuitBreaker
46+
AWSBedrock // Region, AccessKey, AccessKeySecret, SessionToken, Model, SmallFastModel, BaseURL
47+
}
48+
3749
type OpenAI struct {
3850
// Name is the provider instance name. If empty, defaults to "openai".
3951
Name string
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//go:build ignore
2+
3+
// Usage:
4+
//
5+
// go run parse_eventstream.go [dir]
6+
//
7+
// Finds all .resp.bin files in dir (default: current directory) and
8+
// generates a corresponding .resp.decoded file for each one. Existing
9+
// .resp.decoded files are overwritten.
10+
//
11+
// To decode a single file:
12+
//
13+
// go run parse_eventstream.go path/to/file.resp.bin
14+
package main
15+
16+
import (
17+
"bytes"
18+
"encoding/base64"
19+
"encoding/json"
20+
"fmt"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
25+
"github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream"
26+
"github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi"
27+
)
28+
29+
func main() {
30+
arg := "."
31+
if len(os.Args) > 1 {
32+
arg = os.Args[1]
33+
}
34+
35+
info, err := os.Stat(arg)
36+
if err != nil {
37+
fmt.Fprintf(os.Stderr, "stat %s: %v\n", arg, err)
38+
os.Exit(1)
39+
}
40+
41+
if !info.IsDir() {
42+
if err := decodeFile(arg, os.Stdout); err != nil {
43+
fmt.Fprintf(os.Stderr, "%v\n", err)
44+
os.Exit(1)
45+
}
46+
return
47+
}
48+
49+
files, err := filepath.Glob(filepath.Join(arg, "*.resp.bin"))
50+
if err != nil {
51+
fmt.Fprintf(os.Stderr, "glob: %v\n", err)
52+
os.Exit(1)
53+
}
54+
if len(files) == 0 {
55+
fmt.Fprintf(os.Stderr, "no .resp.bin files found in %s\n", arg)
56+
os.Exit(1)
57+
}
58+
59+
for _, binFile := range files {
60+
outFile := strings.TrimSuffix(binFile, ".resp.bin") + ".resp.decoded"
61+
f, err := os.Create(outFile)
62+
if err != nil {
63+
fmt.Fprintf(os.Stderr, "create %s: %v\n", outFile, err)
64+
continue
65+
}
66+
if err := decodeFile(binFile, f); err != nil {
67+
fmt.Fprintf(os.Stderr, "decode %s: %v\n", binFile, err)
68+
}
69+
f.Close()
70+
fmt.Printf("%s -> %s\n", filepath.Base(binFile), filepath.Base(outFile))
71+
}
72+
}
73+
74+
func decodeFile(path string, w *os.File) error {
75+
data, err := os.ReadFile(path)
76+
if err != nil {
77+
return fmt.Errorf("read %s: %w", path, err)
78+
}
79+
80+
decoder := eventstream.NewDecoder()
81+
reader := bytes.NewReader(data)
82+
frameNum := 0
83+
84+
for {
85+
msg, err := decoder.Decode(reader, nil)
86+
if err != nil {
87+
break
88+
}
89+
frameNum++
90+
91+
messageType := msg.Headers.Get(eventstreamapi.MessageTypeHeader)
92+
eventType := msg.Headers.Get(eventstreamapi.EventTypeHeader)
93+
94+
fmt.Fprintf(w, "=== Frame %d ===\n", frameNum)
95+
fmt.Fprintf(w, " message-type: %s\n", headerStr(messageType))
96+
fmt.Fprintf(w, " event-type: %s\n", headerStr(eventType))
97+
98+
if headerStr(eventType) != "chunk" {
99+
fmt.Fprintf(w, " payload: %s\n\n", string(msg.Payload))
100+
continue
101+
}
102+
103+
var chunk struct {
104+
Bytes string `json:"bytes"`
105+
}
106+
if err := json.Unmarshal(msg.Payload, &chunk); err != nil {
107+
fmt.Fprintf(w, " unmarshal error: %v\n\n", err)
108+
continue
109+
}
110+
111+
decoded, err := base64.StdEncoding.DecodeString(chunk.Bytes)
112+
if err != nil {
113+
fmt.Fprintf(w, " base64 decode error: %v\n\n", err)
114+
continue
115+
}
116+
117+
var pretty json.RawMessage
118+
if err := json.Unmarshal(decoded, &pretty); err != nil {
119+
fmt.Fprintf(w, " json: %s\n\n", string(decoded))
120+
continue
121+
}
122+
123+
indented, _ := json.MarshalIndent(pretty, " ", " ")
124+
fmt.Fprintf(w, " body:\n %s\n\n", string(indented))
125+
}
126+
127+
fmt.Fprintf(w, "Total frames: %d\n", frameNum)
128+
return nil
129+
}
130+
131+
func headerStr(h eventstream.Value) string {
132+
if h == nil {
133+
return "<nil>"
134+
}
135+
return h.String()
136+
}

fixtures/bedrock/simple.req.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"anthropic_version": "bedrock-2023-05-31",
3+
"max_tokens": 50,
4+
"messages": [
5+
{
6+
"role": "user",
7+
"content": "how many angels can dance on the head of a pin"
8+
}
9+
]
10+
}

fixtures/bedrock/simple.resp.bin

10.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)