豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 1540197

Browse files
authored
Merge pull request #64 from Jeffrin-dev/codex/add-bedrock-mock-llm-emulator
Add Bedrock mock LLM emulator service and tests
2 parents c053689 + 74dabc5 commit 1540197

File tree

3 files changed

+215
-0
lines changed

3 files changed

+215
-0
lines changed

cmd/up.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/clouddev/clouddev/internal/persist"
1414
"github.com/clouddev/clouddev/internal/services/apigateway"
1515
"github.com/clouddev/clouddev/internal/services/apigatewayv2"
16+
"github.com/clouddev/clouddev/internal/services/bedrock"
1617
"github.com/clouddev/clouddev/internal/services/cloudformation"
1718
"github.com/clouddev/clouddev/internal/services/cloudwatchevents"
1819
"github.com/clouddev/clouddev/internal/services/cloudwatchlogs"
@@ -229,6 +230,12 @@ var upCmd = &cobra.Command{
229230
}
230231
}()
231232
printSuccess("Rekognition server starting on port %d", 4594)
233+
go func() {
234+
if err := bedrock.Start(4591); err != nil {
235+
fmt.Fprintf(os.Stderr, "Bedrock server error: %v\n", err)
236+
}
237+
}()
238+
printSuccess("Bedrock server starting on port %d", 4591)
232239
manager, err := docker.NewManager(os.Stdout)
233240
if err != nil {
234241
return err
@@ -270,6 +277,7 @@ var upCmd = &cobra.Command{
270277
"route53": 4589,
271278
"iam": 4593,
272279
"sts": 4592,
280+
"bedrock": 4591,
273281
"kms": 4599,
274282
"cloudformation": 4581,
275283
"step_functions": 4585,
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package bedrock
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
)
9+
10+
const jsonContentType = "application/json"
11+
12+
type foundationModel struct {
13+
ModelID string `json:"modelId"`
14+
ModelName string `json:"modelName"`
15+
ProviderName string `json:"providerName"`
16+
InputModalities []string `json:"inputModalities"`
17+
OutputModalities []string `json:"outputModalities"`
18+
}
19+
20+
var mockModels = []foundationModel{
21+
{ModelID: "anthropic.claude-3-sonnet-20240229-v1:0", ModelName: "Claude 3 Sonnet", ProviderName: "Anthropic", InputModalities: []string{"TEXT"}, OutputModalities: []string{"TEXT"}},
22+
{ModelID: "anthropic.claude-instant-v1", ModelName: "Claude Instant", ProviderName: "Anthropic", InputModalities: []string{"TEXT"}, OutputModalities: []string{"TEXT"}},
23+
{ModelID: "amazon.titan-text-express-v1", ModelName: "Titan Text Express", ProviderName: "Amazon", InputModalities: []string{"TEXT"}, OutputModalities: []string{"TEXT"}},
24+
{ModelID: "meta.llama2-13b-chat-v1", ModelName: "Llama 2 13B Chat", ProviderName: "Meta", InputModalities: []string{"TEXT"}, OutputModalities: []string{"TEXT"}},
25+
}
26+
27+
func Start(port int) error {
28+
return http.ListenAndServe(fmt.Sprintf(":%d", port), newServer())
29+
}
30+
31+
func newServer() http.Handler {
32+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+
switch {
34+
case r.Method == http.MethodPost && r.URL.Path == "/foundation-models":
35+
listFoundationModels(w)
36+
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/foundation-models/"):
37+
getFoundationModel(w, r)
38+
case r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/model/"):
39+
invokeModelRoute(w, r)
40+
default:
41+
writeError(w, http.StatusNotFound, "ResourceNotFoundException", "Not found")
42+
}
43+
})
44+
}
45+
46+
func listFoundationModels(w http.ResponseWriter) {
47+
writeJSON(w, http.StatusOK, map[string]any{"modelSummaries": mockModels})
48+
}
49+
50+
func getFoundationModel(w http.ResponseWriter, r *http.Request) {
51+
modelID := strings.TrimPrefix(r.URL.Path, "/foundation-models/")
52+
for _, model := range mockModels {
53+
if model.ModelID == modelID {
54+
writeJSON(w, http.StatusOK, model)
55+
return
56+
}
57+
}
58+
writeError(w, http.StatusNotFound, "ResourceNotFoundException", "Model not found")
59+
}
60+
61+
func invokeModelRoute(w http.ResponseWriter, r *http.Request) {
62+
path := strings.TrimPrefix(r.URL.Path, "/model/")
63+
parts := strings.Split(path, "/")
64+
if len(parts) != 2 {
65+
writeError(w, http.StatusNotFound, "ResourceNotFoundException", "Not found")
66+
return
67+
}
68+
modelID, action := parts[0], parts[1]
69+
if action != "invoke" && action != "invoke-with-response-stream" {
70+
writeError(w, http.StatusNotFound, "ResourceNotFoundException", "Not found")
71+
return
72+
}
73+
invokeModel(w, r, modelID)
74+
}
75+
76+
func invokeModel(w http.ResponseWriter, r *http.Request, modelID string) {
77+
var body map[string]any
78+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
79+
writeError(w, http.StatusBadRequest, "ValidationException", "Invalid JSON body")
80+
return
81+
}
82+
83+
switch {
84+
case strings.HasPrefix(modelID, "anthropic."):
85+
writeJSON(w, http.StatusOK, map[string]any{
86+
"content": []map[string]any{{"type": "text", "text": "Mock response from Claude: "}},
87+
"stop_reason": "end_turn",
88+
"usage": map[string]any{"input_tokens": 10, "output_tokens": 20},
89+
})
90+
case strings.HasPrefix(modelID, "amazon.titan-"):
91+
writeJSON(w, http.StatusOK, map[string]any{
92+
"results": []map[string]any{{"outputText": "Mock response from Titan: ", "tokenCount": 20, "completionReason": "FINISH"}},
93+
})
94+
case strings.HasPrefix(modelID, "meta.llama"):
95+
writeJSON(w, http.StatusOK, map[string]any{
96+
"generation": "Mock response from Llama: ",
97+
"prompt_token_count": 10,
98+
"generation_token_count": 20,
99+
"stop_reason": "stop",
100+
})
101+
default:
102+
writeError(w, http.StatusNotFound, "ResourceNotFoundException", "Model not found")
103+
}
104+
}
105+
106+
func writeJSON(w http.ResponseWriter, status int, payload any) {
107+
w.Header().Set("Content-Type", jsonContentType)
108+
w.WriteHeader(status)
109+
_ = json.NewEncoder(w).Encode(payload)
110+
}
111+
112+
func writeError(w http.ResponseWriter, status int, code, message string) {
113+
writeJSON(w, status, map[string]any{"__type": code, "message": message})
114+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package bedrock
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
)
10+
11+
func TestListFoundationModels(t *testing.T) {
12+
h := newServer()
13+
req := httptest.NewRequest(http.MethodPost, "/foundation-models", nil)
14+
rec := httptest.NewRecorder()
15+
h.ServeHTTP(rec, req)
16+
17+
if rec.Code != http.StatusOK {
18+
t.Fatalf("expected 200, got %d", rec.Code)
19+
}
20+
if got := rec.Header().Get("Content-Type"); got != jsonContentType {
21+
t.Fatalf("expected content type %s, got %s", jsonContentType, got)
22+
}
23+
24+
var resp map[string]any
25+
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
26+
t.Fatalf("unmarshal response: %v", err)
27+
}
28+
29+
summaries, ok := resp["modelSummaries"].([]any)
30+
if !ok {
31+
t.Fatalf("expected modelSummaries array, got %T", resp["modelSummaries"])
32+
}
33+
if len(summaries) != 4 {
34+
t.Fatalf("expected 4 models, got %d", len(summaries))
35+
}
36+
}
37+
38+
func TestInvokeModelClaude(t *testing.T) {
39+
h := newServer()
40+
payload := map[string]any{
41+
"messages": []map[string]any{{"role": "user", "content": "hello"}},
42+
"max_tokens": 10,
43+
}
44+
resp := performInvoke(t, h, "/model/anthropic.claude-3-sonnet-20240229-v1:0/invoke", payload)
45+
46+
content := resp["content"].([]any)
47+
first := content[0].(map[string]any)
48+
if first["text"] != "Mock response from Claude: " {
49+
t.Fatalf("expected mock claude text, got %v", first["text"])
50+
}
51+
if resp["stop_reason"] != "end_turn" {
52+
t.Fatalf("expected end_turn, got %v", resp["stop_reason"])
53+
}
54+
}
55+
56+
func TestInvokeModelTitan(t *testing.T) {
57+
h := newServer()
58+
payload := map[string]any{"inputText": "hello"}
59+
resp := performInvoke(t, h, "/model/amazon.titan-text-express-v1/invoke", payload)
60+
61+
results := resp["results"].([]any)
62+
first := results[0].(map[string]any)
63+
if first["outputText"] != "Mock response from Titan: " {
64+
t.Fatalf("expected mock titan text, got %v", first["outputText"])
65+
}
66+
if first["completionReason"] != "FINISH" {
67+
t.Fatalf("expected FINISH, got %v", first["completionReason"])
68+
}
69+
}
70+
71+
func performInvoke(t *testing.T, h http.Handler, path string, payload map[string]any) map[string]any {
72+
t.Helper()
73+
body, err := json.Marshal(payload)
74+
if err != nil {
75+
t.Fatalf("marshal payload: %v", err)
76+
}
77+
req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(body))
78+
rec := httptest.NewRecorder()
79+
h.ServeHTTP(rec, req)
80+
81+
if rec.Code != http.StatusOK {
82+
t.Fatalf("expected 200, got %d, body=%s", rec.Code, rec.Body.String())
83+
}
84+
if got := rec.Header().Get("Content-Type"); got != jsonContentType {
85+
t.Fatalf("expected content type %s, got %s", jsonContentType, got)
86+
}
87+
88+
var resp map[string]any
89+
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
90+
t.Fatalf("unmarshal response: %v", err)
91+
}
92+
return resp
93+
}

0 commit comments

Comments
 (0)