feat(api): add subnet_endpoints root field#8050
Conversation
GET /api/v1/subnets/{netuid}/endpoints and the list_subnet_endpoints MCP
tool expose one subnet's endpoint/resource registry with full REST
filter/sort/page parity, but GraphQL had no equivalent: the only per-subnet
endpoint access was the nested Subnet.endpoints field, which took no
arguments and always returned the full list.
Add a root subnet_endpoints(netuid, ...) field and parameterize the nested
Subnet.endpoints field, both wired to the shared list-query engine:
- Root subnet_endpoints reuses loadSubnetEndpointsList -- the same loader
list_subnet_endpoints calls -- so REST, MCP, and GraphQL cannot drift. It
reads the baked per-subnet /metagraph/endpoints/{netuid}.json artifact
(distinct from the network-wide endpoints(...) registry), validates netuid
and every filter/sort value against the REST allowlists, and returns the
same {endpoints, total, returned, limit, cursor, next_cursor, sort, order}
envelope. A cold/absent artifact stays null (the per-subnet contract);
an invalid netuid or filter/sort surfaces as a BAD_USER_INPUT GraphQL
error, mirroring the subnet_candidates sibling.
- Nested Subnet.endpoints gains the same filter/sort/page arguments, filtered
through applyQueryFilters against the shared endpoints collection, mirroring
the nested Subnet.surfaces field (JSONbored#7885). It awaits the parent thunk first
so the bundled and lazy per-netuid paths both keep working; with no
arguments the full list passes through unchanged. An unsupported filter/sort
is a BAD_USER_INPUT error rather than a silently substituted default.
pool_eligible is a Boolean here and cursor is Int (offset), matching the
sibling nested field.
Regenerated generated/graphql/types.ts from the SDL.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8050 +/- ##
=======================================
Coverage 97.67% 97.67%
=======================================
Files 220 220
Lines 27206 27228 +22
Branches 10783 10793 +10
=======================================
+ Hits 26574 26596 +22
Misses 140 140
Partials 492 492
🚀 New features to boost your workflow:
|
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-24 21:01:57 UTC
Review summary Nits — 4 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
Summary
GET /api/v1/subnets/{netuid}/endpointsand thelist_subnet_endpointsMCP tool expose one subnet's endpoint/resource registry with full REST filter/sort/page parity, but GraphQL had no equivalent. The only per-subnet endpoint access was the nestedSubnet.endpointsfield, which took no arguments and always returned the full list.This adds a root
subnet_endpoints(netuid, ...)field and parameterizes the nestedSubnet.endpointsfield, both wired to the shared list-query engine — the same pattern the just-mergedsubnet_candidates(#7878/#8044) and nestedSubnet.surfaces(#7885/#8037) fields established.Closes #7869
What this adds
Root
subnet_endpoints(netuid: Int!, ...)→JSONloadSubnetEndpointsListunchanged — the exact loaderlist_subnet_endpointscalls — so REST, MCP, and GraphQL share one filter/sort/page contract and cannot drift. It reads the baked per-subnet/metagraph/endpoints/{netuid}.jsonartifact (distinct from the network-wideendpoints(...)registry).kind,layer,provider,publication_state,status,pool_eligible(atrue/falsestring, matching the reused loader's enum); rangesmin_/max_latency_msandmin_/max_score;fieldsprojection;sort+order;limit(1–100) /cursor(Int).{ endpoints, total, returned, limit, cursor, next_cursor, sort, order }.null(the documented per-subnet contract, mirroringsubnet_candidates), never a silently substituted empty list. An invalidnetuidor an unsupported filter/sort value surfaces as aBAD_USER_INPUTGraphQL error — not swallowed to[].Nested
Subnet.endpoints(...)→[Endpoint!]!applyQueryFiltersagainst the sharedendpointsquery-collection config (the same one the rootendpoints(...)field GraphQL parity: endpoints field missing kind/layer/provider/publication_state/status/pool_eligible/min-max_latency_ms/min-max_score/sort/order/fields filters #7887 uses), so the allowlists cannot drift.Subnet.surfacesimplementation (GraphQL parity: Subnet.surfaces nested field missing kind/provider/id/sort/order/limit/cursor filters #7885): an explicit resolve is attached to the built schema (a nested field needs one to see its own args), and it awaits the parent thunk first so both the bundled (single-subnet detail artifact) and lazy (per-netuid list) paths keep working — adding arguments does not collapse the lazy path to an empty list.BAD_USER_INPUTerror.pool_eligibleis aBooleanhere andcursorisInt(offset), matching the sibling nested field.Schema / tooling notes
generated/graphql/types.tsregenerated vianpm run build:graphql-types;npm run validate:graphql-types-driftpasses (types are current with the SDL).endpointscollection's validsortfields arekind,last_checked,latency_ms,layer,netuid,pool_eligible,provider,publication_state,score,status(noid) — reflected in the tests.subnet_endpointsis registered inFIELD_COMPLEXITYas a fan-out (relationship) field, matchingsubnet_candidates.Tests
Added to
tests/graphql.test.ts, covering every added runtime line and branch (100% patch coverage):nullon absent artifact; negative-netuid and unsupported-filter/sort →BAD_USER_INPUT; combined filters; provider /pool_eligible/ score-range filtering; sort + page +next_cursorround-trip; unexpected-loader-failure propagation; complexity weight.BAD_USER_INPUT; filtering the lazily-loaded (non-bundled) path.Local gates green:
vitest run tests/graphql.test.ts(1015 pass),eslint,prettier --check,tsc --noEmit(no new errors),validate:graphql-types-drift, and the public-safety scan.