diff --git a/backend/internal/service/protocol_registry.go b/backend/internal/service/protocol_registry.go index d953d061d..a2818032c 100644 --- a/backend/internal/service/protocol_registry.go +++ b/backend/internal/service/protocol_registry.go @@ -70,11 +70,13 @@ func (s *Service) PluginProviderCatalog(scope, capability string, includeUnavail continue } item := PluginProviderCatalogItem{ID: provider.ID, Version: plugin.Manifest.Version, Name: provider.Label, Vendor: plugin.Manifest.Author, Categories: provider.Capabilities, Scopes: provider.Scopes, BaseURL: provider.BaseURL, Enabled: plugin.Status == "enabled", UnavailableReason: plugin.Error, Workflows: workflowsForProvider(plugin.Manifest.Contributes.Workflows, provider.ID)} - item.Create = operationSummary(provider.Create) - if provider.Poll != nil { - item.Poll = operationSummary(*provider.Poll) + item.Create, item.Poll, item.ContentType = operationSummary(provider.Create), operationSummaryPtr(provider.Poll), provider.Create.ContentType + // The registry metadata is the canonical provider projection. This keeps + // host-backed dispatch paths out of every user-facing catalog consumer. + if adapter, ok := canonicalProviderAdapter(s.protocolRegistry(), provider.ID); ok { + metadata := adapter.Metadata() + item.Create, item.Poll, item.ContentType = metadata.Create, metadata.Poll, metadata.ContentType } - item.ContentType = provider.Create.ContentType if includeUnavailable || item.Enabled { items = append(items, item) } @@ -83,6 +85,13 @@ func (s *Service) PluginProviderCatalog(scope, capability string, includeUnavail return items } +func canonicalProviderAdapter(registry *protocol.Registry, id string) (protocol.Adapter, bool) { + if adapter, ok := protocol.Builtins().Resolve(id); ok { + return adapter, true + } + return registry.Resolve(id) +} + func containsPluginSurface(items []protocol.Surface, want protocol.Surface) bool { for _, item := range items { if item == want { @@ -114,6 +123,13 @@ func operationSummary(operation protocol.ManifestOperation) string { return strings.ToUpper(operation.Method) + " " + path } +func operationSummaryPtr(operation *protocol.ManifestOperation) string { + if operation == nil { + return "" + } + return operationSummary(*operation) +} + func (s *Service) protocolRegistry() *protocol.Registry { if s.pluginRuntime != nil { if registry := s.pluginRuntime.registrySnapshot(); registry != nil { diff --git a/backend/internal/service/protocol_runtime_test.go b/backend/internal/service/protocol_runtime_test.go index 474fce80b..b5d6010fb 100644 --- a/backend/internal/service/protocol_runtime_test.go +++ b/backend/internal/service/protocol_runtime_test.go @@ -35,6 +35,24 @@ func TestPluginViewIncludesDocumentationForEveryBundledProtocol(t *testing.T) { } } +func TestBundledProviderCatalogExposesUpstreamOperation(t *testing.T) { + center, err := newPluginRuntime(t.TempDir()) + if err != nil { + t.Fatal(err) + } + catalog := (&Service{pluginRuntime: center}).PluginProviderCatalog(string(protocol.SurfaceAdminSystemChannel), string(protocol.CapabilityVideo), false) + for _, item := range catalog { + if item.ID != "xai-video" { + continue + } + if item.Create != "POST /v1/videos/generations" || strings.Contains(item.Create, "__host__") { + t.Fatalf("xAI catalog create operation = %q", item.Create) + } + return + } + t.Fatal("xAI bundled provider missing from administrator catalog") +} + func TestPluginRuntimeIsTheProtocolSourceOfTruth(t *testing.T) { dataDir := t.TempDir() center, err := newPluginRuntime(dataDir) diff --git a/web/src/components/canvas/canvas-config-composer.tsx b/web/src/components/canvas/canvas-config-composer.tsx index 194766a25..b3cd4cb77 100644 --- a/web/src/components/canvas/canvas-config-composer.tsx +++ b/web/src/components/canvas/canvas-config-composer.tsx @@ -161,13 +161,13 @@ export function CanvasConfigComposer({ value, inputs, skillReferences = [], gene return (