From 5beb773b39d30c166d7f2140bfc2f0256671b974 Mon Sep 17 00:00:00 2001 From: thivindu Date: Mon, 7 Sep 2026 10:42:15 +0530 Subject: [PATCH 1/2] Validate policy params for LLM operationPolicies and the policies list --- .../pkg/config/policy_validator.go | 52 ++++- .../pkg/config/policy_validator_llm_test.go | 188 ++++++++++++++++++ 2 files changed, 232 insertions(+), 8 deletions(-) diff --git a/gateway/gateway-controller/pkg/config/policy_validator.go b/gateway/gateway-controller/pkg/config/policy_validator.go index 99ce35bb0d..c9cff74907 100644 --- a/gateway/gateway-controller/pkg/config/policy_validator.go +++ b/gateway/gateway-controller/pkg/config/policy_validator.go @@ -166,36 +166,72 @@ func (pv *PolicyValidator) ValidateLLMProxyPolicies(cfg *api.LLMProxyConfigurati // validateLLMPolicyRefs validates the three policy collections shared by LLM providers and // proxies: api-level (global) policies, operation-level policies, and the deprecated policies -// list. An empty version resolves to the latest available version (handled by ResolvePolicyVersion). +// list. Every collection gets its name/version reference resolved and its params validated +// against the definition's declared parameter schema. An empty version resolves to the latest +// available version (handled by ResolvePolicyVersion). func (pv *PolicyValidator) validateLLMPolicyRefs(globalPolicies *[]api.Policy, operationPolicies *[]api.OperationPolicy, legacyPolicies *[]api.LLMPolicy) []ValidationError { var errors []ValidationError - // Global (api-level) policies carry params, so reuse validatePolicy to also validate them. + // Global (api-level) policies carry params on the policy itself, so reuse validatePolicy. if globalPolicies != nil { for i, policy := range *globalPolicies { errors = append(errors, pv.validatePolicy(policy, fmt.Sprintf("spec.globalPolicies[%d]", i))...) } } - // Operation-level policies: validate name + version existence. + // Operation-level policies: name + version existence, then each path's params. if operationPolicies != nil { for i, policy := range *operationPolicies { - _, errs := pv.validatePolicyRef(policy.Name, policy.Version, fmt.Sprintf("spec.operationPolicies[%d]", i)) - errors = append(errors, errs...) + fieldPath := fmt.Sprintf("spec.operationPolicies[%d]", i) + policyDef, errs := pv.validatePolicyRef(policy.Name, policy.Version, fieldPath) + if len(errs) > 0 { + errors = append(errors, errs...) + continue + } + for j := range policy.Paths { + errors = append(errors, pv.validateAttachedPolicyParams(policyDef, policy.Paths[j].Params, + fmt.Sprintf("%s.paths[%d]", fieldPath, j))...) + } } } - // Deprecated policies list (still honoured): validate name + version existence. + // Deprecated policies list (still honoured): same as operation-level policies. if legacyPolicies != nil { for i, policy := range *legacyPolicies { - _, errs := pv.validatePolicyRef(policy.Name, policy.Version, fmt.Sprintf("spec.policies[%d]", i)) - errors = append(errors, errs...) + fieldPath := fmt.Sprintf("spec.policies[%d]", i) + policyDef, errs := pv.validatePolicyRef(policy.Name, policy.Version, fieldPath) + if len(errs) > 0 { + errors = append(errors, errs...) + continue + } + for j := range policy.Paths { + errors = append(errors, pv.validateAttachedPolicyParams(policyDef, policy.Paths[j].Params, + fmt.Sprintf("%s.paths[%d]", fieldPath, j))...) + } } } return errors } +// validateAttachedPolicyParams validates one per-path params map from an LLM operation-level +// or deprecated policy attachment against the resolved definition's parameter schema. Params +// are coerced first, since template rendering always yields strings ({{ env "X" }} -> "100" +// even for an integer param) — mirroring validatePolicy's handling of api-level params. +func (pv *PolicyValidator) validateAttachedPolicyParams(policyDef *models.PolicyDefinition, params map[string]interface{}, fieldPath string) []ValidationError { + if policyDef == nil || policyDef.Parameters == nil { + return nil + } + if params == nil { + // A missing params map still has to be validated: the schema may declare + // required properties, and an empty object must fail the same way. + params = map[string]interface{}{} + } else { + coerceParamsBySchema(params, *policyDef.Parameters) + } + return pv.validatePolicyParams(params, *policyDef.Parameters, fieldPath+".params") +} + // validatePolicy validates a single policy reference (name + version existence) and, when the // definition declares a parameter schema, the policy's params against that schema. func (pv *PolicyValidator) validatePolicy(policy api.Policy, fieldPath string) []ValidationError { diff --git a/gateway/gateway-controller/pkg/config/policy_validator_llm_test.go b/gateway/gateway-controller/pkg/config/policy_validator_llm_test.go index 21fe50ae48..ff4362d0d8 100644 --- a/gateway/gateway-controller/pkg/config/policy_validator_llm_test.go +++ b/gateway/gateway-controller/pkg/config/policy_validator_llm_test.go @@ -156,3 +156,191 @@ func TestPolicyValidator_ValidateLLMProxyPolicies_NonExistentMajorVersion(t *tes assert.Len(t, errors, 1, "expected one error for a non-existent major version") assert.Contains(t, errors[0].Message, "major version 'v999' not found") } + +// paramDefs returns definitions whose "token-based-ratelimit" policy declares a parameter +// schema, so per-path params on operation-level and deprecated policies can be exercised. +// additionalProperties:false mirrors the shipped policy definitions. +func paramDefs() map[string]models.PolicyDefinition { + schema := map[string]interface{}{ + "type": "object", + "additionalProperties": false, + "required": []interface{}{"limit"}, + "properties": map[string]interface{}{ + "limit": map[string]interface{}{"type": "integer", "minimum": float64(1)}, + "duration": map[string]interface{}{"type": "string"}, + }, + } + return map[string]models.PolicyDefinition{ + "token-based-ratelimit|v1.0.0": {Name: "token-based-ratelimit", Version: "v1.0.0", Parameters: &schema}, + "no-schema-policy|v1.0.0": {Name: "no-schema-policy", Version: "v1.0.0"}, + } +} + +func TestPolicyValidator_ValidateLLMProviderPolicies_OperationPolicyParamsValid(t *testing.T) { + validator := NewPolicyValidator(paramDefs()) + + cfg := &api.LLMProviderConfiguration{ + Spec: api.LLMProviderConfigData{ + OperationPolicies: &[]api.OperationPolicy{ + {Name: "token-based-ratelimit", Version: "v1", Paths: []api.OperationPolicyPath{ + {Path: "/chat/completions", Params: map[string]interface{}{"limit": 100, "duration": "1m"}}, + }}, + }, + }, + } + + assert.Empty(t, validator.ValidateLLMProviderPolicies(cfg)) +} + +func TestPolicyValidator_ValidateLLMProviderPolicies_OperationPolicyParamsInvalid(t *testing.T) { + validator := NewPolicyValidator(paramDefs()) + + cfg := &api.LLMProviderConfiguration{ + Spec: api.LLMProviderConfigData{ + OperationPolicies: &[]api.OperationPolicy{ + {Name: "token-based-ratelimit", Version: "v1", Paths: []api.OperationPolicyPath{ + {Path: "/chat/completions", Params: map[string]interface{}{"limit": 100}}, + {Path: "/embeddings", Params: map[string]interface{}{"duration": "1m"}}, + {Path: "/responses", Params: map[string]interface{}{"limit": 0}}, + {Path: "/models", Params: map[string]interface{}{"limit": 1, "bogus": "x"}}, + }}, + }, + }, + } + + errors := validator.ValidateLLMProviderPolicies(cfg) + assert.Len(t, errors, 3, "expected one error each for the missing, out-of-range and unknown param") + + fields := make([]string, 0, len(errors)) + for _, e := range errors { + fields = append(fields, e.Field) + } + assert.NotContains(t, fields, "spec.operationPolicies[0].paths[0].params", + "paths[0] is valid and must not be reported") + assert.Contains(t, fields, "spec.operationPolicies[0].paths[1].params") + assert.Contains(t, errors[0].Message, "limit is required") + assert.Equal(t, "spec.operationPolicies[0].paths[2].params.limit", errors[1].Field) + assert.Contains(t, errors[2].Message, "Additional property bogus is not allowed") +} + +func TestPolicyValidator_ValidateLLMProviderPolicies_OperationPolicyMissingParamsFailsRequired(t *testing.T) { + validator := NewPolicyValidator(paramDefs()) + + cfg := &api.LLMProviderConfiguration{ + Spec: api.LLMProviderConfigData{ + OperationPolicies: &[]api.OperationPolicy{ + {Name: "token-based-ratelimit", Version: "v1", Paths: []api.OperationPolicyPath{ + {Path: "/chat/completions"}, // no params at all + }}, + }, + }, + } + + errors := validator.ValidateLLMProviderPolicies(cfg) + assert.Len(t, errors, 1) + assert.Equal(t, "spec.operationPolicies[0].paths[0].params", errors[0].Field) + assert.Contains(t, errors[0].Message, "limit is required") +} + +func TestPolicyValidator_ValidateLLMProviderPolicies_OperationPolicyParamsCoerced(t *testing.T) { + validator := NewPolicyValidator(paramDefs()) + + // A rendered template ({{ env "LIMIT" }}) always produces a string; coercion must run + // before schema validation so "100" satisfies the integer param. + params := map[string]interface{}{"limit": "100", "duration": "1m"} + cfg := &api.LLMProviderConfiguration{ + Spec: api.LLMProviderConfigData{ + OperationPolicies: &[]api.OperationPolicy{ + {Name: "token-based-ratelimit", Version: "v1", Paths: []api.OperationPolicyPath{ + {Path: "/chat/completions", Params: params}, + }}, + }, + }, + } + + assert.Empty(t, validator.ValidateLLMProviderPolicies(cfg)) + assert.Equal(t, float64(100), params["limit"], "params must be coerced in place") +} + +func TestPolicyValidator_ValidateLLMProviderPolicies_OperationPolicyNoSchemaSkipsParams(t *testing.T) { + validator := NewPolicyValidator(paramDefs()) + + cfg := &api.LLMProviderConfiguration{ + Spec: api.LLMProviderConfigData{ + OperationPolicies: &[]api.OperationPolicy{ + {Name: "no-schema-policy", Version: "v1", Paths: []api.OperationPolicyPath{ + {Path: "/chat/completions", Params: map[string]interface{}{"anything": "goes"}}, + }}, + }, + }, + } + + assert.Empty(t, validator.ValidateLLMProviderPolicies(cfg), + "a definition without a parameter schema must not reject params") +} + +func TestPolicyValidator_ValidateLLMProviderPolicies_BadRefSkipsParamValidation(t *testing.T) { + validator := NewPolicyValidator(paramDefs()) + + cfg := &api.LLMProviderConfiguration{ + Spec: api.LLMProviderConfigData{ + OperationPolicies: &[]api.OperationPolicy{ + {Name: "token-based-ratelimit", Version: "v999", Paths: []api.OperationPolicyPath{ + {Path: "/chat/completions", Params: map[string]interface{}{"bogus": "x"}}, + }}, + }, + }, + } + + errors := validator.ValidateLLMProviderPolicies(cfg) + assert.Len(t, errors, 1, "an unresolvable reference must report once, not also per path") + assert.Contains(t, errors[0].Message, "major version 'v999' not found") +} + +func TestPolicyValidator_ValidateLLMProxyPolicies_LegacyPolicyParamsInvalid(t *testing.T) { + validator := NewPolicyValidator(paramDefs()) + + cfg := &api.LLMProxyConfiguration{ + Spec: api.LLMProxyConfigData{ + Policies: &[]api.LLMPolicy{ + {Name: "token-based-ratelimit", Version: "v1", Paths: []api.LLMPolicyPath{ + {Path: "/chat/completions", Params: map[string]interface{}{"limit": 100}}, + {Path: "/embeddings", Params: map[string]interface{}{"limit": "not-a-number"}}, + }}, + }, + }, + } + + errors := validator.ValidateLLMProxyPolicies(cfg) + assert.Len(t, errors, 1) + assert.Equal(t, "spec.policies[0].paths[1].params.limit", errors[0].Field) +} + +// The LLM->RestAPI transform merges the provider template's extraction params +// (requestModel, promptTokens, ...) into every operation-level policy attachment. Those keys +// are declared by no policy schema, and most schemas set additionalProperties:false — so +// validation must run against the user-authored params, never the post-merge result. +func TestPolicyValidator_ValidateLLMProviderPolicies_TemplateExtractionParamsNotRequired(t *testing.T) { + validator := NewPolicyValidator(paramDefs()) + + cfg := &api.LLMProviderConfiguration{ + Spec: api.LLMProviderConfigData{ + OperationPolicies: &[]api.OperationPolicy{ + {Name: "token-based-ratelimit", Version: "v1", Paths: []api.OperationPolicyPath{ + {Path: "/chat/completions", Params: map[string]interface{}{"limit": 100}}, + }}, + }, + }, + } + assert.Empty(t, validator.ValidateLLMProviderPolicies(cfg), + "user-authored params alone must validate; template params are merged later") + + // Sanity check that the merged shape would indeed be rejected, which is why the + // derived RestAPI is deliberately not the validation input. + merged := map[string]interface{}{ + "limit": 100, + "requestModel": map[string]interface{}{"location": "payload", "identifier": "$.model"}, + } + def := paramDefs()["token-based-ratelimit|v1.0.0"] + assert.NotEmpty(t, validator.validatePolicyParams(merged, *def.Parameters, "p")) +} From 937964684e6477ecf13116d5c2f0e70d6623c834 Mon Sep 17 00:00:00 2001 From: thivindu Date: Fri, 18 Sep 2026 13:42:39 +0530 Subject: [PATCH 2/2] Disallow overriding policy system params in individual policies --- .../it/features/token-based-ratelimit.feature | 36 ------------------- .../it/features/token_based_ratelimit.feature | 34 +++++++++--------- ...based_ratelimit_provider_templates.feature | 2 +- 3 files changed, 18 insertions(+), 54 deletions(-) diff --git a/gateway/it/features/token-based-ratelimit.feature b/gateway/it/features/token-based-ratelimit.feature index 132e61ca40..00f82073f2 100644 --- a/gateway/it/features/token-based-ratelimit.feature +++ b/gateway/it/features/token-based-ratelimit.feature @@ -94,8 +94,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 20 duration: "1m" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And the JSON response field "status" should be "success" @@ -229,8 +227,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 15 duration: "1m" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/multi-quota/chat/completions" to be ready @@ -337,8 +333,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 2 duration: "1m" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/gzip-response/chat/completions" to be ready @@ -438,8 +432,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 100 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/headers-ratelimit/chat/completions" to be ready @@ -555,8 +547,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 5 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 @@ -593,8 +583,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 5 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/provider-a/chat/completions" to be ready @@ -736,8 +724,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 25 duration: "1m" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/multi-quota-detailed/chat/completions" to be ready @@ -862,8 +848,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 5 duration: "10s" # Short window for testing - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/window-test/chat/completions" to be ready @@ -966,8 +950,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 10 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/zero-token-test/chat/completions" to be ready @@ -1076,8 +1058,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 10 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/header-cost-test/chat/completions" to be ready @@ -1166,8 +1146,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 5 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/change-test/chat/completions" to be ready @@ -1230,8 +1208,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 10 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/change-test/chat/completions" to be ready @@ -1310,8 +1286,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 5 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 @@ -1349,8 +1323,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 5 duration: "1h" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/provider-alpha/chat/completions" to be ready @@ -1456,8 +1428,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 5 duration: "1m" - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/empty-limits/chat/completions" to be ready @@ -1566,8 +1536,6 @@ Feature: Token-Based Rate Limiting duration: "1m" completionTokenLimits: [] totalTokenLimits: [] - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/prompt-only-empty-limits/chat/completions" to be ready @@ -1675,8 +1643,6 @@ Feature: Token-Based Rate Limiting - count: 5 duration: "1m" totalTokenLimits: [] - algorithm: fixed-window - backend: memory """ Then the response status code should be 201 And I wait for the endpoint "http://localhost:8080/completion-only-empty-limits/chat/completions" to be ready @@ -1871,8 +1837,6 @@ Feature: Token-Based Rate Limiting totalTokenLimits: - count: 1000 duration: "1h" - algorithm: fixed-window - backend: memory consumerBased: true """ Then the response status code should be 201 diff --git a/tests/framework/suites/it/features/token_based_ratelimit.feature b/tests/framework/suites/it/features/token_based_ratelimit.feature index c4d57480fe..d2720908d2 100644 --- a/tests/framework/suites/it/features/token_based_ratelimit.feature +++ b/tests/framework/suites/it/features/token_based_ratelimit.feature @@ -52,7 +52,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[{"count":10,"duration":"1m"}],"totalTokenLimits":[{"count":20,"duration":"1m"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[{"count":10,"duration":"1m"}],"totalTokenLimits":[{"count":20,"duration":"1m"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -103,7 +103,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[{"count":5,"duration":"1m"}],"completionTokenLimits":[{"count":10,"duration":"1m"}],"totalTokenLimits":[{"count":15,"duration":"1m"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[{"count":5,"duration":"1m"}],"completionTokenLimits":[{"count":10,"duration":"1m"}],"totalTokenLimits":[{"count":15,"duration":"1m"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -148,7 +148,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"request-rewrite","version":"v1","paths":[{"path":"/chat/completions","methods":["POST","GET"],"params":{"pathRewrite":{"type":"ReplaceFullPath","replaceFullPath":"/gzip"}}}]},{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":2,"duration":"1m"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"request-rewrite","version":"v1","paths":[{"path":"/chat/completions","methods":["POST","GET"],"params":{"pathRewrite":{"type":"ReplaceFullPath","replaceFullPath":"/gzip"}}}]},{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":2,"duration":"1m"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -203,7 +203,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":100,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":100,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -255,7 +255,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContextA} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContextA}/chat/completions" until status 200 @@ -268,7 +268,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContextB} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContextB}/chat/completions" until status 200 @@ -327,7 +327,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[{"count":10,"duration":"1m"}],"completionTokenLimits":[{"count":20,"duration":"1m"}],"totalTokenLimits":[{"count":25,"duration":"1m"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[{"count":10,"duration":"1m"}],"completionTokenLimits":[{"count":20,"duration":"1m"}],"totalTokenLimits":[{"count":25,"duration":"1m"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -379,7 +379,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"10s"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"10s"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -428,7 +428,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":10,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":10,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -485,7 +485,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":10,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":10,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -533,7 +533,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -562,7 +562,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":10,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":10,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 # A deleted provider's own quota state is not guaranteed to invalidate synchronously with the @@ -611,7 +611,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContextAlpha} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContextAlpha}/chat/completions" until status 200 @@ -624,7 +624,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContextBeta} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"totalTokenLimits":[{"count":5,"duration":"1h"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContextBeta}/chat/completions" until status 200 @@ -679,7 +679,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[],"completionTokenLimits":[],"totalTokenLimits":[{"count":5,"duration":"1m"}],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[],"completionTokenLimits":[],"totalTokenLimits":[{"count":5,"duration":"1m"}]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -723,7 +723,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[{"count":5,"duration":"1m"}],"completionTokenLimits":[],"totalTokenLimits":[],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[{"count":5,"duration":"1m"}],"completionTokenLimits":[],"totalTokenLimits":[]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 @@ -767,7 +767,7 @@ Feature: Token-based rate limiting for LLM providers | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3002 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[],"completionTokenLimits":[{"count":5,"duration":"1m"}],"totalTokenLimits":[],"algorithm":"fixed-window","backend":"memory"}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/chat/completions","methods":["POST"],"params":{"promptTokenLimits":[],"completionTokenLimits":[{"count":5,"duration":"1m"}],"totalTokenLimits":[]}}]}] | Then the response status code should be 201 And I send a "GET" request to "${CTX:providerContext}/chat/completions" until status 200 diff --git a/tests/framework/suites/it/features/token_based_ratelimit_provider_templates.feature b/tests/framework/suites/it/features/token_based_ratelimit_provider_templates.feature index 1323b7c445..549a9bc282 100644 --- a/tests/framework/suites/it/features/token_based_ratelimit_provider_templates.feature +++ b/tests/framework/suites/it/features/token_based_ratelimit_provider_templates.feature @@ -110,7 +110,7 @@ Feature: Token-based rate limiting with built-in provider templates | spec.context | ${CTX:providerContext} | | spec.upstream.url | http://testbench:3008 | | accessControl.mode | allow_all | - | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/*","methods":["*"],"params":{"totalTokenLimits":[{"count":1000,"duration":"1h"}],"algorithm":"fixed-window","backend":"memory","consumerBased":true}}]}] | + | spec.policies | [{"name":"token-based-ratelimit","version":"v1","paths":[{"path":"/*","methods":["*"],"params":{"totalTokenLimits":[{"count":1000,"duration":"1h"}],"consumerBased":true}}]}] | Then the response status code should be 201 And I send a "POST" request to "${CTX:providerContext}/anthropic/v1/messages" until status 200 with body: """