Please select the area the issue is related to
Gateway
Please select the aspect the issue is related to
Aspect/API (API backends, definitions, contracts, interfaces, OpenAPI)
Description
PolicyValidator validates policy params against the policy definition's declared JSON Schema for most artifact types, but silently skips it for two of the five policy collections an LlmProvider/LlmProxy can declare.
validateLLMPolicyRefs resolves the policy reference for the operation-level and deprecated lists and then discards the resolved definition, so the params are never schema-checked:
for i, policy := range *operationPolicies {
_, errs := pv.validatePolicyRef(policy.Name, policy.Version, fmt.Sprintf("spec.operationPolicies[%d]", i))
errors = append(errors, errs...)
}
Only the global (api-level) list routes through validatePolicy, which performs the schema check.
Coverage today
| Artifact kind |
Policy collection |
Name + version |
Params vs. schema |
RestApi |
spec.policies |
✅ |
✅ |
RestApi |
spec.operations[].policies |
✅ |
✅ |
Mcp |
spec.policies |
✅ |
✅ |
LlmProvider / LlmProxy |
spec.globalPolicies |
✅ |
✅ |
LlmProvider / LlmProxy |
spec.operationPolicies[].paths[].params |
✅ |
❌ not validated |
LlmProvider / LlmProxy |
spec.policies[].paths[].params (deprecated) |
✅ |
❌ not validated |
Entry points for the rows that do work: ValidateRestAPIPolicies and ValidateMCPProxyPolicies.
Impact
A misconfigured operation-level LLM policy deploys successfully instead of being rejected at deploy time. Missing required params, out-of-range values, and unknown properties all pass. The failure surfaces later at runtime, where the policy is dropped or misbehaves with no deploy-time signal — and the same params on the same policy are correctly rejected when attached as a globalPolicy, which makes the behavior look arbitrary to users.
Nothing downstream closes the gap:
llm_deployment.go validates the source config after Transform, so the derived RestAPI (which does carry these params in operations[].policies) is never passed to ValidateRestAPIPolicies — see the provider and proxy paths.
- The event-listener path only calls
CoerceRestAPIPolicies — coercion, no validation: provider, proxy.
Note that coercion already walks all three LLM collections correctly — coerceLLMPolicyRefs iterates paths[j].Params. It is specifically the validation half that is asymmetric.
Root cause
Introduced in 18ab436e0 ("Validate policy name/version existence for LLM providers and proxies"), whose scope was name/version only. The inline comment shows the assumption behind it:
// Global (api-level) policies carry params, so reuse validatePolicy to also validate them.
Operation-level and deprecated policies do carry params - nested one level deeper, under paths[].params.
Notes for the fix
Validate the source config's per-path params, not the derived RestAPI.
The LLM→RestAPI transform merges the provider template's extraction params — requestModel, responseModel, promptTokens, completionTokens, totalTokens, remainingTokens — into every operation-level policy attachment via mergeParams, at three call sites. None of the shipped policy definitions declare those keys, and most set additionalProperties: false — so schema-validating the post-transform result rejects essentially every LLM operation-level policy with Additional property requestModel is not allowed. The user-authored params validate clean on their own.
Two details the fix must get right:
- A nil/absent
params map still has to be validated, or omitting params: entirely bypasses the schema's required list.
- Coerce before validating, since template rendering always yields strings (
{{ env "LIMIT" }} → "100" for an integer param) — matching how validatePolicy handles api-level params.
Steps to Reproduce
-
Start the gateway (controller on :9090) and deploy an LlmProvider named openai-dp-1 — e.g. gateway/examples/llm-provider.yaml.
-
Save an LlmProxy that attaches llm-cost-based-ratelimit as an operation-level policy with params that violate its schema — budgetLimits is required and the definition sets additionalProperties: false:
apiVersion: gateway.api-platform.wso2.com/v1
kind: LlmProxy
metadata:
name: param-validation-repro
spec:
displayName: Param Validation Repro
version: v1.0
context: /repro
provider:
id: openai-dp-1
operationPolicies:
- name: llm-cost-based-ratelimit
version: v1
paths:
- path: /chat/completions
methods: [POST]
params:
bogus: x # unknown property; required budgetLimits missing
-
Deploy it:
curl --location 'http://localhost:9090/api/management/v1/llm-proxies' \
--header 'Content-Type: text/yaml' \
--user "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
--data-binary '@param-validation-repro.yaml'
-
Contrast: move the identical policy block from operationPolicies to globalPolicies (params inline on the policy, no paths) and deploy again.
Expected
Both deploys are rejected with a policy validation error naming the offending param path.
Actual
globalPolicies → rejected, as expected:
spec.globalPolicies[0].params: budgetLimits is required
spec.globalPolicies[0].params: Additional property bogus is not allowed
operationPolicies → accepted and deployed. No errors.
The deprecated spec.policies[].paths[].params list behaves the same as operationPolicies.
Severity Level of the Issue
Severity/Minor (Non-critical functionality. Can be fixed in future releases)
Environment Details (with versions)
No response
Please select the area the issue is related to
Gateway
Please select the aspect the issue is related to
Aspect/API (API backends, definitions, contracts, interfaces, OpenAPI)
Description
PolicyValidatorvalidates policyparamsagainst the policy definition's declared JSON Schema for most artifact types, but silently skips it for two of the five policy collections anLlmProvider/LlmProxycan declare.validateLLMPolicyRefsresolves the policy reference for the operation-level and deprecated lists and then discards the resolved definition, so theparamsare never schema-checked:operationPoliciesand deprecatedpolicies.Only the global (api-level) list routes through
validatePolicy, which performs the schema check.Coverage today
RestApispec.policiesRestApispec.operations[].policiesMcpspec.policiesLlmProvider/LlmProxyspec.globalPoliciesLlmProvider/LlmProxyspec.operationPolicies[].paths[].paramsLlmProvider/LlmProxyspec.policies[].paths[].params(deprecated)Entry points for the rows that do work:
ValidateRestAPIPoliciesandValidateMCPProxyPolicies.Impact
A misconfigured operation-level LLM policy deploys successfully instead of being rejected at deploy time. Missing required params, out-of-range values, and unknown properties all pass. The failure surfaces later at runtime, where the policy is dropped or misbehaves with no deploy-time signal — and the same params on the same policy are correctly rejected when attached as a
globalPolicy, which makes the behavior look arbitrary to users.Nothing downstream closes the gap:
llm_deployment.govalidates the source config afterTransform, so the derivedRestAPI(which does carry these params inoperations[].policies) is never passed toValidateRestAPIPolicies— see the provider and proxy paths.CoerceRestAPIPolicies— coercion, no validation: provider, proxy.Note that coercion already walks all three LLM collections correctly —
coerceLLMPolicyRefsiteratespaths[j].Params. It is specifically the validation half that is asymmetric.Root cause
Introduced in
18ab436e0("Validate policy name/version existence for LLM providers and proxies"), whose scope was name/version only. The inline comment shows the assumption behind it:// Global (api-level) policies carry params, so reuse validatePolicy to also validate them.Operation-level and deprecated policies do carry params - nested one level deeper, under
paths[].params.Notes for the fix
Validate the source config's per-path params, not the derived
RestAPI.The LLM→RestAPI transform merges the provider template's extraction params —
requestModel,responseModel,promptTokens,completionTokens,totalTokens,remainingTokens— into every operation-level policy attachment viamergeParams, at three call sites. None of the shipped policy definitions declare those keys, and most setadditionalProperties: false— so schema-validating the post-transform result rejects essentially every LLM operation-level policy withAdditional property requestModel is not allowed. The user-authored params validate clean on their own.Two details the fix must get right:
paramsmap still has to be validated, or omittingparams:entirely bypasses the schema'srequiredlist.{{ env "LIMIT" }}→"100"for an integer param) — matching howvalidatePolicyhandles api-level params.Steps to Reproduce
Start the gateway (controller on
:9090) and deploy anLlmProvidernamedopenai-dp-1— e.g.gateway/examples/llm-provider.yaml.Save an
LlmProxythat attachesllm-cost-based-ratelimitas an operation-level policy with params that violate its schema —budgetLimitsis required and the definition setsadditionalProperties: false:Deploy it:
Contrast: move the identical policy block from
operationPoliciestoglobalPolicies(params inline on the policy, nopaths) and deploy again.Expected
Both deploys are rejected with a policy validation error naming the offending param path.
Actual
globalPolicies→ rejected, as expected:operationPolicies→ accepted and deployed. No errors.The deprecated
spec.policies[].paths[].paramslist behaves the same asoperationPolicies.Severity Level of the Issue
Severity/Minor (Non-critical functionality. Can be fixed in future releases)
Environment Details (with versions)
No response