Repository navigation
GH-31: per-node model awareness - #41
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-node model awareness (GH-31)
Closes #31
Summary
The gateway now discovers each node's served models via
GET /v1/modelsand excludes nodes that cannot serve the requested model, without coupling discovery failures to health.What changed
internal/health/): per-nodeModelDiscoverywith statesuntried/known/known_empty/degraded/expired. Exact string equality ondata[].id— no aliases.discoverAllis a non-blocking spawn with its ownmodelsCheckinginflight map. Listing failures never touchStatus,ConsecutiveFailures, or the health fail threshold. A hung/v1/modelscannot delay health checks (proven by test).untried(zero successful fetches,Fails < N) is a filter identity — nodes pass. NoStart()⇒ stayuntried.data: []responses enterknown_empty(excluded from all model routing).Names(last-known-good). Never cleared on error.health_fail_thresholdconsecutive listing failures →expired(excluded until listing recovers). No wall-clock stale deadline.datafield are parse errors, not empty successes — an error body on HTTP 200 cannot poisonknown_empty.internal/api/): afterfilterByRing, nodes are filtered byAllows(modelName)on the injected model name. Per-node exclusion reasons (missing/empty/expired) are counted. Empty pool still returns 503no healthy node available(unchanged, alert-safe)./healthis additive only:status/nodes/healthyunchanged. Newmodels_ok(count ofknown+known_emptynodes) anddetails[].models{state,names,age_seconds,fails,last_error}.gateway_model_filter_exclusions_total{model,reason},gateway_node_models_ok{node},gateway_model_discovery_total{node,result}. All existing metric names/labels and log messages are immutable.models discovered,node reports no models,model discovery failed,model discovery expired,model discovery recovered,node model list changed,configured model not served by node. Exactly one message per state transition.Modelsstate (incl.Fails/Empties/LastError) acrossNewMonitorWithState;GetNodeStatesdeep-copiesModels.Names.Behavior change (documented)
Standalone/ad-hoc request models are now filtered the same way as configured models — a model missing from every node's known list returns 503.
models.defaultand every[[models.available]].namemust equal the node's served model id exactly. Discovery failures never mark a node unhealthy. See the README Limitations bullet.Soft-reject policy
warnConfigModelsMissingwarnsconfigured model not served by nodeonce per (node, model) per list change when a configured model that should be on the node is absent from its list. It never rejects config.models.defaultis checked only when it is absent from[[models.available]]entirely.Verification
go test ./...green;-raceonapi/health/metrics;GOTOOLCHAIN=go1.22.12(CI pin);-count=5stability onapi/health.make test vet fmtclean.Follow-ups (not blocking)
GetNodeStatesaliases theGPUsbacking array (pre-existing)/v1/modelsand metrics-scrape response body reads (hardening)Review notes
Two plan-mandated deviations were user-approved during execution:
parseModelListrejects bodies without adatafield (error bodies cannot feedknown_empty), and the transition-log switch emits exactly one message per transition per spec §6.3.filterByModelinsnapshot.gois a tested helper unused in production (handler uses an inline loop for per-node reason labels) — pre-accepted.