Commit Graph

1802 Commits

Author SHA1 Message Date
Julian Sommer 020344ec46 release: update go.mod for v1.16.0 v1.16.0 2026-08-12 09:14:29 +10:00
Ben Duncan c92294e7d5 feat: render predastore templates with the cluster and S3 planes split (#763)
* feat: render predastore templates with the cluster and S3 planes split

The templates set one bind_addr per host, which put raft and blob wherever
S3 was published. A host now binds the cluster plane on the address peers
dial and the gate carries its own bind_addr for the S3 API, so the installer
can hand S3 the WAN without exposing replication with it.

The single-node template also drops to one node of each role. Extra blob
nodes only hash-partition keys across more badger indexes on the same drive,
and extra meta replicas add fsyncs for a quorum that dies with the machine.

* docs: regenerate the AWS model operation coverage table

* fix: repin predastore to the merged dev commit
2026-08-11 16:35:42 -05:00
joshsiv-mulga 203b805cf0 docs: lead IAM pages with the AWS concept terms users search 2026-08-11 19:35:30 +10:00
joshsiv-mulga 8006cc2f17 docs: SEO pass on the IAM docs cluster
Rework the frontmatter titles and descriptions for the five Identity
docs so each leads with the AWS concept term users actually search, and
add contextual in-prose links between them. Only the roles and instance
profiles page currently earns search impressions, so it now links to all
four siblings and each sibling links back into the cluster.
2026-08-11 17:58:27 +10:00
brynmailer-mdc d40747acae fix: repin predastore to the merged dev commit 2026-08-11 17:23:42 +10:00
brynmailer-mdc fbeb2133c3 docs: note the release range the object storage exemption applies to 2026-08-11 17:23:42 +10:00
brynmailer-mdc 197ae5ccf1 fix: repin predastore to the commit carrying the pkg/ layout
The branch imports predastore/pkg/auth, predastore/pkg/ratelimit and the
root predastore package, none of which exist in the pinned v1.15.0. Every
build that goes through the go.work workspace resolved those from the
sibling checkout and passed; the e2e binary build on the CI runner uses a
bare checkout governed by go.mod alone, and failed to resolve them.
2026-08-11 17:23:42 +10:00
brynmailer-mdc 1394fbf488 docs: describe the predastore host and node model
The port tables were wrong in both directions: inter-node traffic is QUIC over
UDP rather than TCP, and the shard range 9991-9993 no longer exists. Ports are
unique within a host rather than across the cluster, so every machine uses the
same three, and a single-node install opens only the S3 port because colocated
nodes talk over an in-process pipe.

The reference architectures scripted a move of node data out of a directory
layout that no longer exists, which would have broken any install following
them. The update guide now says object storage has no migration path.
2026-08-11 17:23:42 +10:00
brynmailer-mdc a7a62addeb test: rebuild the predastore fixture and service tests on the facade
The fixture wrote a config the strict decoder rejects and built its cluster
through clusterrun, so it now renders a v1 TOML, loads it through LoadConfig and
serves under Run. Loading the real file means the fixture trips over the same
validation an install does. Its data directory holds node-<id> directly, and it
drains on cancellation rather than being abandoned at exit.

The runtime tests drove buildRuntime, serve and the old on-disk layout, all of
which predastore owns now, so they go. What replaces them covers what spinifex
still decides: the flag merge and its precedence, a host id or gate node the
config does not have, and the pid file behind Stop and Status.

Two fixes to earlier commits in this series: the host id conversion moves behind
its own guard, and the node status probes use WaitGroup.Go.
2026-08-11 17:23:42 +10:00
brynmailer-mdc 0a22e86304 refactor: read the nested predastore topology and rename storage nodes to blob and meta
Hosts carry their nodes under [[host.node]] now, and a node's address is its
host's addr plus its own port rather than a single host address it shared. The
gate belongs to neither storage table, so it is matched explicitly and an
unknown role still warns.

The reported vocabulary follows predastore's: db_nodes and shard_nodes become
meta_nodes and blob_nodes through the daemon, the gateway and the UI. Bucket
type is gone from predastore entirely, so the field goes with it.
2026-08-11 17:23:42 +10:00
brynmailer-mdc 89df817601 refactor: run the predastore service through predastore.Run
The facade owns every node of a host, the S3 gate among them, so the service no
longer builds a cluster runtime or a gateway of its own. Waiting for a leader,
draining the gateway and the shutdown grace all moved behind Run; pprof and the
base path have no equivalent and are gone.

spinifex.toml stays authoritative for the S3 endpoint: the host entry is
settled from the service flags before Run, and the gate is found by role rather
than by position. A host id is now required, so init writes one for single-node
installs too, where the topology was previously left implicit.

BasePath survives as the pid directory alone. Stop and Status now read the same
value start writes, which they did not before.
2026-08-11 17:23:42 +10:00
brynmailer-mdc 588162c136 feat: rewrite the predastore config templates onto the v1 schema
The decoder is strict now, so every key the reorganised predastore no longer
reads is a startup failure rather than an ignored line. Both templates move to
nested [[host.node]] tables with the gate, blob and meta roles, portless
bind_addr and addr, singular [[bucket]], and the host TLS and encryption-key
paths the schema requires. version resets to 1 with the format.

A gate node is what serves the S3 API, so a host without one answers nothing.
Ports are unique within a host rather than across the cluster: gate 8443, blob
from 6660, meta from 7660. Colocated nodes bind no socket and still need one.
2026-08-11 17:23:42 +10:00
brynmailer-mdc 94db344556 refactor: stop probing predastore for a raft role
The daemon dialled a /status endpoint on each meta node to report which held
raft leadership. That endpoint lived in predastore's s3db package; after the
transport collapse the meta nodes speak rpc over QUIC and the in-process pipe
and expose no HTTP surface, so the probe dialled a dead port.

NodeStatusResponse.PredastoreRole is retained and left empty. Every consumer
already guards on the empty string, so the CLI and the UI simply omit the
badge, and restoring the probe later needs no contract change.
2026-08-11 17:23:42 +10:00
brynmailer-mdc 96dc1af568 refactor: delete the predastore cluster topology migration
The migration rewrote a v4 topology into v5 and recovered raft state through
predastore's clusterrun package, which no longer exists. The on-disk layout it
relocated data into has moved too: node directories are now node-<id> under the
host root, with no host-N level. Nothing migrates a pre-cutover install; it is
re-initialised from scratch.

node-reset.sh keeps directories by design, so it now removes the predastore
cluster tree outright rather than leaving the old skeleton standing.
2026-08-11 17:23:42 +10:00
brynmailer-mdc c5995e8c69 refactor: repoint the moved predastore auth and ratelimit imports under pkg/ 2026-08-11 17:23:42 +10:00
Julian Sommer 57d3db1683 feat: installer multidisk roles (#760) 2026-08-11 13:52:38 +10:00
Julian Sommer 89916e841f last README trim 2026-08-11 11:29:03 +10:00
Julian Sommer 3c5840a4b6 fix: README link 2026-08-11 11:27:21 +10:00
Julian Sommer 6a449d7ddd fix: last README tweaks 2026-08-11 11:26:04 +10:00
Julian Sommer 7dd3961e27 fix: README tweaks 2026-08-11 11:19:11 +10:00
Josh Siviour 2f0eb734ba feat(bedrock): guardrail CRUD store and ARN (#758)
* feat: add guardrail CRUD store and ARN support to bedrock gateway

Stage 1 of Ochre Guardrails: control-plane store for CreateGuardrail,
CreateGuardrailVersion, GetGuardrail, ListGuardrails, UpdateGuardrail,
and DeleteGuardrail, backed by a JetStream KV bucket keyed per account.
Records keep the full policy config so later stages can build the
filter engine and inference enforcement on top without re-deriving it.

* feat: add deterministic guardrail filter engine and ApplyGuardrail op

Adds the pure wordPolicy/sensitiveInformationPolicy filter (regex-detectable
PII plus a starter profanity list), scaffolds content/topic/contextualGrounding
as NONE, and exposes it all through the bedrock-runtime ApplyGuardrail route.

* feat: enforce guardrails on Converse and ConverseStream

Wire GuardrailConfig enforcement into Router.Converse and
ConverseStream via a shared enforceGuardrail helper (resolve, load,
apply the deterministic filter engine). INPUT blocks skip the
backend entirely; OUTPUT blocks or ANONYMIZE redactions rewrite the
completion before it reaches the caller. ConverseStream buffers
delta text and assesses it at block/message-stop boundaries rather
than streaming unassessed model output. Both self-host and
Anthropic-direct backends are covered automatically since the
enforcement sits above Router's per-backend dispatch. Requests
without a GuardrailConfig are unaffected. An unresolvable or foreign
guardrail fails closed with ResourceNotFoundException.

* feat: enforce guardrails on InvokeModel and InvokeModelWithResponseStream

Adds the Invoke half of Ochre guardrails (plan §5), reusing the Stage 3
enforceGuardrail helper and Stage 2 filter engine unchanged. Guardrail
selection comes from the X-Amzn-Bedrock-Guardrail{Identifier,Version}
headers. Since InvokeModel bodies are family-native/opaque at the
Router, text is extracted/reinjected per family (llama self-host,
anthropic) in the new guardrail_invoke.go. INPUT is checked before the
backend is ever called (fail closed on an unresolved guardrail, no
backend call); OUTPUT is checked after, handling block/anonymize.
InvokeModelWithResponseStream buffers the inner stream and assesses
once at end-of-stream via a guardrailInvokeStreamSource decorator,
mirroring Stage 3's guardrailStreamSource.

* fix: route ApplyGuardrail through bedrock-runtime and use guardrail-specific RNF text

The service discriminator only promoted /model/... paths from bedrock to
bedrock-runtime, so ApplyGuardrail's singular /guardrail/.../apply route fell
through to the control-plane handler and was rejected. Extend the
discriminator to also promote /guardrail/..., which can't collide with the
plural /guardrails control-plane CRUD routes.

Also give guardrail-not-found paths their own ResourceNotFoundException
message instead of the generic "could not resolve the foundation model"
wording shared with model-lookup failures.
2026-08-11 09:55:38 +10:00
Julian Sommer 7440446bbb feat: install ext4 with role-assigned drives for spinifex and predastore (#759) 2026-08-11 09:50:42 +10:00
Julian Sommer 5301af4c7b chore: repin northstar to the otel v1.45.0 migration
CI builds spinifex in a workspace over viperblock and predastore only, so
northstar resolves from the proxy. Its v1.15.0 log sanitiser predates the
attribute.Value migration and does not compile against otel/log v0.21.0.
2026-08-11 09:45:02 +10:00
Julian Sommer e68c965ef8 chore: bump coverage req to 70 2026-08-11 09:40:24 +10:00
Julian Sommer 19024f79a4 chore: bump otel to v1.45.0 and the log family to v0.21.0
The log API moved bodies and attributes onto attribute.Value in v0.21.0, so the
otelslog bridge and the sibling log sanitisers have to move in the same window.
No spinifex source change is needed; the workspace resolves the migrated
predastore and viperblock.
2026-08-11 09:38:31 +10:00
Josh Siviour 835dbbaf1c feat: parameterise bedrock endpoint create/delete by account and pinned (#757)
* feat: parameterise bedrock endpoint create/delete by account and pinned

Widens EnsureEndpointInput/DescribeEndpointInput/DeleteEndpointInput
with AccountID and (Ensure-only) Pinned, and keys the daemon's
Ensure/Describe/Delete on the resolved account instead of the
hardcoded GlobalAccountID. An empty AccountID still resolves to
GlobalAccountID and Pinned defaults to false, so every existing
caller (resolver, reaper, evictForCapacity, the NATS gateway client)
is unchanged. This is stage 1 of provisioned throughput: it reuses
the existing launch/capacity/readiness machinery for a dedicated,
account-scoped, pinned endpoint without adding a parallel surface.

* feat(bedrock): add provisioned-throughput CRUD and PT ARN

Implements CreateProvisionedModelThroughput, Get, List, Update, and
Delete over a new bedrock-provisioned KV store, plus the
provisioned-model ARN format/parse. Create rejects provider-hosted
models and requests a pinned, account-scoped endpoint from the daemon;
Get derives status live from the pinned endpoint's state; Update
honours name changes only and rejects a served-model swap; Delete
tears down the pinned endpoint before removing the record.

EndpointProvisioner is declared in gateway_bedrock with primitive-typed
methods (handlers_bedrock already imports gateway_bedrock, so it can't
be imported back for its EndpointService interface directly); a small
adapter in handlers_bedrock bridges the two, always setting Pinned:true
and the caller's real account on every Ensure.

Does not yet wire a PT ARN into the Converse/InvokeModel inference
path (next stage).

* feat(bedrock): accept a provisioned-throughput ARN on Converse/InvokeModel

Translates a PT ARN before resolve: the gateway inference entry parses it,
loads the bedrock-provisioned record, and resolves against the pinned
endpoint for the commitment's own account rather than the GlobalAccountID
shorthand every bare modelId uses. Adds EndpointResolver.EndpointForAccount
(gateway-side interface, implemented by DynamicEndpointResolver) so the seam
stays import-cycle-safe, and threads it through vllm.go/invoke_llama.go's
resolve call sites. A foreign-account or unknown PT ARN reads as
ResourceNotFoundException, same as any other unresolvable model identifier.

* fix(bedrock): accept a provisioned-throughput ARN on streaming inference

ConverseStream and InvokeModelWithResponseStream built their router/adapter
with a nil ProvisionedStore and the GlobalAccountID-shorthand provider, so a
PT ARN was silently never translated on the streaming path even though the
non-stream Converse/InvokeModel siblings already handled it. Thread the
gateway's provisioned store through both entry points and give
Router.ConverseStream / InvokeStreamRouter.InvokeModelWithResponseStream the
same translate-before-resolve treatment Converse/InvokeModel already have.

* fix(bedrock): surface pinned endpoints in the operator CLI

Service.List only ever enumerated the GlobalAccountID key prefix, and
'endpoint describe' always looked up GlobalAccountID too, so a pinned,
account-scoped endpoint held a GPU while reading ABSENT and missing from
list. List now scans every account's records, and describe gains an
optional --account flag (default empty, preserving GlobalAccountID
resolution) to look up a specific account's endpoint. list/describe both
render the endpoint's account and pinned status.
2026-08-10 14:49:24 +10:00
Julian Sommer e710fdc1d0 chore: rebuild frontend + fix new lint errors 2026-08-10 12:35:26 +10:00
dependabot[bot] 7be9839c83 chore: bump the npm-dependencies group (#756) 2026-08-10 12:30:59 +10:00
Julian Sommer f6ef90a605 fix: fail closed in the resource authorization check (#755) 2026-08-10 12:28:03 +10:00
Josh Siviour 9b2334b964 fix(ochre): self-heal weights stage when the recorded snapshot is gone (#753)
* fix(ochre): self-heal weights stage when the recorded snapshot is gone

runStageWeights's idempotency check trusted a matching source URI without
verifying the snapshot it names still exists, so a host rebuild or volume
GC that lost the snapshot left stage permanently short-circuited on a
useless KV record until an operator ran 'weights remove' first. Inject a
weightsSnapshotChecker, verify liveness before the no-op return, and fall
through to re-materialise when the snapshot is gone. 'weights list' uses
the same checker to mark dangling entries so an operator can see which
staged models can actually serve without waiting for endpoint launch to
fail.

* docs: note weights stage self-heal and list STATUS column
2026-08-10 12:10:56 +10:00
Josh Siviour 46143be964 test(bedrock): cover resolveServingAMI's multi-match selection (#751)
Adds resolveServingAMI test cases: single match, multiple matches
selected deterministically by newest CreationDate independent of
describe-order, zero matches, and nil/empty ImageId entries skipped.
2026-08-10 12:10:40 +10:00
Julian Sommer f243061e17 fix: STS unbounded body read 2026-08-10 11:17:01 +10:00
Julian Sommer 430c02de88 fix: enforce TLS 1.3 in spinifexui 2026-08-10 11:13:12 +10:00
Julian Sommer cfaa7e04eb fix: check resource ownership in ELBv2 handlers 2026-08-10 11:09:48 +10:00
Julian Sommer 26afcfa77e fix: check source snapshot ownership 2026-08-10 10:54:49 +10:00
Julian Sommer 3601bb4d36 fix: don't output secret keys on installs 2026-08-10 10:40:50 +10:00
Julian Sommer 53b31cb76c fix: keep the RDS master password out of the server and console logs (#754) 2026-08-10 10:16:53 +10:00
Julian Sommer 7b33219617 fix: slow tests 2026-08-10 09:29:00 +10:00
Julian Sommer ff4f46dcb5 chore: run dependabot on sunday 2026-08-10 09:19:40 +10:00
Julian Sommer 92dae2e449 fix: judge transitional-state heartbeats by the persist floor (#752) 2026-08-10 09:18:11 +10:00
Josh Siviour d178ef5992 feat: version-aware host-asset preflight for binary-swap deploys (#750)
A binary-swap deploy (make deploy or a manual spx copy) ships a new binary
whose host-config prerequisites — the privileged helper scripts and their
sudoers grant, placed only by setup.sh / setup-ovn.sh — it does not ship.
The node looks healthy and the gap surfaces later as a cryptic launch
failure or a silent security regression.

Add a preflight package that checks the host against what this build
expects, comparing asset content, not existence, so a stale-but-present
asset is caught too. The managed helpers install from quoted heredocs, so
their on-disk bytes equal the heredoc body verbatim; the binary carries
the canonical sha256s and compares.

- spinifex/preflight: CheckHost / CheckHostAt, content hash of the two
  managed helpers and a NOPASSWD grant-line check for the sysctl helper.
- Generated canonicalHashes plus a drift guard that re-extracts the
  heredocs and fails if the committed manifest no longer matches.
- Three surfaces: spx admin preflight, a make deploy gate
  (SKIP_ASSET_PREFLIGHT=1 to bypass), and a non-fatal daemon boot warning.
2026-08-10 07:52:39 +10:00
Josh Siviour bc3d59c0c3 fix(gpu): treat an already-released GPU as success during teardown (#749)
Release reported "nothing was claimed" as an error, and the terminated-instance
teardown reaper persisted that as the gpu teardown mark. The mark could then
never reach done, so the record was never purged and the GC re-drove it every
two minutes for as long as it existed, failing identically each time because
the condition being retried was that the work had already been done.

Stop-then-terminate is ordinary: the stop releases the GPU and the later
terminate finds nothing left to release. Two instances on wattle had been in
this state for days, at 2251 and 640 retries, logging an ERROR naming the
node's only GPU and falsely claiming it might need a manual rebind.

Release now returns a matchable ErrNoGPUClaimed and the teardown cleaner reads
it as success, which is what the reaper already documents its cleaners to do.
The sentinel stays an error rather than becoming nil so the launch-rollback
path can still tell that its claim/release pair is out of step, and so a real
vfio rebind failure - which clears Available, the health signal the Bedrock
capacity gate reads - stays distinct from the benign case.
2026-08-07 21:57:39 +10:00
Josh Siviour c0f3e0bee2 fix(bedrock): treat a claimed GPU as unavailable in the capacity gate (#748)
checkCapacity read only PoolEntry.Available, but that field tracks device
health: Release clears it when a vfio rebind fails, so a device that cannot
be handed back is not offered again. Occupancy is the separate InstanceID
field, which PoolEntry documents as empty when free, and gpu.Manager.Claim
tests both before taking a device.

Reading Available alone let a GPU already held by a running serving VM admit.
Ensure never took its capacity refusal, so evictForCapacity was never called
and LRU eviction never fired: on a single-GPU node the launch instead
proceeded to STARTING and failed later with InsufficientInstanceCapacity,
with no eviction attempted and no reason logged.

The existing table had a case named 'already claimed' that set Available to
false, which is not the shape Claim leaves behind. That encoded the wrong
model and is why the gap went unnoticed, so it is renamed to what it really
covers and the real claimed-device shapes are added alongside.

Also stop daemonGPUSnapshotter capturing d.gpuManager by value.
applyGPUConfig swaps that pointer on a passthrough toggle and re-points
daemonGPUClaimer but not the snapshotter, which would leave bedrock reading
an orphaned pool that sees no claims and admits everything.
2026-08-07 14:37:32 +10:00
Josh Siviour c7fdd23b7e feat(bedrock): add Llama 3.2 3B Instruct as a second self-host model (#747)
The catalog carried exactly one self-host entry, so two models could never
contend for a GPU: an Ensure for the model already running returns the
existing record before reaching the capacity check. LRU eviction was
therefore built and unit-tested but unable to fire in production.

Llama 3.2 3B Instruct is a real AWS-published Bedrock model ID, so it keeps
the drop-in-SDK promise, and it is a genuine quality tier above the 1B model
rather than a fixture added only to unblock a test.

Staying inside the Meta family is deliberate. InvokeRouter dispatches
self-host requests to llamaInvokeAdapter on tier alone, so a model of another
family would be served with Llama's native request and response schema
instead of being refused. A new test pins that assumption to the catalog so
it fails loudly rather than silently mis-serving.

Sizing for the RTX A1000: 3.21B params at bf16 is roughly 6124 MiB of weights
against 8188 MiB of VRAM, so --enforce-eager skips CUDA graph capture to free
several hundred MiB and --max-model-len is half the 1B entry's. fp8 is not an
option on this GPU: GA107 is compute capability 8.6, and fp8 needs sm_89.
These figures are estimates to be corrected against a live reading.

Weights gating is now measurably per-entry rather than all-or-nothing, which
only became observable once two self-host entries could disagree.
2026-08-07 12:46:48 +10:00
Josh Siviour bc1dd3413e fix(bedrock): make the reaper's decision visible to an operator (#746)
Three gaps found while verifying idle reclaim on live hardware.

The reclaim log line passed a time.Duration, which slog renders as an int64
nanosecond count in JSON: the line read idleFor:135000000000 rather than 2m15s,
in journalctl and in the metrics sink alike. It now logs an integer second
count under a unit-suffixed field, which is both readable and still
aggregatable — a string would have cost the latter. The same shape appears at
around ten other sites across the fleet and is tracked separately.

The endpoint command's help still told operators the gateway could not request
endpoints itself, which stopped being true when dynamic resolution landed. It
now describes the lifecycle that actually runs, and says what the manual
commands are still for.

describe showed nothing the idle sweep observed, so an operator asking why an
endpoint was or was not reclaimed had to read the KV. It now reports in-flight
count, last activity, idle duration, pinned status and consecutive scrape
failures for a READY endpoint, and nothing for any other state, since no other
state is swept.

LastActive moves onto EndpointRecord so the idle clock, the LRU eviction order
and this output all read one definition rather than three.
2026-08-07 09:43:49 +10:00
Josh Siviour 35ccb298df feat(bedrock): evict the LRU idle endpoint when a launch has no GPU (#745)
A refused launch and an idle GPU could coexist indefinitely: capacity said no
while a device sat under a model nobody was calling. Ensure now makes room once
before giving up, choosing the least recently used endpoint that is safe to
stop — READY, nothing in flight, not pinned, past minResidency, and on this
node, since GPU capacity is node-local and stopping an endpoint elsewhere frees
nothing here.

The eviction goes through Delete, so the device is released by the same
DRAINING -> terminate -> release path an operator delete takes rather than
being taken from underneath a running process. Nothing evictable returns the
original ModelNotReadyException unchanged: capacity contention breaches no
quota and must not claim to.

minResidency defaults to 5m so two models cannot thrash each other with a full
cold boot per call. Beyond that the deployment is under-provisioned, and the
eviction rate is what says so.

Also adds coldStartWait (OCHRE_COLD_START_WAIT), default 0. At zero the
fail-fast contract is unchanged and costs no extra describe. Above zero the
resolver polls inside its existing singleflight, so a burst of cold callers
shares one wait and one launch. The default stays 0 because the measurement
behind it has not changed: cold start is minutes, and no client retry policy
spans that.
2026-08-06 22:32:20 +10:00
Josh Siviour f7d61f8ce1 feat(bedrock): reclaim idle serving GPUs with a leader-elected reaper (#744)
A launched model held its GPU until an operator deleted the endpoint. On a
single-GPU deployment that made every other model in the catalog permanently
unreachable, with no path to recovery that did not involve a human.

The reaper scrapes each READY endpoint's vLLM Prometheus /metrics and reads
idleness from the serving process itself rather than from gateway-reported
activity, which keeps the daemon the single writer of endpoint state and works
unchanged with any number of gateways. Idle means both scheduler queues are
empty and the completed-request counter has not advanced since the previous
sweep: the counter clause is what catches a request that started and finished
entirely between two sweeps.

Two rules bound the reclaim. An endpoint is never reaped within idleTTL of
readyAt, so it cannot be taken back between reaching READY and the request that
launched it arriving. A failed scrape is unknown, never idle; five consecutive
failures terminate and relaunch, so a wedged VM is not mistaken for a quiet one
and left holding a device.

Observations are persisted on the record rather than kept in the reaper's
memory: a leader change must not reset the idle clock, and the eviction
decision that follows in the next change runs on whichever replica the queue
group picked. A sweep writes only when what it saw actually changed.
2026-08-06 22:24:00 +10:00
Josh Siviour fa070a5359 feat(bedrock): resolve self-host endpoints through the daemon registry (#742)
The inference path resolved self-host models from a static pinned map and
never asked for a launch, so on any deployment that had not pinned an endpoint
by hand a staged model was advertised, accepted by the access gate, and then
rejected by every invoke forever with no VM ever started.

Add a resolver over the endpoint registry, satisfying the existing
EndpointResolver interface so the provider and router layers are unchanged.
It lives in handlers/bedrock rather than gateway/bedrock because that package
cannot import this one; restating the subjects and payloads gateway-side would
be two definitions of one wire contract, free to drift.

Resolution order keeps the escape hatch first: OCHRE_VLLM_ENDPOINTS wins, then
a short-TTL cache of READY base URLs, then describe. STARTING and DRAINING do
not resolve and do not re-request, since a launch or a teardown is already in
flight. ABSENT requests one and reports not-resolved, which the invoke paths
turn into a retryable ModelNotReadyException — the cold-start contract.

Concurrent cold calls for one model collapse through singleflight, so a burst
costs one describe and one ensure rather than a pair per request. Describe and
ensure errors propagate rather than being flattened into a cold start, so a
dead control plane stays distinguishable from a model that is merely starting,
and the daemon's own refusals keep their AWS codes.
2026-08-06 21:41:50 +10:00
Josh Siviour 2cbf50adf7 fix(vm): converge when the GC finalises an instance mid-terminate (#743)
finalizeTerminated treated a refused terminated -> terminated transition as a
failure, but reaching the state we were driving to is convergence: the VM GC
had reconciled the same instance while terminateCleanup was running.

The caller pays for that error. Observed on wattle: an Ochre endpoint delete
wrote DRAINING, lost this race, and returned before the DRAINING -> ABSENT
write, so the VM was gone and the GPU released while the record stayed
DRAINING. Delete only accepted READY, so no retry could clear it and the model
was unreachable until the KV was edited by hand. ELB, EKS and RDS system
instances race the same GC.

So converge in the vm layer when the instance did reach terminated, keeping
the refusal for one that did not, and let Delete resume from DRAINING: a
teardown that failed partway has to be retryable.
2026-08-06 20:56:29 +10:00
Julian Sommer f5adfb1c79 fix: skip parameter groups deleted mid-describe 2026-08-06 16:28:26 +10:00