mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-24 16:41:30 +02:00
[proxy] Keep deliberately pinned dated models distinct
Two changes to routeClaimsModel, both about dated Anthropic ids.
Normalising the configured candidate as well as the requested model made
every dated build of a family interchangeable: a route registered against
claude-sonnet-4-5-20250101 also claimed ...-20250202, so an operator who
pinned a build deliberately would have served a different one, and with
several such routes declaration or path order decided which. Only an undated
registration now absorbs a dated request.
The per-model lookup also stamps the model its path names, so the guardrail's
allowlist — a separate and possibly narrower list than the route's — still
decides GET /v1/models/{id} rather than seeing no model at all.
This commit is contained in:
@@ -110,6 +110,9 @@ func (m *Middleware) MetadataKeys() []string {
|
||||
middleware.KeyLLMPolicyDecision,
|
||||
middleware.KeyLLMPolicyReason,
|
||||
middleware.KeyLLMNonInference,
|
||||
// Emitted only for the per-model lookup, whose model lives in the path
|
||||
// rather than a body the parser could read.
|
||||
middleware.KeyLLMModel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +184,14 @@ func (m *Middleware) Invoke(_ context.Context, in *middleware.Input) (*middlewar
|
||||
// the token pre-flight it would otherwise charge nothing against.
|
||||
if detail, isDetail := modelDetailID(reqPath); isDetail && isNonInferenceMethod(in.Method) {
|
||||
route, outcome := m.matchRoute(detail, surface, reqPath, in.UserGroups)
|
||||
return m.decide(route, outcome, surface, detail, in.UserGroups, markNonInference), nil
|
||||
return m.decide(route, outcome, surface, detail, in.UserGroups, func(out *middleware.Output) {
|
||||
markNonInference(out)
|
||||
// The parser reads models from JSON bodies only, and this request
|
||||
// has none, so stamp the one the path names. Without it the
|
||||
// guardrail's own allowlist — a separate, possibly narrower list
|
||||
// than the route's — never sees a model to check.
|
||||
out.Metadata = append(out.Metadata, middleware.KV{Key: middleware.KeyLLMModel, Value: detail})
|
||||
}), nil
|
||||
}
|
||||
|
||||
if model == "" {
|
||||
@@ -692,9 +702,13 @@ func routeClaimsModel(route ProviderRoute, model string) bool {
|
||||
return true
|
||||
}
|
||||
// A client may pin a dated Anthropic id ("claude-sonnet-4-5-20250929")
|
||||
// where the operator registered the undated one. Exact matches above
|
||||
// win, so two dated releases stay distinct when both are registered.
|
||||
if llm.NormalizeAnthropicModel(candidate) == llm.NormalizeAnthropicModel(model) {
|
||||
// where the operator registered the undated one. Only an undated
|
||||
// registration absorbs a dated request: normalising both sides would
|
||||
// let a route pinned to one dated release claim a different one, so an
|
||||
// operator who deliberately pinned a build would silently serve
|
||||
// another — and with several such routes, ordering would decide which.
|
||||
if candidate == llm.NormalizeAnthropicModel(candidate) &&
|
||||
candidate == llm.NormalizeAnthropicModel(model) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ func TestMiddlewareIdentity(t *testing.T) {
|
||||
middleware.KeyLLMResolvedProviderID,
|
||||
middleware.KeyLLMAuthorisingGroups,
|
||||
middleware.KeyLLMNonInference,
|
||||
middleware.KeyLLMModel,
|
||||
middleware.KeyLLMPolicyDecision,
|
||||
middleware.KeyLLMPolicyReason,
|
||||
},
|
||||
@@ -1087,3 +1088,60 @@ func TestRouter_NonInferenceRequiresReadMethod(t *testing.T) {
|
||||
"the HEAD warm probe carries no model to meter")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRouter_PinnedDatedModelStaysDistinct pins that a route registered
|
||||
// against one dated Anthropic release does not claim another. Normalising
|
||||
// both sides of the comparison made every dated build of a family
|
||||
// interchangeable, so an operator who deliberately pinned a build would have
|
||||
// served a different one — and with several such routes, declaration or path
|
||||
// order would have decided which.
|
||||
func TestRouter_PinnedDatedModelStaysDistinct(t *testing.T) {
|
||||
pinned := ProviderRoute{
|
||||
ID: "anthropic-pinned",
|
||||
Vendor: "anthropic",
|
||||
Models: []string{"claude-sonnet-4-5-20250101"},
|
||||
AllowedGroupIDs: []string{defaultTestGroup},
|
||||
UpstreamScheme: "https",
|
||||
UpstreamHost: "pinned.example.com",
|
||||
}
|
||||
|
||||
t.Run("a different dated release is not claimed", func(t *testing.T) {
|
||||
mw := New(Config{Providers: []ProviderRoute{pinned}})
|
||||
in := newInputWithModelAndURL("claude-sonnet-4-5-20250202", "/v1/messages")
|
||||
in.Metadata = append(in.Metadata, middleware.KV{Key: middleware.KeyLLMProvider, Value: "anthropic"})
|
||||
|
||||
out, err := mw.Invoke(context.Background(), in)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, middleware.DecisionDeny, out.Decision,
|
||||
"a route pinned to one dated build must not serve another")
|
||||
})
|
||||
|
||||
t.Run("its own dated release still routes", func(t *testing.T) {
|
||||
mw := New(Config{Providers: []ProviderRoute{pinned}})
|
||||
in := newInputWithModelAndURL("claude-sonnet-4-5-20250101", "/v1/messages")
|
||||
in.Metadata = append(in.Metadata, middleware.KV{Key: middleware.KeyLLMProvider, Value: "anthropic"})
|
||||
|
||||
out, err := mw.Invoke(context.Background(), in)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, middleware.DecisionAllow, out.Decision, "the exact match must still route")
|
||||
})
|
||||
|
||||
t.Run("two pinned builds each route to their own provider", func(t *testing.T) {
|
||||
other := pinned
|
||||
other.ID = "anthropic-pinned-newer"
|
||||
other.Models = []string{"claude-sonnet-4-5-20250202"}
|
||||
other.UpstreamHost = "newer.example.com"
|
||||
mw := New(Config{Providers: []ProviderRoute{pinned, other}})
|
||||
|
||||
in := newInputWithModelAndURL("claude-sonnet-4-5-20250202", "/v1/messages")
|
||||
in.Metadata = append(in.Metadata, middleware.KV{Key: middleware.KeyLLMProvider, Value: "anthropic"})
|
||||
|
||||
out, err := mw.Invoke(context.Background(), in)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, middleware.DecisionAllow, out.Decision)
|
||||
require.NotNil(t, out.Mutations)
|
||||
require.NotNil(t, out.Mutations.RewriteUpstream)
|
||||
assert.Equal(t, "newer.example.com", out.Mutations.RewriteUpstream.Host,
|
||||
"declaration order must not decide between two deliberately pinned builds")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user