diff --git a/README.md b/README.md index b1c509d..26b43ff 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,10 @@ print(response.choices[0].message.content) Supported capabilities depend on the selected model and can include streaming, tool calls, parallel tool calls, structured outputs and reasoning. The current capabilities for each model are shown in the -[model catalog](https://nexus.dappnode.com/models). +[model catalog](https://nexus.dappnode.com/models). They describe each model +and are not checked against chat requests: the selected provider validates chat +options, so a stale catalog flag cannot reject a request the provider would +accept. Endpoint routing, privacy policy and credit checks still apply. ## Privacy and usage controls diff --git a/apps/gateway/internal/application/services/capabilities_test.go b/apps/gateway/internal/application/services/capabilities_test.go new file mode 100644 index 0000000..661f7ad --- /dev/null +++ b/apps/gateway/internal/application/services/capabilities_test.go @@ -0,0 +1,24 @@ +package services + +import ( + "testing" + + "github.com/dappnode/dappnode-nexus-gateway/pkg/domain" +) + +func TestStaleCapabilityFlagsDoNotRejectChatOptions(t *testing.T) { + parallel := true + model := domain.PublicModel{PublicModelID: "fixture", SupportsChatCompletions: true} + req := domain.GenerateRequest{Stream: true, Tools: []domain.ToolDefinition{{}}, ParallelToolCalls: ¶llel, StructuredOutput: true} + if err := (&GenerateService{}).validateRequest(domain.EndpointChatCompletions, req, model); err != nil { + t.Fatal(err) + } +} + +func TestSystemOneStillRejectsStreaming(t *testing.T) { + model := domain.PublicModel{PublicModelID: "jev", ProviderConfig: domain.ProviderConfig{ProviderName: "typesafe"}} + err := (&GenerateService{}).validateRequest(domain.EndpointSystemOne, domain.GenerateRequest{Stream: true}, model) + if gwErr, ok := err.(*domain.GatewayError); !ok || gwErr.Code != domain.ErrCodeUnsupportedFeature { + t.Fatalf("SystemOne stream = %v", err) + } +} diff --git a/apps/gateway/internal/application/services/generate_service.go b/apps/gateway/internal/application/services/generate_service.go index 6be0ca9..53dd598 100644 --- a/apps/gateway/internal/application/services/generate_service.go +++ b/apps/gateway/internal/application/services/generate_service.go @@ -187,22 +187,14 @@ func (s *GenerateService) validateRequest(endpoint string, req domain.GenerateRe return domain.ErrUnsupportedEndpoint(model.PublicModelID, endpoint) } - if req.Stream && !model.SupportsStreamForEndpoint(endpoint) { + // Catalog capability flags describe the catalog; they are not a request + // schema. Chat options go to the provider unchanged and the provider + // decides, so a stale flag cannot reject a request the provider accepts. + // SystemOne has no streaming wire protocol in Nexus. + if endpoint == domain.EndpointSystemOne && req.Stream { return domain.ErrUnsupportedFeature("streaming for " + endpoint) } - if len(req.Tools) > 0 && !model.SupportsTools { - return domain.ErrUnsupportedFeature("tools") - } - - if req.ParallelToolCalls != nil && *req.ParallelToolCalls && !model.SupportsParallelToolCalls { - return domain.ErrUnsupportedFeature("parallel_tool_calls") - } - - if req.StructuredOutput && !model.SupportsStructuredOutput { - return domain.ErrUnsupportedFeature("structured_output") - } - if strings.EqualFold(model.ProviderConfig.ProviderName, "doubleword") && req.ServiceTier != nil { return domain.ErrInvalidField("service_tier is fixed by the selected Doubleword model") } diff --git a/pkg/domain/domain_test.go b/pkg/domain/domain_test.go index 1479622..99db7b7 100644 --- a/pkg/domain/domain_test.go +++ b/pkg/domain/domain_test.go @@ -77,16 +77,3 @@ func TestPublicModelSupportsEndpoint(t *testing.T) { t.Error("should not support unknown") } } - -func TestPublicModelSupportsStreamForEndpoint(t *testing.T) { - m := domain.PublicModel{ - SupportsChatCompletionsStream: true, - } - if !m.SupportsStreamForEndpoint(domain.EndpointChatCompletions) { - t.Error("should support chat_completions stream") - } - m.SupportsChatCompletionsStream = false - if m.SupportsStreamForEndpoint(domain.EndpointChatCompletions) { - t.Error("should not support chat_completions stream when disabled") - } -} diff --git a/pkg/domain/model.go b/pkg/domain/model.go index 2b079b1..6600a77 100644 --- a/pkg/domain/model.go +++ b/pkg/domain/model.go @@ -78,16 +78,3 @@ func (m PublicModel) SupportsEndpoint(endpoint string) bool { return false } } - -// SupportsStreamForEndpoint checks if the model supports streaming for the given endpoint. -func (m PublicModel) SupportsStreamForEndpoint(endpoint string) bool { - if m.ProviderConfig.ProviderName == "typesafe" { - return false - } - switch endpoint { - case EndpointChatCompletions: - return m.SupportsChatCompletionsStream - default: - return false - } -}