diff --git a/docs/http-api.md b/docs/http-api.md index 597f985..8cc103a 100644 --- a/docs/http-api.md +++ b/docs/http-api.md @@ -54,7 +54,9 @@ with no `address`: ] ``` -Runner records also carry mode, capacity, prices, and optional metadata/GPU fields. +Runner records also carry `runner_id` (the registered runner identity), mode, capacity, +prices, and optional metadata/GPU fields. Consumers can bind admission to this identity +without deriving it from the proxy URL or conflating it with the application name. Discovery preserves complete runner records. `nodes` lists successfully queried, non-evicted sources, and `queried_at` records each source's last successful query time. Empty node and runner lists are omitted. With neither to advertise, discovery diff --git a/orchestrator/http_test.go b/orchestrator/http_test.go index 01d7fef..a8021b4 100644 --- a/orchestrator/http_test.go +++ b/orchestrator/http_test.go @@ -353,8 +353,9 @@ func TestTrickleChannelsScopedToSession(t *testing.T) { func TestRunnerGPUIsPreservedForPythonDiscovery(t *testing.T) { var request heartbeatRequest - require.NoError(t, json.Unmarshal([]byte(`{"runner_id":"gpu-runner","runner_url":"https://runner.example","app":"retained","mode":"single-shot","status":"ready","capacity":1,"gpu":{"id":"0","name":"H100","vram_mb":80000}}`), &request)) + require.NoError(t, json.Unmarshal([]byte(`{"runner_id":"gpu-runner","proxy":true,"runner_url":"https://runner.example","app":"retained","mode":"single-shot","status":"ready","capacity":1,"gpu":{"id":"0","name":"H100","vram_mb":80000}}`), &request)) registry := NewRegistry("bootstrap", "https://orchestrator.example", time.Second, 30*time.Second) + registry.proxyTemplate = "https://{proxy}.example" _, status, err := registry.Heartbeat(request, "bootstrap") require.NoError(t, err) require.Equal(t, http.StatusOK, status) @@ -362,4 +363,10 @@ func TestRunnerGPUIsPreservedForPythonDiscovery(t *testing.T) { require.Len(t, entries, 1) require.Len(t, entries[0].Runners, 1) require.Equal(t, "H100", entries[0].Runners[0].GPU.Name) + encoded, err := json.Marshal(entries[0].Runners[0]) + require.NoError(t, err) + var record map[string]any + require.NoError(t, json.Unmarshal(encoded, &record)) + require.Equal(t, "gpu-runner", record["runner_id"]) + require.Equal(t, "https://gpu-runner.example", record["url"]) } diff --git a/orchestrator/registry.go b/orchestrator/registry.go index 1b6ef63..2c6a1f0 100644 --- a/orchestrator/registry.go +++ b/orchestrator/registry.go @@ -384,6 +384,7 @@ func (r *Registry) Unregister(id, auth string) (int, error) { } type discoveryRunner struct { + RunnerID string `json:"runner_id"` ObservedAt string `json:"observed_at"` URL string `json:"url"` App string `json:"app"` @@ -414,7 +415,7 @@ func (r *Registry) Discovery() []discoveryEntry { continue } available := max(0, item.Capacity-len(item.Sessions)) - result = append(result, discoveryRunner{ObservedAt: observed, URL: r.discoveryURL(id, item), App: item.App, Version: item.Version, Metadata: item.Metadata, GPU: item.GPU, Mode: item.Mode, Capacity: item.Capacity, CapacityUsed: len(item.Sessions), CapacityAvailable: available, PriceInfo: item.PriceInfo}) + result = append(result, discoveryRunner{RunnerID: id, ObservedAt: observed, URL: r.discoveryURL(id, item), App: item.App, Version: item.Version, Metadata: item.Metadata, GPU: item.GPU, Mode: item.Mode, Capacity: item.Capacity, CapacityUsed: len(item.Sessions), CapacityAvailable: available, PriceInfo: item.PriceInfo}) } return []discoveryEntry{{Address: r.service, Runners: result}} } diff --git a/signer/discovery_test.go b/signer/discovery_test.go index d2ebf84..d1ef8fa 100644 --- a/signer/discovery_test.go +++ b/signer/discovery_test.go @@ -28,7 +28,7 @@ func discoveryResponse(s *Service, query string) *httptest.ResponseRecorder { } func TestSignerDiscovery(t *testing.T) { - const advertised = `[{"address":"http://localhost:9000","runners":[{"url":"http://localhost:9000/app","observed_at":"2026-01-01T00:00:00Z","app":"retained","version":"v1","metadata":"model-x","mode":"single-shot","gpu":{"id":"0","name":" H100 ","vram_mb":80000},"capacity":3,"capacity_used":1,"capacity_available":2,"price_info":{"price":10,"price_usd":0.1,"currency":"wei","unit":"fixed"}},{"url":"http://localhost:9000/cpu","observed_at":"2026-01-01T00:00:00Z","app":"cpu","mode":"single-shot","capacity":1,"price_info":{"price":1,"currency":"wei","unit":"fixed"}}]}]` + const advertised = `[{"address":"http://localhost:9000","runners":[{"runner_id":"gpu-runner","url":"https://gpu-runner.example","observed_at":"2026-01-01T00:00:00Z","app":"retained","version":"v1","metadata":"model-x","mode":"single-shot","gpu":{"id":"0","name":" H100 ","vram_mb":80000},"capacity":3,"capacity_used":1,"capacity_available":2,"price_info":{"price":10,"price_usd":0.1,"currency":"wei","unit":"fixed"}},{"url":"http://localhost:9000/cpu","observed_at":"2026-01-01T00:00:00Z","app":"cpu","mode":"single-shot","capacity":1,"price_info":{"price":1,"currency":"wei","unit":"fixed"}}]}]` bodies := map[string]string{ "empty": "[]", "good": advertised, "failed": "", "malformed": "not JSON", "metadata": `[{"nodes":[]}]`, @@ -124,6 +124,12 @@ func TestSignerDiscovery(t *testing.T) { require.Equal(t, expected[0].Address, result[0].Address) want := slices.DeleteFunc(slices.Clone(expected[0].Runners), func(r discoveredRunner) bool { return !slices.Contains(test.apps, r.App) }) require.Equal(t, want, result[0].Runners, "preserve advertised fields and URLs") + for _, runner := range result[0].Runners { + if runner.App == "retained" { + require.Equal(t, "gpu-runner", runner.RunnerID) + require.Equal(t, "https://gpu-runner.example", runner.URL) + } + } }) } } diff --git a/signer/service.go b/signer/service.go index 7c0cca0..e2d4029 100644 --- a/signer/service.go +++ b/signer/service.go @@ -210,6 +210,7 @@ type discoveredGPU struct { } type discoveredRunner struct { + RunnerID string `json:"runner_id,omitempty"` ObservedAt string `json:"observed_at"` URL string `json:"url"` App string `json:"app"`