mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-30 19:41:30 +02:00
Merge remote-tracking branch 'origin/main' into revert/component-types
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
This commit is contained in:
@@ -10,9 +10,88 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// bedrockRegionPrefixes are the cross-region inference-profile prefixes that
|
||||
// front a Bedrock model id (e.g. "eu.anthropic.claude-...").
|
||||
var bedrockRegionPrefixes = []string{"us.", "eu.", "apac.", "global."}
|
||||
// bedrockVendorNamespaces are the vendor segments a Bedrock model id is
|
||||
// published under. They identify the geography in front of a cross-region
|
||||
// inference profile without knowing the geography: in
|
||||
// "eu.anthropic.claude-...", what makes "eu" a geography is that "anthropic"
|
||||
// follows it.
|
||||
//
|
||||
// A vendor missing from here is not fatal — bedrockGeographies covers the
|
||||
// same id from the other side — but it is one of the two ways an id can go
|
||||
// unrecognised, and the list needs a new entry whenever AWS onboards a
|
||||
// vendor. A live listing found "global.xai.grok-4.6" days after this was
|
||||
// first written.
|
||||
var bedrockVendorNamespaces = map[string]struct{}{
|
||||
"ai21": {},
|
||||
"amazon": {},
|
||||
"anthropic": {},
|
||||
"cohere": {},
|
||||
"deepseek": {},
|
||||
"luma": {},
|
||||
"meta": {},
|
||||
"mistral": {},
|
||||
"openai": {},
|
||||
"qwen": {},
|
||||
"stability": {},
|
||||
"twelvelabs": {},
|
||||
"writer": {},
|
||||
"xai": {},
|
||||
}
|
||||
|
||||
// bedrockGeographies are the geography segments AWS issues cross-region
|
||||
// inference profiles under. They recognise a profile whose vendor we have
|
||||
// never seen, which is the case bedrockVendorNamespaces alone gets wrong:
|
||||
// "global.xai.grok-4.6" is a geography and a model whether or not "xai" is
|
||||
// a name we know.
|
||||
//
|
||||
// Neither list is sufficient alone. A geography list on its own is what this
|
||||
// file started with, and it aged badly — it held us, eu, apac and global, so
|
||||
// every profile issued under jp, au, ca, sa or us-gov carried its prefix into
|
||||
// the pricing key, matched no catalog entry, and reported the model unpriced.
|
||||
// A vendor list on its own misses a new vendor under a known geography.
|
||||
// Together, an id has to be new on both axes at once to go unrecognised.
|
||||
var bedrockGeographies = map[string]struct{}{
|
||||
"apac": {},
|
||||
"au": {},
|
||||
"ca": {},
|
||||
"eu": {},
|
||||
"global": {},
|
||||
"jp": {},
|
||||
"sa": {},
|
||||
"us": {},
|
||||
"us-gov": {},
|
||||
}
|
||||
|
||||
// stripBedrockGeography removes the cross-region inference-profile geography
|
||||
// from a Bedrock model id, leaving the "<vendor>.<model>" form the catalog and
|
||||
// the pricing table key on.
|
||||
//
|
||||
// A leading segment counts as a geography when it is one we know, or when a
|
||||
// known vendor follows it. Either alone is enough: the id has to be new on
|
||||
// both axes before its geography survives.
|
||||
//
|
||||
// The segment has to be followed by two more, so "amazon.nova-pro" stays a
|
||||
// vendor and a model rather than becoming a geography and a model — cutting
|
||||
// its first segment would strip the vendor away. Over-stripping is the
|
||||
// dangerous direction, because the result also decides which route may claim
|
||||
// a model.
|
||||
func stripBedrockGeography(modelID string) string {
|
||||
geo, rest, found := strings.Cut(modelID, ".")
|
||||
if !found || geo == "" {
|
||||
return modelID
|
||||
}
|
||||
vendor, _, found := strings.Cut(rest, ".")
|
||||
if !found {
|
||||
return modelID
|
||||
}
|
||||
if _, ok := bedrockGeographies[geo]; ok {
|
||||
return rest
|
||||
}
|
||||
if _, ok := bedrockVendorNamespaces[vendor]; ok {
|
||||
return rest
|
||||
}
|
||||
return modelID
|
||||
}
|
||||
|
||||
// bedrockVersionSuffix matches the trailing "-vN[:N]" or "-YYYYMMDD-vN[:N]"
|
||||
// version/throughput suffix of a Bedrock model id.
|
||||
@@ -37,15 +116,31 @@ func NormalizeBedrockModel(modelID string) string {
|
||||
m = m[i+1:]
|
||||
}
|
||||
}
|
||||
for _, p := range bedrockRegionPrefixes {
|
||||
if strings.HasPrefix(m, p) {
|
||||
m = m[len(p):]
|
||||
break
|
||||
}
|
||||
}
|
||||
m = stripBedrockGeography(m)
|
||||
return bedrockVersionSuffix.ReplaceAllString(m, "")
|
||||
}
|
||||
|
||||
// anthropicDatedModel matches a Claude model id carrying the trailing
|
||||
// "-YYYYMMDD" release-date suffix Anthropic appends to a pinned release,
|
||||
// capturing the id without it. The "claude" anchor is load-bearing: pricing
|
||||
// looks every model up through this helper regardless of surface, and an
|
||||
// operator may register a custom id with any shape at all, so an unanchored
|
||||
// "-\d{8}$" would let "internal-llm-20250101" silently inherit the rate
|
||||
// registered for "internal-llm". The anchor also covers the vendor-prefixed
|
||||
// forms ("anthropic.claude-...", "us.anthropic.claude-...").
|
||||
var anthropicDatedModel = regexp.MustCompile(`(?i)^(.*claude.*)-\d{8}$`)
|
||||
|
||||
// NormalizeAnthropicModel strips the trailing release-date suffix from a
|
||||
// Claude model id, e.g. "claude-sonnet-4-5-20250929" -> "claude-sonnet-4-5",
|
||||
// so a dated id a client pins matches the undated one the operator
|
||||
// registered. Ids that are not Claude-family are returned untouched.
|
||||
// Callers try the verbatim id first and fall back to this, so two dated
|
||||
// releases of the same family stay distinct wherever both are registered
|
||||
// explicitly.
|
||||
func NormalizeAnthropicModel(modelID string) string {
|
||||
return anthropicDatedModel.ReplaceAllString(modelID, "$1")
|
||||
}
|
||||
|
||||
// NormalizeVertexModel strips the "@version" suffix from a Vertex AI model id
|
||||
// (e.g. "claude-sonnet-4-5@20250929" -> "claude-sonnet-4-5") so it matches
|
||||
// the catalog/pricing key. Vertex publisher models are priced under their
|
||||
|
||||
@@ -34,3 +34,87 @@ func TestNormalizeVertexModel(t *testing.T) {
|
||||
require.Equal(t, want, NormalizeVertexModel(in), "normalize %q", in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAnthropicModel(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
|
||||
"claude-3-5-haiku-20241022": "claude-3-5-haiku",
|
||||
"claude-sonnet-5": "claude-sonnet-5",
|
||||
"claude-opus-4-8": "claude-opus-4-8",
|
||||
"anthropic.claude-haiku-4-5": "anthropic.claude-haiku-4-5",
|
||||
"anthropic.claude-sonnet-4-5-20250929": "anthropic.claude-sonnet-4-5",
|
||||
"us.anthropic.claude-opus-4-8-20250101": "us.anthropic.claude-opus-4-8",
|
||||
// Non-Claude ids must survive untouched even when they end in eight
|
||||
// consecutive digits: an operator can register a custom model under
|
||||
// any id, and pricing looks every one of them up through this helper.
|
||||
"gpt-4o": "gpt-4o",
|
||||
"gpt-4o-2024-08-06": "gpt-4o-2024-08-06",
|
||||
"gpt-4o-20240806": "gpt-4o-20240806",
|
||||
"internal-llm-20250101": "internal-llm-20250101",
|
||||
"deepseek-r1-20250120": "deepseek-r1-20250120",
|
||||
"Qwen/Qwen2.5-20250101": "Qwen/Qwen2.5-20250101",
|
||||
"gemini-2-5-pro-20250101": "gemini-2-5-pro-20250101",
|
||||
"": "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
require.Equal(t, want, NormalizeAnthropicModel(in), "normalize %q", in)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeBedrockModel_GeographiesBeyondTheOriginalFour covers the bug
|
||||
// that made this vendor-anchored: the geography used to be matched against a
|
||||
// list of four, so a profile issued anywhere else kept its prefix, missed the
|
||||
// catalog key it was supposed to match, and reported the model unpriced.
|
||||
func TestNormalizeBedrockModel_GeographiesBeyondTheOriginalFour(t *testing.T) {
|
||||
for _, geo := range []string{"us", "eu", "apac", "global", "jp", "au", "ca", "sa", "us-gov", "il", "mx"} {
|
||||
t.Run(geo, func(t *testing.T) {
|
||||
got := NormalizeBedrockModel(geo + ".anthropic.claude-sonnet-5-20260514-v1:0")
|
||||
require.Equal(t, "anthropic.claude-sonnet-5", got,
|
||||
"a cross-region profile must reduce to the catalog key whatever geography issued it")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeBedrockModel_KeepsAVendorItCannotMistakeForAGeography pins the
|
||||
// direction that must never break: a plain "<vendor>.<model>" id has no
|
||||
// geography, and cutting its first segment would strip the vendor away and
|
||||
// hand the id to whichever route claims the bare model name.
|
||||
func TestNormalizeBedrockModel_KeepsAVendorItCannotMistakeForAGeography(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"amazon.nova-pro-v1:0": "amazon.nova-pro",
|
||||
"anthropic.claude-sonnet-5-v1:0": "anthropic.claude-sonnet-5",
|
||||
"meta.llama3-3-70b-instruct-v1:0": "meta.llama3-3-70b-instruct",
|
||||
"cohere.command-r-plus-v1:0": "cohere.command-r-plus",
|
||||
// Unknown on both axes: neither the leading segment nor the one
|
||||
// after it is a name we hold, so the id is left exactly as it came.
|
||||
"xx.unknownvendor.some-model-v1:0": "xx.unknownvendor.some-model",
|
||||
"Qwen/Qwen2.5-0.5B-Instruct": "Qwen/Qwen2.5-0.5B-Instruct",
|
||||
}
|
||||
for in, want := range cases {
|
||||
t.Run(in, func(t *testing.T) {
|
||||
require.Equal(t, want, NormalizeBedrockModel(in))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeBedrockModel_RecognisesAnIdNewOnOneAxis covers what a live
|
||||
// eu-central-1 listing returned days after the vendor list was written:
|
||||
// "global.xai.grok-4.6", a vendor the list did not hold. Anchoring only on the
|
||||
// vendor left the geography in the key, so the id matched no catalog entry and
|
||||
// the model metered at zero. Each id below is unfamiliar on one axis and
|
||||
// recognised through the other.
|
||||
func TestNormalizeBedrockModel_RecognisesAnIdNewOnOneAxis(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
// Known geography, vendor we had never seen (the live case).
|
||||
"global.xai.grok-4.6": "xai.grok-4.6",
|
||||
"eu.xai.grok-4.6": "xai.grok-4.6",
|
||||
// Known vendor, geography outside the list.
|
||||
"il.anthropic.claude-sonnet-5-20260514-v1:0": "anthropic.claude-sonnet-5",
|
||||
"mx.amazon.nova-2-lite-v1:0": "amazon.nova-2-lite",
|
||||
}
|
||||
for in, want := range cases {
|
||||
t.Run(in, func(t *testing.T) {
|
||||
require.Equal(t, want, NormalizeBedrockModel(in))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,7 +591,7 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) {
|
||||
expectedFlowInfo := &mgmtProto.PKCEAuthorizationFlow{
|
||||
ProviderConfig: &mgmtProto.ProviderConfig{
|
||||
ClientID: "client",
|
||||
ClientSecret: "secret",
|
||||
ClientSecret: "secret", //nolint:staticcheck
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -5335,6 +5335,84 @@ components:
|
||||
- input_per_1k
|
||||
- output_per_1k
|
||||
- context_window
|
||||
AgentNetworkModelDiscoveryRequest:
|
||||
type: object
|
||||
properties:
|
||||
catalog_provider_id:
|
||||
type: string
|
||||
description: Catalog provider to query (AgentNetworkCatalogProvider.id). Determines the listing endpoint, the auth header and the response shape.
|
||||
example: "bedrock_api"
|
||||
upstream_url:
|
||||
type: string
|
||||
description: |
|
||||
The upstream being configured. Used to reach vendors that serve their listing from the same host as inference, and to read back the region for those whose host embeds one. Ignored when provider_id is supplied.
|
||||
example: "https://bedrock-runtime.eu-central-1.amazonaws.com"
|
||||
api_key:
|
||||
type: string
|
||||
description: Credential to query the vendor with, for a provider that has not been saved yet. Mutually exclusive with provider_id.
|
||||
example: "sk-..."
|
||||
provider_id:
|
||||
type: string
|
||||
description: Existing Agent Network provider record whose stored credential and upstream should be used. Lets the form refresh the list without the client holding the key.
|
||||
example: "ch8i4ug6lnn4g9hqv7m0"
|
||||
required:
|
||||
- catalog_provider_id
|
||||
AgentNetworkModelDiscoveryResponse:
|
||||
type: object
|
||||
properties:
|
||||
models:
|
||||
type: array
|
||||
description: Models the credential can reach, in the order the vendor returned them.
|
||||
items:
|
||||
$ref: '#/components/schemas/AgentNetworkDiscoveredModel'
|
||||
required:
|
||||
- models
|
||||
AgentNetworkDiscoveredModel:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: |
|
||||
Identifier to register on the provider record, in the form the vendor issues it. For Bedrock this is the region-prefixed inference-profile id, which is the only form AWS accepts at invoke time.
|
||||
example: "eu.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
label:
|
||||
type: string
|
||||
description: Vendor-supplied display name, where the vendor supplies one.
|
||||
example: "EU Anthropic Claude Haiku 4.5"
|
||||
pricing_known:
|
||||
type: boolean
|
||||
description: Whether NetBird's shipped pricing table can price this model. When false the rates below are all zero and the operator must set them, or requests to this model would record a cost of zero.
|
||||
example: true
|
||||
input_per_1k:
|
||||
type: number
|
||||
format: double
|
||||
description: Default input token price per 1k tokens, in USD, from the same table the proxy bills with. Zero when pricing_known is false.
|
||||
example: 0.005
|
||||
output_per_1k:
|
||||
type: number
|
||||
format: double
|
||||
description: Default output token price per 1k tokens, in USD. Zero when pricing_known is false.
|
||||
example: 0.015
|
||||
cached_input_per_1k:
|
||||
type: number
|
||||
format: double
|
||||
description: OpenAI-shape cache rate — default cost per 1k cached prompt tokens (a subset of input tokens), in USD. Absent when the model has no cached-input discount.
|
||||
example: 0.000075
|
||||
cache_read_per_1k:
|
||||
type: number
|
||||
format: double
|
||||
description: Anthropic-shape cache rate — default cost per 1k cache-read tokens (additive to input tokens), in USD. Absent when the model has no cache-read rate.
|
||||
example: 0.0003
|
||||
cache_creation_per_1k:
|
||||
type: number
|
||||
format: double
|
||||
description: Anthropic-shape cache rate — default cost per 1k cache-creation tokens (additive to input tokens), in USD. Absent when the model has no cache-creation rate.
|
||||
example: 0.00375
|
||||
required:
|
||||
- id
|
||||
- pricing_known
|
||||
- input_per_1k
|
||||
- output_per_1k
|
||||
AgentNetworkCatalogProvider:
|
||||
type: object
|
||||
properties:
|
||||
@@ -14004,6 +14082,42 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/agent-network/catalog/providers/models:
|
||||
post:
|
||||
summary: Discover the models a provider credential can reach
|
||||
description: |
|
||||
Asks the vendor which models the supplied credential can actually use, so the provider form can offer a live list instead of only the static catalog. The endpoint, auth header and response shape are taken from the catalog entry, never from the request.
|
||||
|
||||
Supply either an api_key together with the upstream_url being configured (before the provider is saved), or a provider_id of an existing record to reuse its stored credential.
|
||||
|
||||
Returns 422 for a catalog provider that has no listing endpoint (most gateways); the caller should fall back to the catalog's own model list. A model whose price the shipped table does not know is returned with pricing_known false, and the operator must set rates for it.
|
||||
tags: [ Agent Network ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentNetworkModelDiscoveryRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: The models the credential can reach
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentNetworkModelDiscoveryResponse'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'422':
|
||||
"$ref": "#/components/responses/validation_failed_simple"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/agent-network/providers:
|
||||
get:
|
||||
summary: List all Agent Network Providers
|
||||
|
||||
@@ -2120,6 +2120,33 @@ type AgentNetworkConsumption struct {
|
||||
// AgentNetworkConsumptionDimensionKind Whether this row counts a single end user or a single source group across every member.
|
||||
type AgentNetworkConsumptionDimensionKind string
|
||||
|
||||
// AgentNetworkDiscoveredModel defines model for AgentNetworkDiscoveredModel.
|
||||
type AgentNetworkDiscoveredModel struct {
|
||||
// CacheCreationPer1k Anthropic-shape cache rate — default cost per 1k cache-creation tokens (additive to input tokens), in USD. Absent when the model has no cache-creation rate.
|
||||
CacheCreationPer1k *float64 `json:"cache_creation_per_1k,omitempty"`
|
||||
|
||||
// CacheReadPer1k Anthropic-shape cache rate — default cost per 1k cache-read tokens (additive to input tokens), in USD. Absent when the model has no cache-read rate.
|
||||
CacheReadPer1k *float64 `json:"cache_read_per_1k,omitempty"`
|
||||
|
||||
// CachedInputPer1k OpenAI-shape cache rate — default cost per 1k cached prompt tokens (a subset of input tokens), in USD. Absent when the model has no cached-input discount.
|
||||
CachedInputPer1k *float64 `json:"cached_input_per_1k,omitempty"`
|
||||
|
||||
// Id Identifier to register on the provider record, in the form the vendor issues it. For Bedrock this is the region-prefixed inference-profile id, which is the only form AWS accepts at invoke time.
|
||||
Id string `json:"id"`
|
||||
|
||||
// InputPer1k Default input token price per 1k tokens, in USD, from the same table the proxy bills with. Zero when pricing_known is false.
|
||||
InputPer1k float64 `json:"input_per_1k"`
|
||||
|
||||
// Label Vendor-supplied display name, where the vendor supplies one.
|
||||
Label *string `json:"label,omitempty"`
|
||||
|
||||
// OutputPer1k Default output token price per 1k tokens, in USD. Zero when pricing_known is false.
|
||||
OutputPer1k float64 `json:"output_per_1k"`
|
||||
|
||||
// PricingKnown Whether NetBird's shipped pricing table can price this model. When false the rates below are all zero and the operator must set them, or requests to this model would record a cost of zero.
|
||||
PricingKnown bool `json:"pricing_known"`
|
||||
}
|
||||
|
||||
// AgentNetworkGuardrail defines model for AgentNetworkGuardrail.
|
||||
type AgentNetworkGuardrail struct {
|
||||
// Checks Guardrail check parameters. Each entry has an `enabled` flag plus per-check configuration; disabled entries are inert.
|
||||
@@ -2167,6 +2194,27 @@ type AgentNetworkGuardrailRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// AgentNetworkModelDiscoveryRequest defines model for AgentNetworkModelDiscoveryRequest.
|
||||
type AgentNetworkModelDiscoveryRequest struct {
|
||||
// ApiKey Credential to query the vendor with, for a provider that has not been saved yet. Mutually exclusive with provider_id.
|
||||
ApiKey *string `json:"api_key,omitempty"`
|
||||
|
||||
// CatalogProviderId Catalog provider to query (AgentNetworkCatalogProvider.id). Determines the listing endpoint, the auth header and the response shape.
|
||||
CatalogProviderId string `json:"catalog_provider_id"`
|
||||
|
||||
// ProviderId Existing Agent Network provider record whose stored credential and upstream should be used. Lets the form refresh the list without the client holding the key.
|
||||
ProviderId *string `json:"provider_id,omitempty"`
|
||||
|
||||
// UpstreamUrl The upstream being configured. Used to reach vendors that serve their listing from the same host as inference, and to read back the region for those whose host embeds one. Ignored when provider_id is supplied.
|
||||
UpstreamUrl *string `json:"upstream_url,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkModelDiscoveryResponse defines model for AgentNetworkModelDiscoveryResponse.
|
||||
type AgentNetworkModelDiscoveryResponse struct {
|
||||
// Models Models the credential can reach, in the order the vendor returned them.
|
||||
Models []AgentNetworkDiscoveredModel `json:"models"`
|
||||
}
|
||||
|
||||
// AgentNetworkPolicy defines model for AgentNetworkPolicy.
|
||||
type AgentNetworkPolicy struct {
|
||||
// CreatedAt Timestamp when the policy was created.
|
||||
@@ -6179,6 +6227,9 @@ type PostApiAgentNetworkBudgetRulesJSONRequestBody = AgentNetworkBudgetRuleReque
|
||||
// PutApiAgentNetworkBudgetRulesRuleIdJSONRequestBody defines body for PutApiAgentNetworkBudgetRulesRuleId for application/json ContentType.
|
||||
type PutApiAgentNetworkBudgetRulesRuleIdJSONRequestBody = AgentNetworkBudgetRuleRequest
|
||||
|
||||
// PostApiAgentNetworkCatalogProvidersModelsJSONRequestBody defines body for PostApiAgentNetworkCatalogProvidersModels for application/json ContentType.
|
||||
type PostApiAgentNetworkCatalogProvidersModelsJSONRequestBody = AgentNetworkModelDiscoveryRequest
|
||||
|
||||
// PostApiAgentNetworkGuardrailsJSONRequestBody defines body for PostApiAgentNetworkGuardrails for application/json ContentType.
|
||||
type PostApiAgentNetworkGuardrailsJSONRequestBody = AgentNetworkGuardrailRequest
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ func ToProtocolDNSConfig(update nbdns.Config, cache DNSConfigCache, forwardPort
|
||||
ServiceEnable: update.ServiceEnable,
|
||||
CustomZones: make([]*proto.CustomZone, 0, len(update.CustomZones)),
|
||||
NameServerGroups: make([]*proto.NameServerGroup, 0, len(update.NameServerGroups)),
|
||||
ForwarderPort: forwardPort,
|
||||
ForwarderPort: forwardPort, //nolint:staticcheck
|
||||
}
|
||||
|
||||
for _, zone := range update.CustomZones {
|
||||
|
||||
Reference in New Issue
Block a user