[management,proxy] Serve guardrail allowlists of declared model ids (#7389)

After #7221, guardrail allowlists built from a path-style provider's declared
model ids (Bedrock, Vertex) stopped working: the raw region/version form was
compared against the parser's canonical id, so the agent config advertised an
empty model list and requests for the allowlisted model were refused.

Make every allowlist compare provider-aware, keyed on the destination
provider's catalog id: the agent config, the policy gate, and the synthesized
proxy allowlists match an entry on both its verbatim and canonical form —
Bedrock's strip only under bedrock_api, Vertex's only under vertex_ai_api,
verbatim everywhere else, so a plain provider's suffixed entries never widen.
The router's claim compare learns the Vertex @version strip.

New e2e, realstore, and unit tests reproduce both regressions and pin the fix.
This commit is contained in:
Maycon Santos
2026-09-02 18:57:02 +02:00
committed by GitHub
parent 8a5e940c84
commit 26e5495e5d
11 changed files with 785 additions and 55 deletions

View File

@@ -115,3 +115,20 @@ func TestRouter_BedrockNamespacedInferenceProfilesStripsPrefix(t *testing.T) {
assert.Equal(t, "/bedrock", out.Mutations.RewriteUpstream.StripPathPrefix,
"the namespace prefix must not reach the real Bedrock endpoint")
}
// TestRouteClaimsModel_VertexNormalizesCandidate is the Vertex counterpart of
// the Bedrock case above: the parser strips the "@version" suffix from the
// path model, so a provider registered with the versioned form must still
// match the normalized request model.
func TestRouteClaimsModel_VertexNormalizesCandidate(t *testing.T) {
route := ProviderRoute{Vertex: true, Models: []string{"claude-sonnet-4-5@20250929"}}
assert.True(t, routeClaimsModel(route, "claude-sonnet-4-5"),
"raw @version Vertex model must match the normalized request model")
assert.False(t, routeClaimsModel(route, "claude-opus-4-8"),
"a model outside the provider's list must not match")
// Non-Vertex routes keep exact matching (no @version stripping).
openai := ProviderRoute{Models: []string{"gpt-4o@2024"}}
assert.False(t, routeClaimsModel(openai, "gpt-4o"),
"non-Vertex routes must not strip an @version suffix")
}

View File

@@ -331,6 +331,11 @@ func discoverableModels(route ProviderRoute, userGroups []string) ([]string, boo
intersection[m] = struct{}{}
}
}
if route.Vertex {
if _, ok := permitted[llm.NormalizeVertexModel(m)]; ok {
intersection[m] = struct{}{}
}
}
}
return sortedModels(intersection), true
}
@@ -869,6 +874,11 @@ func routeClaimsModel(route ProviderRoute, model string) bool {
if route.Bedrock && llm.NormalizeBedrockModel(candidate) == model {
return true
}
// Vertex likewise: the parser strips the "@version" suffix from the
// path model, while the operator may register the versioned form.
if route.Vertex && llm.NormalizeVertexModel(candidate) == model {
return true
}
// A client may pin a dated Anthropic id ("claude-sonnet-4-5-20250929")
// where the operator registered the undated one. Only an undated
// registration absorbs a dated request: normalising both sides would