mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-30 18:49:08 +02:00
[management,proxy] Add agentgateway integration (#7274)
* [management] Add agentgateway provider catalog entry Allow Agent Network providers to target an operator-supplied agentgateway proxy while stamping trusted NetBird identity headers. Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io> * [proxy] Allow trusted Agent Network identity headers Permit only the built-in identity injector to replace the two reserved agentgateway attribution headers while keeping them blocked for every other middleware. Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io> * [management,proxy] Add multi-vendor gateway routing Let one Agent Network route declare multiple parser surfaces while preserving the existing singular vendor wire field. Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io> * [management] Update router test for model policies Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io> * [proxy] Cover reserved header policy Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io> * [management] Add agentgateway model discovery Use agentgateway's OpenAI-compatible models endpoint and omit wildcard patterns until NetBird can authorize and price them consistently. Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io> --------- Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>
This commit is contained in:
@@ -81,6 +81,10 @@ type Provider struct {
|
||||
// surface — the proxy middleware then falls back to URL sniffing
|
||||
// or skips request-side enrichment.
|
||||
ParserID string
|
||||
// RouterVendors declares every parser surface a gateway route can serve.
|
||||
// Leave empty for single-surface providers, where ParserID remains the
|
||||
// router discriminator for backward compatibility.
|
||||
RouterVendors []string
|
||||
// PricingSurfaces names the cost-meter pricing surfaces this
|
||||
// provider's Models are priced under ("openai", "anthropic",
|
||||
// "bedrock" — the llm.Parser surface the request parser stamps as
|
||||
@@ -116,8 +120,7 @@ type Provider struct {
|
||||
// Discovery, when non-nil, describes how to ask this vendor which
|
||||
// models the operator's own credential can actually reach, so the
|
||||
// provider form can offer a live list instead of only the hand-curated
|
||||
// Models above. Nil for entries with no listing endpoint (gateways
|
||||
// vary too much) — those keep free-text entry.
|
||||
// Models above. Nil entries keep free-text entry.
|
||||
Discovery *Discovery
|
||||
}
|
||||
|
||||
@@ -154,10 +157,13 @@ const (
|
||||
// one from the caller is also what keeps this from being an open proxy: the
|
||||
// only hosts management will dial are the ones written here.
|
||||
type Discovery struct {
|
||||
Host string
|
||||
Path string
|
||||
Query string
|
||||
Shape ListingShape
|
||||
Host string
|
||||
Path string
|
||||
Query string
|
||||
Shape ListingShape
|
||||
// ExactModelsOnly omits wildcard patterns from listings when NetBird's
|
||||
// provider model rows cannot represent the vendor's matching semantics.
|
||||
ExactModelsOnly bool
|
||||
// Headers are static headers the vendor requires beyond the credential
|
||||
// (Anthropic versions its API through one and rejects a request without
|
||||
// it). The auth header itself comes from AuthHeaderName/Template.
|
||||
@@ -635,6 +641,34 @@ var providers = []Provider{
|
||||
},
|
||||
Models: []Model{},
|
||||
},
|
||||
{
|
||||
ID: "agentgateway",
|
||||
Kind: KindGateway,
|
||||
Name: "agentgateway",
|
||||
Description: "Bring your own agentgateway with trusted NetBird identity stamped on every request",
|
||||
DefaultHost: "",
|
||||
AuthHeaderName: "Authorization",
|
||||
AuthHeaderTemplate: "Bearer ${API_KEY}",
|
||||
DefaultContentType: "application/json",
|
||||
BrandColor: "#8023C3",
|
||||
// Agentgateway accepts both OpenAI and Anthropic request shapes.
|
||||
// Leave ParserID empty so the proxy detects the shape from the URL.
|
||||
ParserID: "",
|
||||
RouterVendors: []string{"openai", "anthropic"},
|
||||
PricingSurfaces: []string{"openai", "anthropic"},
|
||||
Discovery: &Discovery{
|
||||
Path: "/v1/models",
|
||||
Shape: ShapeOpenAIData,
|
||||
ExactModelsOnly: true,
|
||||
},
|
||||
IdentityInjection: &IdentityInjection{
|
||||
HeaderPair: &HeaderPairInjection{
|
||||
EndUserIDHeader: "x-netbird-user-id",
|
||||
TagsHeader: "x-netbird-groups",
|
||||
},
|
||||
},
|
||||
Models: []Model{},
|
||||
},
|
||||
{
|
||||
ID: "portkey",
|
||||
Kind: KindGateway,
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
// TestClaudeLineupSelectable pins the models Claude Code resolves to by
|
||||
@@ -34,3 +36,51 @@ func TestClaudeLineupSelectable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentgatewayCatalogEntry(t *testing.T) {
|
||||
entry, ok := Lookup("agentgateway")
|
||||
require.True(t, ok, "agentgateway must be available in the provider catalog")
|
||||
|
||||
assert.Equal(t, KindGateway, entry.Kind, "agentgateway must be grouped with AI gateways")
|
||||
assert.Empty(t, entry.DefaultHost, "operators must provide their agentgateway proxy URL")
|
||||
assert.Equal(t, "Authorization", entry.AuthHeaderName)
|
||||
assert.Equal(t, "Bearer ${API_KEY}", entry.AuthHeaderTemplate)
|
||||
assert.Equal(t, "application/json", entry.DefaultContentType)
|
||||
assert.Empty(t, entry.ParserID, "URL detection must select the OpenAI or Anthropic parser")
|
||||
assert.Equal(t, []string{"openai", "anthropic"}, entry.RouterVendors,
|
||||
"agentgateway must accept both parser surfaces")
|
||||
assert.Equal(t, []string{"openai", "anthropic"}, entry.PricingSurfaces,
|
||||
"agentgateway models can use either pricing surface")
|
||||
assert.Empty(t, entry.Models, "an empty model list makes agentgateway a catch-all route")
|
||||
require.NotNil(t, entry.Discovery)
|
||||
assert.Empty(t, entry.Discovery.Host, "discovery must use the configured proxy URL")
|
||||
assert.Equal(t, "/v1/models", entry.Discovery.Path)
|
||||
assert.Equal(t, ShapeOpenAIData, entry.Discovery.Shape)
|
||||
assert.True(t, entry.Discovery.ExactModelsOnly,
|
||||
"wildcard model semantics are not supported by NetBird")
|
||||
|
||||
require.NotNil(t, entry.IdentityInjection)
|
||||
require.NotNil(t, entry.IdentityInjection.HeaderPair)
|
||||
assert.Nil(t, entry.IdentityInjection.JSONMetadata)
|
||||
assert.False(t, entry.IdentityInjection.HeaderPair.Customizable,
|
||||
"NetBird identity header names are part of the integration contract")
|
||||
assert.Equal(t, "x-netbird-user-id", entry.IdentityInjection.HeaderPair.EndUserIDHeader)
|
||||
assert.Equal(t, "x-netbird-groups", entry.IdentityInjection.HeaderPair.TagsHeader)
|
||||
assert.False(t, entry.IdentityInjection.HeaderPair.EndUserIDInBody)
|
||||
assert.False(t, entry.IdentityInjection.HeaderPair.TagsInBody)
|
||||
}
|
||||
|
||||
func TestAgentgatewayCatalogAPIResponse(t *testing.T) {
|
||||
entry, ok := Lookup("agentgateway")
|
||||
require.True(t, ok)
|
||||
|
||||
resp := entry.ToAPIResponse()
|
||||
assert.Equal(t, "agentgateway", resp.Id)
|
||||
assert.Equal(t, api.AgentNetworkCatalogProviderKindGateway, resp.Kind)
|
||||
assert.Empty(t, resp.Models)
|
||||
require.NotNil(t, resp.IdentityInjection)
|
||||
require.NotNil(t, resp.IdentityInjection.HeaderPair)
|
||||
assert.False(t, resp.IdentityInjection.HeaderPair.Customizable)
|
||||
assert.Equal(t, "x-netbird-user-id", resp.IdentityInjection.HeaderPair.EndUserIdHeader)
|
||||
assert.Equal(t, "x-netbird-groups", resp.IdentityInjection.HeaderPair.TagsHeader)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user