mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 10:52:04 +00:00
206 lines
8.0 KiB
Go
206 lines
8.0 KiB
Go
//go:build e2e
|
|
|
|
package bedrock
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/bedrock"
|
|
"github.com/aws/aws-sdk-go/service/bedrockruntime"
|
|
"github.com/mulgadc/spinifex/tests/e2e/harness"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Catalog model IDs mirror the static Phase-1 catalog in gateway_bedrock.
|
|
const (
|
|
modelSelfHostLlama = "meta.llama3-2-1b-instruct-v1:0"
|
|
modelAnthropic = "anthropic.claude-3-5-sonnet-20240620-v1:0"
|
|
modelBogus = "does.not-exist-v1:0"
|
|
)
|
|
|
|
// TestGetFoundationModel asserts ValidationException for an unknown model ID,
|
|
// as AWS returns. v1 ships no provider-tier entry that resolves unconditionally
|
|
// (modelAnthropic is not in the catalog at all — it returns the same
|
|
// ValidationException as a genuinely unknown ID), so the positive,
|
|
// known-model path is covered separately by TestGetFoundationModelSelfHost,
|
|
// which is gated on a real weights snapshot being staged.
|
|
func TestGetFoundationModel(t *testing.T) {
|
|
f := requireBedrockFixture(t)
|
|
|
|
t.Run("unknown model", func(t *testing.T) {
|
|
harness.ExpectError(t, "ValidationException", func() error {
|
|
_, e := f.AWS.Bedrock.GetFoundationModel(&bedrock.GetFoundationModelInput{
|
|
ModelIdentifier: aws.String(modelBogus),
|
|
})
|
|
return e
|
|
})
|
|
})
|
|
|
|
t.Run("provider-tier model ID is absent from v1", func(t *testing.T) {
|
|
harness.ExpectError(t, "ValidationException", func() error {
|
|
_, e := f.AWS.Bedrock.GetFoundationModel(&bedrock.GetFoundationModelInput{
|
|
ModelIdentifier: aws.String(modelAnthropic),
|
|
})
|
|
return e
|
|
})
|
|
})
|
|
}
|
|
|
|
// TestGetFoundationModelSelfHost describes the self-host model directly.
|
|
// Gated behind OCHRE_E2E_SELFHOST=1: besides a GPU-backed vLLM endpoint, it
|
|
// needs the model's weights snapshot staged in the bedrock-weights KV bucket.
|
|
func TestGetFoundationModelSelfHost(t *testing.T) {
|
|
if os.Getenv("OCHRE_E2E_SELFHOST") != "1" {
|
|
t.Skip("OCHRE_E2E_SELFHOST!=1; skipping self-host GetFoundationModel")
|
|
}
|
|
f := requireBedrockFixture(t)
|
|
|
|
out, err := f.AWS.Bedrock.GetFoundationModel(&bedrock.GetFoundationModelInput{
|
|
ModelIdentifier: aws.String(modelSelfHostLlama),
|
|
})
|
|
require.NoError(t, err, "get-foundation-model %s", modelSelfHostLlama)
|
|
require.NotNil(t, out.ModelDetails, "empty ModelDetails")
|
|
assert.Equal(t, modelSelfHostLlama, aws.StringValue(out.ModelDetails.ModelId))
|
|
}
|
|
|
|
// TestConverseSelfHost exercises the real vLLM Converse path. Gated behind
|
|
// OCHRE_E2E_SELFHOST=1 — it needs a GPU-backed vLLM endpoint configured on the
|
|
// gateway via OCHRE_VLLM_ENDPOINTS at boot.
|
|
func TestConverseSelfHost(t *testing.T) {
|
|
if os.Getenv("OCHRE_E2E_SELFHOST") != "1" {
|
|
t.Skip("OCHRE_E2E_SELFHOST!=1; skipping real self-host Converse")
|
|
}
|
|
f := requireBedrockFixture(t)
|
|
|
|
out, err := f.AWS.BedrockRuntime.Converse(&bedrockruntime.ConverseInput{
|
|
ModelId: aws.String(modelSelfHostLlama),
|
|
Messages: []*bedrockruntime.Message{{
|
|
Role: aws.String(bedrockruntime.ConversationRoleUser),
|
|
Content: []*bedrockruntime.ContentBlock{{Text: aws.String("Say hello in one word.")}},
|
|
}},
|
|
InferenceConfig: &bedrockruntime.InferenceConfiguration{MaxTokens: aws.Int64(64)},
|
|
})
|
|
require.NoError(t, err, "converse self-host")
|
|
require.NotNil(t, out.Output, "nil output")
|
|
require.NotNil(t, out.Output.Message, "nil output message")
|
|
assert.NotEmpty(t, converseText(out.Output.Message), "empty assistant text")
|
|
require.NotNil(t, out.Usage, "nil usage")
|
|
assert.Greater(t, aws.Int64Value(out.Usage.OutputTokens), int64(0), "OutputTokens must be > 0")
|
|
}
|
|
|
|
// TestConverseAnthropic exercises the real Anthropic-direct Converse path.
|
|
// v1 ships no provider-tier catalog entry, so modelAnthropic cannot resolve
|
|
// on any cluster; this always skips regardless of OCHRE_E2E_ANTHROPIC until a
|
|
// later phase re-enables the provider-direct tier.
|
|
func TestConverseAnthropic(t *testing.T) {
|
|
t.Skip("provider-direct tier not shipped in v1; anthropic entry is absent from the catalog")
|
|
if os.Getenv("OCHRE_E2E_ANTHROPIC") != "1" {
|
|
t.Skip("OCHRE_E2E_ANTHROPIC!=1; skipping real Anthropic Converse")
|
|
}
|
|
f := requireBedrockFixture(t)
|
|
|
|
out, err := f.AWS.BedrockRuntime.Converse(&bedrockruntime.ConverseInput{
|
|
ModelId: aws.String(modelAnthropic),
|
|
Messages: []*bedrockruntime.Message{{
|
|
Role: aws.String(bedrockruntime.ConversationRoleUser),
|
|
Content: []*bedrockruntime.ContentBlock{{Text: aws.String("Say hello in one word.")}},
|
|
}},
|
|
InferenceConfig: &bedrockruntime.InferenceConfiguration{MaxTokens: aws.Int64(64)},
|
|
})
|
|
require.NoError(t, err, "converse anthropic")
|
|
require.NotNil(t, out.Output, "nil output")
|
|
require.NotNil(t, out.Output.Message, "nil output message")
|
|
assert.NotEmpty(t, converseText(out.Output.Message), "empty assistant text")
|
|
require.NotNil(t, out.Usage, "nil usage")
|
|
assert.Greater(t, aws.Int64Value(out.Usage.InputTokens), int64(0), "InputTokens must be > 0")
|
|
assert.Greater(t, aws.Int64Value(out.Usage.OutputTokens), int64(0), "OutputTokens must be > 0")
|
|
}
|
|
|
|
// TestInvokeModelSelfHost exercises the real vLLM InvokeModel path with the
|
|
// Bedrock-native Llama request/response shape. Gated behind
|
|
// OCHRE_E2E_SELFHOST=1 — it needs a GPU-backed vLLM endpoint configured on the
|
|
// gateway via OCHRE_VLLM_ENDPOINTS at boot.
|
|
func TestInvokeModelSelfHost(t *testing.T) {
|
|
if os.Getenv("OCHRE_E2E_SELFHOST") != "1" {
|
|
t.Skip("OCHRE_E2E_SELFHOST!=1; skipping real self-host InvokeModel")
|
|
}
|
|
f := requireBedrockFixture(t)
|
|
|
|
reqBody, err := json.Marshal(map[string]any{
|
|
"prompt": "Say hello in one word.",
|
|
"max_gen_len": 64,
|
|
})
|
|
require.NoError(t, err, "marshal invoke-model request")
|
|
|
|
out, err := f.AWS.BedrockRuntime.InvokeModel(&bedrockruntime.InvokeModelInput{
|
|
ModelId: aws.String(modelSelfHostLlama),
|
|
Body: reqBody,
|
|
ContentType: aws.String("application/json"),
|
|
})
|
|
require.NoError(t, err, "invoke-model self-host")
|
|
assert.Equal(t, "application/json", aws.StringValue(out.ContentType))
|
|
|
|
var respBody struct {
|
|
Generation string `json:"generation"`
|
|
GenerationTokenCount int `json:"generation_token_count"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(out.Body, &respBody), "unmarshal invoke-model response")
|
|
assert.NotEmpty(t, respBody.Generation, "empty generation")
|
|
assert.Greater(t, respBody.GenerationTokenCount, 0, "GenerationTokenCount must be > 0")
|
|
}
|
|
|
|
// TestInvokeModelAnthropic exercises the real Anthropic-direct InvokeModel
|
|
// path with the Bedrock-native Claude Messages request/response shape. v1
|
|
// ships no provider-tier catalog entry, so modelAnthropic cannot resolve on
|
|
// any cluster; this always skips regardless of OCHRE_E2E_ANTHROPIC until a
|
|
// later phase re-enables the provider-direct tier.
|
|
func TestInvokeModelAnthropic(t *testing.T) {
|
|
t.Skip("provider-direct tier not shipped in v1; anthropic entry is absent from the catalog")
|
|
if os.Getenv("OCHRE_E2E_ANTHROPIC") != "1" {
|
|
t.Skip("OCHRE_E2E_ANTHROPIC!=1; skipping real Anthropic InvokeModel")
|
|
}
|
|
f := requireBedrockFixture(t)
|
|
|
|
reqBody, err := json.Marshal(map[string]any{
|
|
"anthropic_version": "bedrock-2023-05-31",
|
|
"max_tokens": 64,
|
|
"messages": []map[string]any{
|
|
{"role": "user", "content": "Say hello in one word."},
|
|
},
|
|
})
|
|
require.NoError(t, err, "marshal invoke-model request")
|
|
|
|
out, err := f.AWS.BedrockRuntime.InvokeModel(&bedrockruntime.InvokeModelInput{
|
|
ModelId: aws.String(modelAnthropic),
|
|
Body: reqBody,
|
|
ContentType: aws.String("application/json"),
|
|
})
|
|
require.NoError(t, err, "invoke-model anthropic")
|
|
assert.Equal(t, "application/json", aws.StringValue(out.ContentType))
|
|
|
|
var respBody struct {
|
|
Content []struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
} `json:"content"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(out.Body, &respBody), "unmarshal invoke-model response")
|
|
require.NotEmpty(t, respBody.Content, "empty content")
|
|
assert.NotEmpty(t, respBody.Content[0].Text, "empty assistant text")
|
|
}
|
|
|
|
// converseText concatenates the text content blocks of a Converse output message.
|
|
func converseText(msg *bedrockruntime.Message) string {
|
|
var text string
|
|
for _, c := range msg.Content {
|
|
if c != nil && c.Text != nil {
|
|
text += *c.Text
|
|
}
|
|
}
|
|
return text
|
|
}
|