Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions apps/gateway/internal/application/services/capabilities_test.go
Original file line number Diff line number Diff line change
@@ -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: &parallel, 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)
}
}
18 changes: 5 additions & 13 deletions apps/gateway/internal/application/services/generate_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
13 changes: 0 additions & 13 deletions pkg/domain/domain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
13 changes: 0 additions & 13 deletions pkg/domain/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading