Files
spinifex/tests/integration/bedrock_test.go
T
Josh Siviour 261d5d505e feat: Ochre catalog serving spec and a servable self-host model (#672)
* feat: in-tree serving specs + KV weights override for ochre catalog

Extend catalogEntry with MinVRAMMiB, InstanceType and VLLMArgs so a
self-host model's serving requirements ship with the catalog rather
than living only in an operator's head. Add WeightsStore, a
deployment-scoped (not account-scoped) JetStream KV override in a new
bedrock-weights bucket that resolves a model's staged weights snapshot
ID, following the same shape as the existing CredentialStore. It is
plumbed into tieredCatalog and GetFoundationModel through a
package-level resolver (SetWeightsResolver/currentWeightsResolver),
since both are called from gateway/bedrock.go's fixed-arity route
table, which is out of scope here.

A self-host model with no resolvable weights snapshot is not
servable and must not be advertised: tieredCatalog filters it out and
GetFoundationModel returns ResourceNotFoundException for it.

Replace the meta.llama3-70b-instruct-v1:0 self-host entry, which
needs roughly 140 GB of VRAM in fp16 and has never actually run, with
meta.llama3-2-1b-instruct-v1:0, a real AWS-published Bedrock model
ID. At bf16 its 1.24B params are roughly 2.5 GiB of weights, well
inside wattle's single RTX A1000 (8188 MiB); MinVRAMMiB is set to
4096 to budget headroom for CUDA context, activations and KV cache on
top of that. This is an estimate, not a measurement off wattle -
.7.5's launcher should correct it against a real nvidia-smi reading
once the model is actually running there.

lookupCatalogEntry, used by the untiered invoke/converse routing
path, is left unchanged and still ignores weights resolvability;
gating that path is out of scope for this bead.

* fix: separate weights resolve errors from not-found in ochre catalog

An internal fault (e.g. a JetStream outage) reaching the KV weights
resolver was being collapsed into the same "not servable" verdict as
a clean not-found: tieredCatalog would silently unadvertise every
self-host model, and GetFoundationModel would return
ResourceNotFoundException for a model that does exist but whose
resolver is temporarily broken. Both call sites now distinguish the
two: a resolve error is logged and surfaced as its own error (mapped
to ServerInternal at the AWS error boundary), and only a clean
not-resolvable result yields ResourceNotFoundException.

tieredCatalog's signature changes to return an error alongside the
entries so ListFoundationModels can propagate a resolve fault instead
of silently returning a thinned-out list.

Also raises the self-host entry's gpu-memory-utilization from 0.5 to
0.6 (roughly 4913 MiB of vLLM's own pool) and MinVRAMMiB from 4096 to
5120 to match: at 0.5, weights (~2.5 GiB) left under 1.5 GiB for KV
cache and activations at max-model-len=8192, likely too tight for any
real concurrency. The two numbers are meant to agree: MinVRAMMiB is
the admission-gate floor, gpu-memory-utilization is what tells vLLM
to actually claim that much on an 8188 MiB device.

Comments trimmed to the repo's 3-line limit and stripped of plan/bead
references; the VRAM reasoning and model-choice rationale live in the
commit body and the plan doc instead.

Threading the weights resolver through as an explicit parameter
(mirroring CredentialResolver) rather than a package-level global was
considered per review. Doing so needs edits to gateway/bedrock.go
(bedrockRouteHandler's signature, both route closures, the
Bedrock_Request call site, a new bedrockWeightsResolver() method) and
gateway/gateway.go (a new BedrockWeights field on GatewayConfig).
Both are outside gateway/bedrock/ and are explicitly out of scope for
this change, owned by a parallel workstream already modifying the
same route table. The global resolver stays, now with the
error-handling fix above.

* fix: stop e2e GetFoundationModel regressing on the new weights gate

TestGetFoundationModel's "known model" subtest asserted on
meta.llama3-2-1b-instruct-v1:0 unconditionally, but that entry is now
gated on a weights snapshot staged in the bedrock-weights KV bucket.
On any real deployment that bucket starts empty, so the subtest would
fail with ResourceNotFoundException outside of OCHRE_E2E_SELFHOST.

The "known model" assertion now targets the Anthropic entry, which
resolves unconditionally and keeps testing what the subtest was
actually there for: a known model round-trips, an unknown one is
ResourceNotFoundException. The self-host assertion moves to a new
TestGetFoundationModelSelfHost, gated behind OCHRE_E2E_SELFHOST=1
alongside TestConverseSelfHost, where weights are expected staged.

tests/integration/bedrock_test.go's TestListFoundationModels already
wires a weights-resolver stub before asserting the self-host entry is
present, so it does not regress the same way.

* test: correct a stale assertion message on the ochre catalog weights gate
2026-08-03 12:34:07 +10:00

69 lines
2.7 KiB
Go

//go:build integration
package integration
import (
"context"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/bedrock"
gateway_bedrock "github.com/mulgadc/spinifex/spinifex/gateway/bedrock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// modelSelfHostLlama and modelAnthropic mirror the static Phase-1 catalog in
// gateway/bedrock/catalog.go — see tests/e2e/bedrock/bedrock_test.go, whose
// TestListFoundationModels this ports.
const (
modelSelfHostLlama = "meta.llama3-2-1b-instruct-v1:0"
modelAnthropic = "anthropic.claude-3-5-sonnet-20240620-v1:0"
)
// integrationWeightsStub resolves a weights snapshot only for
// modelSelfHostLlama, standing in for the KV override that gates a self-host
// catalog entry on having somewhere to serve it from.
type integrationWeightsStub struct{}
func (integrationWeightsStub) Resolve(_ context.Context, modelID string) (string, bool, error) {
if modelID != modelSelfHostLlama {
return "", false, nil
}
return "snap-stub", true, nil
}
// TestListFoundationModels ports tests/e2e/bedrock/bedrock_test.go's
// TestListFoundationModels. ListFoundationModels is pure gateway-side static
// catalog logic (gateway/bedrock/catalog.go) with no NATS/daemon hop at all —
// no guest, no daemon wiring needed — so it round-trips through the real
// gateway HTTP/SigV4 path exactly like the live test, without a live cluster.
func TestListFoundationModels(t *testing.T) {
// Not t.Parallel(): ListFoundationModels/GetFoundationModel read the
// weights resolver through a package-level setter (StartGateway builds a
// GatewayConfig directly, with no field for it), so this test owns that
// process-wide state for its duration.
gateway_bedrock.SetWeightsResolver(integrationWeightsStub{})
t.Cleanup(func() { gateway_bedrock.SetWeightsResolver(nil) })
gw := StartGateway(t)
bedrockCli := gw.BedrockClient(t)
out, err := bedrockCli.ListFoundationModels(&bedrock.ListFoundationModelsInput{})
require.NoError(t, err, "list-foundation-models")
ids := make(map[string]*bedrock.FoundationModelSummary, len(out.ModelSummaries))
for _, m := range out.ModelSummaries {
ids[aws.StringValue(m.ModelId)] = m
}
assert.Contains(t, ids, modelSelfHostLlama, "self-host model must be advertised when its weights resolve")
// This tier configures no BedrockCredentials resolver, so the Anthropic
// entry is never resolvable and must not be advertised — the mirror image
// of the live test's "only assert when present" check (which tolerates
// either state depending on live cluster credential configuration).
_, anthropicPresent := ids[modelAnthropic]
assert.False(t, anthropicPresent, "anthropic model advertised without a resolvable credential")
}