From f9d978b14b8a1f91f6ec61d48da01e0c90dcccb9 Mon Sep 17 00:00:00 2001 From: pamaljayasinghe Date: Wed, 27 May 2026 09:17:25 +0530 Subject: [PATCH 1/9] Multi Provider Routing Policy changes --- .../openai-header-router.yaml | 78 ++++++ .../default-policies/openai-to-anthropic.yaml | 84 ++++++ .../openai-to-azure-openai.yaml | 78 ++++++ .../default-policies/openai-to-mistral.yaml | 61 +++++ gateway/build-manifest.yaml | 15 ++ gateway/build.yaml | 10 + gateway/examples/anthropic-openai-proxy.yaml | 42 +++ gateway/examples/anthropic-provider.yaml | 62 +++++ gateway/examples/azure-openai-provider.yaml | 67 +++++ gateway/examples/azure-openai-proxy.yaml | 37 +++ gateway/examples/gemini-provider.yaml | 58 +++++ gateway/examples/llm-provider.yaml | 38 +++ gateway/examples/llm-proxy.yaml | 4 + gateway/examples/mistral-openai-proxy.yaml | 43 +++ gateway/examples/mistral-provider.yaml | 63 +++++ .../examples/openai-multi-provider-proxy.yaml | 151 +++++++++++ gateway/examples/openai-provider.yaml | 67 +++++ .../api/management-openapi.yaml | 35 +++ .../pkg/api/management/generated.go | 13 + .../pkg/config/llm_validator.go | 75 ++++++ .../pkg/utils/llm_transformer.go | 134 +++++++--- .../pkg/utils/llm_transformer_test.go | 128 +++++++++ .../slugify-body/policy-definition.yaml | 19 +- .../analytics/policy-definition.yaml | 7 +- .../api/v1alpha1/llmproxy_types.go | 22 ++ .../api/v1alpha1/zz_generated.deepcopy.go | 32 +++ ...eway.api-platform.wso2.com_llmproxies.yaml | 79 ++++++ .../controller/llmproxy_controller.go | 38 ++- .../controller/llmproxy_controller_test.go | 82 ++++++ .../management_upstream_auth_payload.go | 20 ++ .../management_valuefrom_enqueue.go | 9 +- .../management_valuefrom_fingerprint.go | 6 +- ...eway.api-platform.wso2.com_llmproxies.yaml | 79 ++++++ mistral-proxy.yaml | 246 ++++++++++++++++++ platform-api/src/api/generated.go | 12 + .../src/internal/dto/llm_deployment.go | 18 +- platform-api/src/internal/model/llm.go | 26 +- platform-api/src/internal/service/llm.go | 59 ++++- .../src/internal/service/llm_deployment.go | 17 +- platform-api/src/resources/openapi.yaml | 31 +++ 40 files changed, 2057 insertions(+), 88 deletions(-) create mode 100644 event-gateway/default-policies/openai-header-router.yaml create mode 100644 event-gateway/default-policies/openai-to-anthropic.yaml create mode 100644 event-gateway/default-policies/openai-to-azure-openai.yaml create mode 100644 event-gateway/default-policies/openai-to-mistral.yaml create mode 100644 gateway/examples/anthropic-openai-proxy.yaml create mode 100644 gateway/examples/anthropic-provider.yaml create mode 100644 gateway/examples/azure-openai-provider.yaml create mode 100644 gateway/examples/azure-openai-proxy.yaml create mode 100644 gateway/examples/gemini-provider.yaml create mode 100644 gateway/examples/mistral-openai-proxy.yaml create mode 100644 gateway/examples/mistral-provider.yaml create mode 100644 gateway/examples/openai-multi-provider-proxy.yaml create mode 100644 gateway/examples/openai-provider.yaml create mode 100644 mistral-proxy.yaml diff --git a/event-gateway/default-policies/openai-header-router.yaml b/event-gateway/default-policies/openai-header-router.yaml new file mode 100644 index 0000000000..c7633e3e3e --- /dev/null +++ b/event-gateway/default-policies/openai-header-router.yaml @@ -0,0 +1,78 @@ +name: openai-header-router +version: v1.0.0 +description: | + Selects a target LLM provider for each incoming OpenAI Chat Completions + request by inspecting a request header, then publishes the chosen + provider id into SharedContext.Metadata["selected_provider"]. + + This policy is a "setter" — its only job is to publish the selection. + Downstream consumer policies (the per-provider translators such as + openai-to-anthropic, openai-to-azure-openai, and openai-to-mistral) + read that key and own both the translation and the upstream-cluster + selection. + + Selection algorithm: + + 1. The configured header is read from the request (case-insensitive). + 2. Each entry in "mappings" is checked: if its "headerValue" matches the + incoming header value (case-insensitive), the entry's "provider" is + selected. + 3. If the header is absent, empty, or no mapping matches, the configured + "defaultProvider" is selected. + + The defaultProvider field is required so this policy always produces a + selection — there is no "pass through with no provider" mode. + +parameters: + type: object + additionalProperties: false + properties: + headerName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the request header to read for provider selection. + Comparison is case-insensitive. + default: x-provider + minLength: 1 + defaultProvider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id selected when the header is missing, empty, or does + not match any entry in "mappings". This must be a provider id that + a downstream translator is bound to. + minLength: 1 + mappings: + type: array + x-wso2-policy-advanced-param: false + description: | + Mapping from incoming header values to provider ids. The first + matching entry wins. + items: + type: object + additionalProperties: false + properties: + headerValue: + type: string + description: | + Header value to match (case-insensitive, leading/trailing + whitespace trimmed). + minLength: 1 + provider: + type: string + description: | + Provider id published to SharedContext.Metadata + ["selected_provider"] when this entry matches. + minLength: 1 + required: + - headerValue + - provider + minItems: 1 + required: + - mappings + - defaultProvider + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-anthropic.yaml b/event-gateway/default-policies/openai-to-anthropic.yaml new file mode 100644 index 0000000000..bd695e8ce0 --- /dev/null +++ b/event-gateway/default-policies/openai-to-anthropic.yaml @@ -0,0 +1,84 @@ +name: openai-to-anthropic +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the Anthropic + Messages API format and forwards it to an Anthropic-compatible upstream. + + Request transformations: + - system/developer messages are lifted into the top-level "system" field. + - messages[] is converted to Anthropic role/content blocks. + - assistant tool_calls become tool_use content blocks; tool-role messages + are grouped into a single user message of tool_result blocks. + - tools[] / tool_choice are converted to the Anthropic equivalents. + - stop is mapped to stop_sequences; image_url parts to image blocks. + - The request path is rewritten to /v1/messages and the anthropic-version + header is set. + + Response transformations: + - The Anthropic Messages response body is rewritten into the OpenAI + ChatCompletion shape (id, object, created, model, choices, usage). + - text content blocks become choice.message.content; tool_use blocks + become choice.message.tool_calls; stop_reason is mapped to + finish_reason; cache token counts are carried in usage + .prompt_tokens_details. + - Anthropic error envelopes are rewritten into OpenAI error envelopes + so clients see consistent error shapes regardless of provider. + - Streaming (Server-Sent Events) responses are passed through + untouched in this policy version; clients that need true streaming + should request non-streaming (omit "stream": true) until a streaming + variant ships. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests pass + through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Anthropic API key must be configured on the + upstream LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Anthropic model name used in the translated request (for example + claude-sonnet-4-20250514). Overrides the OpenAI "model" field. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + anthropicVersion: + type: string + x-wso2-policy-advanced-param: true + description: | + Value of the anthropic-version request header sent upstream. + default: "2023-06-01" + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-azure-openai.yaml b/event-gateway/default-policies/openai-to-azure-openai.yaml new file mode 100644 index 0000000000..67bb75e537 --- /dev/null +++ b/event-gateway/default-policies/openai-to-azure-openai.yaml @@ -0,0 +1,78 @@ +name: openai-to-azure-openai +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the Azure + OpenAI request shape and forwards it to an Azure-OpenAI-compatible + upstream. + + Transformation: + - The request path is rewritten to + /openai/deployments/{deployment}/{pathSuffix}?api-version=. + - The body passes through unchanged — Azure OpenAI accepts the OpenAI + request shape directly. + - The deployment id is taken from the "model" parameter when set, + otherwise from the request body's "model" field. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests + pass through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Azure api-key must be configured on the upstream + LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + apiVersion: + type: string + x-wso2-policy-advanced-param: false + description: | + Azure OpenAI api-version query-string value (for example + 2024-02-15-preview). Required because Azure rejects requests + without it. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Azure deployment id used in the rewritten path. When omitted, the + request body's "model" field is used as the deployment id. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + pathSuffix: + type: string + x-wso2-policy-advanced-param: true + description: | + Endpoint suffix appended after the deployment segment in the + rewritten path. Defaults to /chat/completions. + default: /chat/completions + minLength: 1 + required: + - apiVersion + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-mistral.yaml b/event-gateway/default-policies/openai-to-mistral.yaml new file mode 100644 index 0000000000..f2ac1d85bd --- /dev/null +++ b/event-gateway/default-policies/openai-to-mistral.yaml @@ -0,0 +1,61 @@ +name: openai-to-mistral +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the format + expected by Mistral AI's chat completions endpoint and forwards it to a + Mistral-compatible upstream. + + Mistral's chat completions API is largely OpenAI-compatible, so the + translation is small in scope: + - The "model" field is overridden with the configured Mistral model. + - OpenAI request fields that Mistral does not accept (logprobs, n, + service_tier, store, metadata, user, etc.) are stripped. + - The request path is rewritten to /v1/chat/completions. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests + pass through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Mistral API key must be configured on the upstream + LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Mistral model name used in the translated request (for example + mistral-large-latest). Overrides the OpenAI "model" field. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/gateway/build-manifest.yaml b/gateway/build-manifest.yaml index 3787d4e44d..cb4dec14cc 100644 --- a/gateway/build-manifest.yaml +++ b/gateway/build-manifest.yaml @@ -126,3 +126,18 @@ policies: - name: word-count-guardrail version: v1.0.2 gomodule: github.com/wso2/gateway-controllers/policies/word-count-guardrail@v1 + - name: openai-to-azure-openai + version: v1.0.0 + filePath: ./dev-policies/openai-to-azure-openai + - name: openai-to-anthropic + version: v1.0.0 + filePath: ./dev-policies/openai-to-anthropic + - name: openai-to-mistral + version: v1.0.0 + filePath: ./dev-policies/openai-to-mistral + - name: openai-to-gemini + version: v1.0.0 + filePath: ./dev-policies/openai-to-gemini + - name: openai-header-router + version: v1.0.0 + filePath: ./dev-policies/openai-header-router diff --git a/gateway/build.yaml b/gateway/build.yaml index 7e4d2cd995..205afba44e 100644 --- a/gateway/build.yaml +++ b/gateway/build.yaml @@ -86,3 +86,13 @@ policies: gomodule: github.com/wso2/gateway-controllers/policies/url-guardrail@v1 - name: word-count-guardrail gomodule: github.com/wso2/gateway-controllers/policies/word-count-guardrail@v1 + - name: openai-to-azure-openai + filePath: ./dev-policies/openai-to-azure-openai + - name: openai-to-anthropic + filePath: ./dev-policies/openai-to-anthropic + - name: openai-to-mistral + filePath: ./dev-policies/openai-to-mistral + - name: openai-to-gemini + filePath: ./dev-policies/openai-to-gemini + - name: openai-header-router + filePath: ./dev-policies/openai-header-router diff --git a/gateway/examples/anthropic-openai-proxy.yaml b/gateway/examples/anthropic-openai-proxy.yaml new file mode 100644 index 0000000000..4ac65baec2 --- /dev/null +++ b/gateway/examples/anthropic-openai-proxy.yaml @@ -0,0 +1,42 @@ +# -------------------------------------------------------------------- +# OpenAI -> Anthropic LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-anthropic` policy translates into Anthropic's Messages API +# format and forwards to the existing `anthropic-provider` upstream +# (api.anthropic.com). +# +# Single-provider mode: there is no router in front of the policy, so +# nothing sets metadata["selected_provider"] and the translator runs on +# every request. "id" is omitted, so it routes to the default upstream +# (the primary provider above). +# +# The provider must already be deployed and must have its x-api-key +# set on upstream.auth (the policy does NOT inject the Anthropic key). +# +# The provider example's api-key-auth policy expects the proxy to send the +# issued provider loopback key in X-API-Key. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-anthropic +spec: + displayName: OpenAI to Anthropic Proxy + version: v1.0 + context: /openai-anthropic + provider: + id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + policies: + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 diff --git a/gateway/examples/anthropic-provider.yaml b/gateway/examples/anthropic-provider.yaml new file mode 100644 index 0000000000..6e1e8ec75b --- /dev/null +++ b/gateway/examples/anthropic-provider.yaml @@ -0,0 +1,62 @@ +# -------------------------------------------------------------------- +# Anthropic LLM Provider Configuration +# +# Uses the built-in `anthropic` template and points to the public +# Anthropic API endpoint. Replace the placeholder with a real Anthropic +# API key (sk-ant-...) before deploying. +# +# The openai-to-anthropic policy on the proxy layer translates +# OpenAI-format requests into Anthropic's Messages API wire format +# (path /v1/messages, anthropic-version header, body rewrite). +# +# Auth: upstream.auth is the gateway -> Anthropic vendor auth. Proxies in +# front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: anthropic-provider +spec: + displayName: Anthropic Provider + version: v1.0 + template: anthropic + upstream: + url: https://api.anthropic.com + auth: + type: api-key + header: x-api-key + # Anthropic's header is x-api-key with the raw key (NO "Bearer" prefix). + value: sk-ant-REPLACE_WITH_ANTHROPIC_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/messages + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/messages + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: anthropic-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: anthropic-provider + displayName: Anthropic provider loopback key + apiKey: + value: anthropic_provider_loopback_key_1234567890abcdef + # anthropic_provider_loopback_key_1234567890abcdef + maskedApiKey: anthropic_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/azure-openai-provider.yaml b/gateway/examples/azure-openai-provider.yaml new file mode 100644 index 0000000000..e1345ac3c4 --- /dev/null +++ b/gateway/examples/azure-openai-provider.yaml @@ -0,0 +1,67 @@ +# -------------------------------------------------------------------- +# Azure OpenAI LLM Provider Configuration +# +# Uses the built-in 'azure-openai' template and points to your Azure +# OpenAI endpoint. The openai-to-azure-openai policy on the proxy layer +# translates OpenAI-format requests into the Azure OpenAI wire format +# (path rewriting, api-version query param). +# +# Replace before deploying: +# - url: your Azure OpenAI endpoint, e.g. https://my-resource.openai.azure.com +# - value: your Azure OpenAI API key +# +# Auth: upstream.auth is the gateway -> Azure vendor auth (header: api-key). +# Proxies in front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: azure-openai-provider +spec: + displayName: Azure OpenAI Provider + version: v1.0 + template: azure-openai + upstream: + url: #endpoint url + auth: + type: api-key + header: api-key + value: testkeyaddhere + policies: + - name: api-key-auth + version: v1 + paths: + - path: /openai/deployments/{deployment}/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /openai/deployments/{deployment}/chat/completions + methods: [POST] + - path: /openai/deployments/{deployment}/completions + methods: [POST] + - path: /openai/deployments/{deployment}/embeddings + methods: [POST] + - path: /openai/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: azure-openai-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: azure-openai-provider + displayName: Azure OpenAI provider loopback key + apiKey: + value: azure_openai_provider_loopback_key_1234567890abcdef + maskedApiKey: azure_openai_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/azure-openai-proxy.yaml b/gateway/examples/azure-openai-proxy.yaml new file mode 100644 index 0000000000..0466a62e72 --- /dev/null +++ b/gateway/examples/azure-openai-proxy.yaml @@ -0,0 +1,37 @@ +# -------------------------------------------------------------------- +# OpenAI -> Azure OpenAI LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-azure-openai` policy translates into Azure OpenAI's +# /openai/deployments/{deployment}/chat/completions?api-version=... +# format and forwards to the `azure-openai-provider` upstream. +# +# Single-provider mode: the policy omits the "provider" parameter, so +# it runs on every request without needing a setter policy. +# +# Replace before deploying: +# - apiVersion param: a valid Azure OpenAI api-version +# (e.g. 2024-02-15-preview) +# - model param (optional): pin the deployment id here, otherwise +# the request body's "model" field is used as the deployment. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-azure-openai +spec: + displayName: OpenAI to Azure OpenAI Proxy + version: v1.0 + context: /azure-openai + provider: + id: azure-openai-provider + policies: + - name: openai-to-azure-openai + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + apiVersion: "2024-02-15-preview" + model: gpt-4o diff --git a/gateway/examples/gemini-provider.yaml b/gateway/examples/gemini-provider.yaml new file mode 100644 index 0000000000..833bc962e5 --- /dev/null +++ b/gateway/examples/gemini-provider.yaml @@ -0,0 +1,58 @@ +# -------------------------------------------------------------------- +# Gemini LLM Provider Configuration +# +# Uses the built-in `gemini` template and points to Google's Gemini API. +# The openai-to-gemini policy on the proxy layer translates OpenAI-format +# requests into Gemini's generateContent API format. +# +# Auth: upstream.auth is the gateway -> Gemini vendor auth +# (header: x-goog-api-key). Proxies inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: gemini-provider +spec: + displayName: Gemini Provider + version: v1.0 + template: gemini + upstream: + url: https://generativelanguage.googleapis.com + auth: + type: api-key + header: x-goog-api-key + value: REPLACE_WITH_GEMINI_API_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1beta/models/{model}:generateContent + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1beta/models/{model}:generateContent + methods: [POST] + - path: /v1beta/models/{model}:streamGenerateContent + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: gemini-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: gemini-provider + displayName: Gemini provider loopback key + apiKey: + value: gemini_provider_loopback_key_1234567890abcdef + maskedApiKey: gemini_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/llm-provider.yaml b/gateway/examples/llm-provider.yaml index 7b4b8a7686..4585091617 100644 --- a/gateway/examples/llm-provider.yaml +++ b/gateway/examples/llm-provider.yaml @@ -31,6 +31,25 @@ spec: type: api-key header: Authorization value: api_key_abc123 + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - path: /models + methods: [GET] + params: + key: X-API-Key + in: header + - path: /models/{modelId} + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: @@ -40,3 +59,22 @@ spec: methods: [GET] - path: /models/{modelId} methods: [GET] + +--- +# Provider API key used by clients or by an LlmProxy provider.auth block. +# In production, prefer apiKey.valueFrom with a Kubernetes Secret. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: my-llm-provider-key +spec: + parentRef: + kind: LlmProvider + name: my-llm-provider + displayName: My LLM provider key + apiKey: + value: provider_loopback_key_1234567890abcdef1234567890 + maskedApiKey: provider_loopback_key_1234********7890 + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/llm-proxy.yaml b/gateway/examples/llm-proxy.yaml index fa3b6d5c02..5a78844a8c 100644 --- a/gateway/examples/llm-proxy.yaml +++ b/gateway/examples/llm-proxy.yaml @@ -26,6 +26,10 @@ spec: context: /assistant provider: id: my-llm-provider + auth: + type: api-key + header: X-API-Key + value: provider_loopback_key_1234567890abcdef1234567890 policies: - name: semanticPromptGuard version: v1 diff --git a/gateway/examples/mistral-openai-proxy.yaml b/gateway/examples/mistral-openai-proxy.yaml new file mode 100644 index 0000000000..17156d1068 --- /dev/null +++ b/gateway/examples/mistral-openai-proxy.yaml @@ -0,0 +1,43 @@ +# -------------------------------------------------------------------- +# OpenAI -> Mistral LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-mistral` policy adapts to Mistral's chat completions API +# and forwards to the existing `mistral-provider` upstream +# (api.mistral.ai). +# +# Single-provider mode: there is no router in front of the policy, so +# nothing sets metadata["selected_provider"] and the translator runs on +# every request. "id" is omitted, so it routes to the default upstream +# (the primary provider above). +# +# The provider must already be deployed and must have its Authorization +# bearer token set on upstream.auth (the policy does NOT inject the +# Mistral key). +# +# The provider example's api-key-auth policy expects the proxy to send the +# issued provider loopback key in X-API-Key. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-mistral +spec: + displayName: OpenAI to Mistral Proxy + version: v1.0 + context: /openai-mistral + provider: + id: mistral-provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + policies: + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest diff --git a/gateway/examples/mistral-provider.yaml b/gateway/examples/mistral-provider.yaml new file mode 100644 index 0000000000..67ec1c2654 --- /dev/null +++ b/gateway/examples/mistral-provider.yaml @@ -0,0 +1,63 @@ +# -------------------------------------------------------------------- +# Mistral LLM Provider Configuration +# +# Uses the built-in `mistralai` template and points to the public +# Mistral API endpoint. Replace the placeholder with a real Mistral key. +# +# The openai-to-mistral policy on the proxy layer translates +# OpenAI-format requests into Mistral's wire format (path +# /v1/chat/completions, model override, drop unsupported fields). +# +# Auth: upstream.auth is the gateway -> Mistral vendor auth. Proxies in +# front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: mistral-provider +spec: + displayName: Mistral Provider + version: v1.0 + template: mistralai + upstream: + url: https://api.mistral.ai + auth: + type: api-key + header: Authorization + value: $dxxxd + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/chat/completions + methods: [POST] + - path: /v1/embeddings + methods: [POST] + - path: /v1/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: mistral-provider + displayName: Mistral provider loopback key + apiKey: + value: mistral_provider_loopback_key_1234567890abcdef + maskedApiKey: mistral_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/openai-multi-provider-proxy.yaml b/gateway/examples/openai-multi-provider-proxy.yaml new file mode 100644 index 0000000000..a8ba025970 --- /dev/null +++ b/gateway/examples/openai-multi-provider-proxy.yaml @@ -0,0 +1,151 @@ +# -------------------------------------------------------------------- +# OpenAI Multi-Provider Proxy +# +# Exposes one OpenAI-shaped /chat/completions endpoint and fans out to +# several LLM providers. Two modes are supported by the translators: +# +# * Single-provider mode — attach exactly one translator with no +# router in front of it. With no "selected_provider" in the request +# metadata the translator runs unconditionally. +# +# * Multi-provider mode — put openai-header-router first. It writes +# the chosen provider into metadata["selected_provider"]. Each +# translator then runs only when that selection equals its own "id". +# The "id" doubles as the upstream cluster name, so it must match an +# entry in additionalProviders (id or as). +# +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-multi +spec: + displayName: OpenAI Multi-Provider Proxy + version: v1.0 + context: /openai-multi + provider: + id: openai-provider + auth: + type: api-key + header: X-API-Key + value: openai_provider_loopback_key_1234567890abcdef + additionalProviders: + - id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + - id: azure-openai-provider + auth: + type: api-key + header: X-API-Key + value: azure_openai_provider_loopback_key_1234567890abcdef + - id: mistral-provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + #mistral_provider_loopback_key_1234567890abcdef + # - id: gemini-provider + # auth: + # type: api-key + # header: X-API-Key + # value: gemini_provider_loopback_key_1234567890abcdef + + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - name: openai-header-router + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + headerName: x-provider + defaultProvider: openai-provider + mappings: + - headerValue: openai + provider: openai-provider + - headerValue: anthropic + provider: anthropic-provider + - headerValue: azure-openai + provider: azure-openai-provider + - headerValue: mistral + provider: mistral-provider + # - headerValue: gemini + # provider: gemini-provider + + # - name: token-based-ratelimit + # version: v1 + # paths: + # - path: /chat/completions + # methods: [POST] + # params: + # totalTokenLimits: + # - count: 40 + # duration: "1m" + # algorithm: fixed-window + # backend: memory + + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 + id: anthropic-provider + + - name: openai-to-azure-openai + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + apiVersion: "2024-02-15-preview" + model: gpt-4o + id: azure-openai-provider + + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + id: mistral-provider + + # - name: openai-to-gemini + # version: v1 + # paths: + # - path: /chat/completions + # methods: [POST] + # params: + # model: gemini-2.5-flash + # id: gemini-provider + # apiVersion: v1beta + +--- +# Client API key for this proxy. The api-key-auth policy above validates +# the X-API-Key header against the key the gateway issues for this resource. +# apiKey is omitted here, so the gateway generates one. Read it from the +# create response (POST .../llm-proxies/openai-multi/api-keys). +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-multi-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-multi + displayName: OpenAI multi-proxy consumer key + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/openai-provider.yaml b/gateway/examples/openai-provider.yaml new file mode 100644 index 0000000000..b206cff3f2 --- /dev/null +++ b/gateway/examples/openai-provider.yaml @@ -0,0 +1,67 @@ +# -------------------------------------------------------------------- +# OpenAI LLM Provider Configuration +# +# Uses the built-in `openai` template and points to the OpenAI public +# endpoint. The auth value below is a SAMPLE / placeholder token -- +# replace it with a real OpenAI API key (Bearer sk-...) before deploying. +# +# Auth: upstream.auth is the gateway -> OpenAI vendor auth. This is the +# only auth this provider needs; proxies in front of it route freely and +# inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: openai-provider +spec: + displayName: OpenAI Provider + version: v1.0 + template: openai + upstream: + url: https://api.openai.com/v1 + auth: + type: api-key + header: Authorization + # OpenAI expects: Authorization: Bearer -- sent verbatim, so the + # "Bearer " prefix MUST be included here. + value: Bearer sk-REPLACE_WITH_OPENAI_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /chat/completions + methods: [POST] + - path: /completions + methods: [POST] + - path: /embeddings + methods: [POST] + - path: /models + methods: [GET] + - path: /models/{modelId} + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: openai-provider + displayName: OpenAI provider loopback key + apiKey: + value: openai_provider_loopback_key_1234567890abcdef + maskedApiKey: openai_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/gateway-controller/api/management-openapi.yaml b/gateway/gateway-controller/api/management-openapi.yaml index 47c58fffb4..7dd022450a 100644 --- a/gateway/gateway-controller/api/management-openapi.yaml +++ b/gateway/gateway-controller/api/management-openapi.yaml @@ -5826,6 +5826,32 @@ components: auth: $ref: '#/components/schemas/LLMUpstreamAuth' + LLMProxyAdditionalProvider: + type: object + required: + - id + description: > + Additional LLM provider attached to this proxy as a selectable upstream. + Policies route to it by referring to the `as` name (defaults to `id`). + Optional auth config is used by the proxy when calling a protected + LlmProvider over the internal loopback route. + properties: + id: + type: string + description: Unique id of a deployed llm provider + example: anthropic-provider + as: + type: string + description: > + Logical upstream name used by policies to select this provider. Must + be unique within the proxy. Defaults to `id` when omitted. + pattern: '^[a-zA-Z0-9\-_]+$' + minLength: 1 + maxLength: 100 + example: anthropic-upstream + auth: + $ref: '#/components/schemas/LLMUpstreamAuth' + LLMAccessControl: type: object required: @@ -6009,6 +6035,15 @@ components: example: "api.openai.com" provider: $ref: '#/components/schemas/LLMProxyProvider' + additionalProviders: + type: array + description: > + Optional list of additional LLM providers attached to this proxy as + selectable upstreams. Policies (e.g. an OpenAI translator) can route + requests to any of these by setting the upstream name. The primary + `provider` field above remains the default upstream and the FK target. + items: + $ref: '#/components/schemas/LLMProxyAdditionalProvider' policies: type: array description: List of policies applied only to this operation (overrides or adds to API-level policies) diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index 4543d15129..2eb32544e0 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -917,8 +917,21 @@ type LLMProviderTemplateResourceMappings struct { Resources *[]LLMProviderTemplateResourceMapping `json:"resources,omitempty" yaml:"resources,omitempty"` } +// LLMProxyAdditionalProvider Additional LLM provider attached to this proxy as a selectable upstream. Policies route to it by referring to the `as` name (defaults to `id`). Optional auth config is used by the proxy when calling a protected LlmProvider over the internal loopback route. +type LLMProxyAdditionalProvider struct { + // As Logical upstream name used by policies to select this provider. Must be unique within the proxy. Defaults to `id` when omitted. + As *string `json:"as,omitempty" yaml:"as,omitempty"` + Auth *LLMUpstreamAuth `json:"auth,omitempty" yaml:"auth,omitempty"` + + // Id Unique id of a deployed llm provider + Id string `json:"id" yaml:"id"` +} + // LLMProxyConfigData defines model for LLMProxyConfigData. type LLMProxyConfigData struct { + // AdditionalProviders Optional list of additional LLM providers attached to this proxy as selectable upstreams. Policies (e.g. an OpenAI translator) can route requests to any of these by setting the upstream name. The primary `provider` field above remains the default upstream and the FK target. + AdditionalProviders *[]LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" yaml:"additionalProviders,omitempty"` + // Context Base path for all API routes (must start with /, no trailing slash) Context *string `json:"context,omitempty" yaml:"context,omitempty"` diff --git a/gateway/gateway-controller/pkg/config/llm_validator.go b/gateway/gateway-controller/pkg/config/llm_validator.go index 62fed822ac..cb44132b63 100644 --- a/gateway/gateway-controller/pkg/config/llm_validator.go +++ b/gateway/gateway-controller/pkg/config/llm_validator.go @@ -565,7 +565,82 @@ func (v *LLMValidator) validateProxyData(spec *api.LLMProxyConfigData) []Validat Message: "spec.provider.id must consist of lowercase alphanumeric characters, hyphens, or dots, and must start and end with an alphanumeric character", }) } + if spec.Provider.Auth != nil { + errors = append(errors, v.validateLLMUpstreamAuth("spec.provider.auth", spec.Provider.Auth)...) + } + + if spec.AdditionalProviders != nil { + seen := map[string]bool{spec.Provider.Id: true} + for i, provider := range *spec.AdditionalProviders { + fieldPrefix := fmt.Sprintf("spec.additionalProviders[%d]", i) + if provider.Id == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".id", + Message: "Provider is required", + }) + } else if !v.metadataNameRegex.MatchString(provider.Id) { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".id", + Message: fieldPrefix + ".id must consist of lowercase alphanumeric characters, hyphens, or dots, and must start and end with an alphanumeric character", + }) + } + + upstreamName := provider.Id + if provider.As != nil && *provider.As != "" { + upstreamName = *provider.As + if !regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString(upstreamName) { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".as", + Message: fieldPrefix + ".as must contain only letters, numbers, hyphens, or underscores", + }) + } + } + if upstreamName != "" { + if seen[upstreamName] { + errors = append(errors, ValidationError{ + Field: fieldPrefix, + Message: fmt.Sprintf("duplicate upstream name '%s' in additionalProviders", upstreamName), + }) + } + seen[upstreamName] = true + } + + if provider.Auth != nil { + errors = append(errors, v.validateLLMUpstreamAuth(fieldPrefix+".auth", provider.Auth)...) + } + } + } + + return errors +} +func (v *LLMValidator) validateLLMUpstreamAuth(fieldPrefix string, auth *api.LLMUpstreamAuth) []ValidationError { + var errors []ValidationError + if auth.Type == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".type", + Message: "Auth type is required", + }) + } else if auth.Type != api.LLMUpstreamAuthTypeApiKey { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".type", + Message: "Auth type must be 'api-key'", + }) + } + if auth.Type == api.LLMUpstreamAuthTypeApiKey { + if auth.Header == nil || *auth.Header == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".header", + Message: "Auth header is required when api-key auth type is set", + }) + } + if auth.Value == nil || *auth.Value == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".value", + Message: "Auth value is required when api-key auth type is set", + }) + } + } return errors } diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer.go b/gateway/gateway-controller/pkg/utils/llm_transformer.go index 3c4d5edf30..701114746c 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer.go @@ -148,6 +148,50 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration spec.Upstream.Main = api.Upstream{ Url: &upstream, } + + // Step 3.1: Resolve additional providers (multi-provider proxies). Each is + // exposed as a named UpstreamDefinition so policies can route to it via + // the loopback context. The primary provider above remains the default. + if proxy.Spec.AdditionalProviders != nil && len(*proxy.Spec.AdditionalProviders) > 0 { + seen := map[string]bool{proxy.Spec.Provider.Id: true} + var defs []api.UpstreamDefinition + for _, ap := range *proxy.Spec.AdditionalProviders { + if ap.Id == "" { + return nil, fmt.Errorf("additionalProviders entry must have a non-empty id") + } + name := ap.Id + if ap.As != nil && *ap.As != "" { + name = *ap.As + } + if seen[name] { + return nil, fmt.Errorf("duplicate upstream name '%s' in additionalProviders (must be unique within the proxy and not collide with the primary provider id)", name) + } + seen[name] = true + + addCfg, err := t.db.GetConfigByKindAndHandle(string(api.LLMProviderConfigurationKindLlmProvider), ap.Id) + if err != nil { + return nil, fmt.Errorf("failed to look up additional provider '%s': %w", ap.Id, err) + } + if addCfg == nil { + return nil, fmt.Errorf("additional provider '%s' not found", ap.Id) + } + addCtx, err := addCfg.GetContext() + if err != nil { + return nil, fmt.Errorf("failed to get context for additional provider '%s': %w", ap.Id, err) + } + addURL := fmt.Sprintf("%s://%s:%d%s", + constants.SchemeHTTP, constants.LocalhostIP, t.routerConfig.ListenerPort, addCtx) + defs = append(defs, api.UpstreamDefinition{ + Name: name, + Upstreams: []struct { + Url string `json:"url" yaml:"url"` + Weight *int `json:"weight,omitempty" yaml:"weight,omitempty"` + }{{Url: addURL}}, + }) + } + spec.UpstreamDefinitions = &defs + } + // If provider has vhost configured add a host adding policy if providerConfig.Spec.Vhost != nil && *providerConfig.Spec.Vhost != "" { providerVhost := *providerConfig.Spec.Vhost @@ -182,34 +226,32 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration } // Step 3.5: Apply proxy-level provider auth for proxy->provider loopback upstream - var upstreamAuthPolicy *api.Policy + var upstreamAuthPolicies []api.Policy if proxy.Spec.Provider.Auth != nil { - auth := proxy.Spec.Provider.Auth - switch auth.Type { - case api.LLMUpstreamAuthTypeApiKey: - if auth.Value == nil || *auth.Value == "" { - return nil, fmt.Errorf("provider.auth.value is required") - } - header := "" - if auth.Header != nil { - header = *auth.Header + pol, err := t.proxyUpstreamAuthPolicy(proxy.Spec.Provider.Auth, "provider.auth") + if err != nil { + return nil, err + } + condition := selectedProviderExecutionCondition(proxy.Spec.Provider.Id, true) + pol.ExecutionCondition = &condition + upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) + } + if proxy.Spec.AdditionalProviders != nil { + for _, ap := range *proxy.Spec.AdditionalProviders { + if ap.Auth == nil { + continue } - params, err := GetUpstreamAuthApikeyPolicyParams(header, *auth.Value) - if err != nil { - return nil, fmt.Errorf("failed to build upstream auth params: %w", err) + name := ap.Id + if ap.As != nil && *ap.As != "" { + name = *ap.As } - policyVersion, err := t.resolvePolicyVersion(constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME) + pol, err := t.proxyUpstreamAuthPolicy(ap.Auth, fmt.Sprintf("additionalProviders[%s].auth", name)) if err != nil { return nil, err } - mh := api.Policy{ - Name: constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME, - Version: policyVersion, - Params: ¶ms, - } - upstreamAuthPolicy = &mh - default: - return nil, fmt.Errorf("unsupported upstream auth type: %s", auth.Type) + condition := selectedProviderExecutionCondition(name, false) + pol.ExecutionCondition = &condition + upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) } } @@ -282,14 +324,10 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration ops = append(ops, *op) } ops = sortOperationsBySpecificity(ops) - if upstreamAuthPolicy != nil { + if len(upstreamAuthPolicies) > 0 { for i := range ops { - if ops[i].Policies == nil { - ops[i].Policies = &[]api.Policy{*upstreamAuthPolicy} - } else { - existing := *ops[i].Policies - existing = append(existing, *upstreamAuthPolicy) - ops[i].Policies = &existing + for _, upstreamAuthPolicy := range upstreamAuthPolicies { + appendOperationPolicy(&ops[i], upstreamAuthPolicy) } } } @@ -625,6 +663,44 @@ func GetUpstreamAuthApikeyPolicyParams(header, value string) (map[string]interfa return m, nil } +func (t *LLMProviderTransformer) proxyUpstreamAuthPolicy(auth *api.LLMUpstreamAuth, field string) (*api.Policy, error) { + if auth == nil { + return nil, nil + } + switch auth.Type { + case api.LLMUpstreamAuthTypeApiKey: + if auth.Header == nil || *auth.Header == "" { + return nil, fmt.Errorf("%s.header is required", field) + } + if auth.Value == nil || *auth.Value == "" { + return nil, fmt.Errorf("%s.value is required", field) + } + params, err := GetUpstreamAuthApikeyPolicyParams(*auth.Header, *auth.Value) + if err != nil { + return nil, fmt.Errorf("failed to build upstream auth params: %w", err) + } + policyVersion, err := t.resolvePolicyVersion(constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME) + if err != nil { + return nil, err + } + return &api.Policy{ + Name: constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME, + Version: policyVersion, + Params: ¶ms, + }, nil + default: + return nil, fmt.Errorf("unsupported upstream auth type: %s", auth.Type) + } +} + +func selectedProviderExecutionCondition(providerName string, includeDefault bool) string { + selectedExpr := fmt.Sprintf("request.Metadata['selected_provider'] == '%s'", providerName) + if includeDefault { + return fmt.Sprintf("!('selected_provider' in request.Metadata) || %s", selectedExpr) + } + return fmt.Sprintf("'selected_provider' in request.Metadata && %s", selectedExpr) +} + // GetHostAdditionPolicyParams renders the policy params with given host value (host-rewrite) func GetHostAdditionPolicyParams(value string) (map[string]interface{}, error) { rendered := fmt.Sprintf(constants.PROXY_HOST__HEADER_POLICY_PARAMS, value) diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go index 061bf2952a..5bc2aaad8a 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go @@ -200,6 +200,119 @@ func TestLLMProviderTransformer_TransformProxy_ReadsProviderAndTemplateFromDB(t assert.Equal(t, "http://127.0.0.1:8080/db-provider", *result.Spec.Upstream.Main.Url) } +func TestLLMProviderTransformer_TransformProxy_AdditionalProviderAuthIsConditional(t *testing.T) { + store := storage.NewConfigStore() + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + db := newTestSQLiteStorage(t, logger) + + template := &models.StoredLLMProviderTemplate{ + UUID: "0000-db-template-id-0000-000000000002", + Configuration: api.LLMProviderTemplate{ + ApiVersion: api.LLMProviderTemplateApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderTemplateKindLlmProviderTemplate, + Metadata: api.Metadata{Name: "openai"}, + Spec: api.LLMProviderTemplateData{DisplayName: "openai"}, + }, + } + require.NoError(t, db.SaveLLMProviderTemplate(template)) + + saveProvider := func(name, context string) { + providerSourceConfig := api.LLMProviderConfiguration{ + ApiVersion: api.LLMProviderConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderConfigurationKindLlmProvider, + Metadata: api.Metadata{Name: name}, + Spec: api.LLMProviderConfigData{ + DisplayName: name, + Version: "v1.0", + Context: stringPtr(context), + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://example.com")}, + AccessControl: api.LLMAccessControl{Mode: api.AllowAll}, + }, + } + require.NoError(t, db.SaveConfig(&models.StoredConfig{ + UUID: name + "-uuid", + Kind: string(api.LLMProviderConfigurationKindLlmProvider), + Handle: name, + DisplayName: name, + Version: "v1.0", + SourceConfiguration: providerSourceConfig, + DesiredState: models.StateDeployed, + })) + } + saveProvider("openai-provider", "/openai-provider") + saveProvider("anthropic-provider", "/anthropic-provider") + + transformer := NewLLMProviderTransformer(store, db, &config.RouterConfig{ListenerPort: 8080}, newTestPolicyVersionResolver()) + + proxy := &api.LLMProxyConfiguration{ + ApiVersion: api.LLMProxyConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProxyConfigurationKindLlmProxy, + Metadata: api.Metadata{Name: "openai-multi"}, + Spec: api.LLMProxyConfigData{ + DisplayName: "openai-multi", + Version: "v1.0", + Provider: api.LLMProxyProvider{ + Id: "openai-provider", + Auth: &api.LLMUpstreamAuth{ + Type: api.LLMUpstreamAuthTypeApiKey, + Header: stringPtr("Authorization"), + Value: stringPtr("Bearer primary"), + }, + }, + AdditionalProviders: &[]api.LLMProxyAdditionalProvider{{ + Id: "anthropic-provider", + Auth: &api.LLMUpstreamAuth{ + Type: api.LLMUpstreamAuthTypeApiKey, + Header: stringPtr("X-Provider-Key"), + Value: stringPtr("anthropic-loopback"), + }, + }}, + Policies: &[]api.LLMPolicy{{ + Name: "openai-header-router", + Version: "v1", + Paths: []api.LLMPolicyPath{{ + Path: "/chat/completions", + Methods: []api.LLMPolicyPathMethods{"POST"}, + Params: map[string]interface{}{ + "defaultProvider": "openai-provider", + }, + }}, + }}, + }, + } + + result, err := transformer.Transform(proxy, &api.RestAPI{}) + require.NoError(t, err) + require.NotNil(t, result.Spec.UpstreamDefinitions) + require.Len(t, *result.Spec.UpstreamDefinitions, 1) + assert.Equal(t, "anthropic-provider", (*result.Spec.UpstreamDefinitions)[0].Name) + + var chatOp *api.Operation + for i := range result.Spec.Operations { + if result.Spec.Operations[i].Path == "/chat/completions" && result.Spec.Operations[i].Method == "POST" { + chatOp = &result.Spec.Operations[i] + break + } + } + require.NotNil(t, chatOp) + require.NotNil(t, chatOp.Policies) + + var authPolicies []api.Policy + for _, pol := range *chatOp.Policies { + if pol.Name == constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME { + authPolicies = append(authPolicies, pol) + } + } + require.Len(t, authPolicies, 2) + require.NotNil(t, authPolicies[0].ExecutionCondition) + require.NotNil(t, authPolicies[1].ExecutionCondition) + assert.Contains(t, *authPolicies[0].ExecutionCondition, "openai-provider") + assert.Contains(t, *authPolicies[1].ExecutionCondition, "anthropic-provider") + assert.Equal(t, "Bearer primary", firstRequestHeaderValue(t, authPolicies[0].Params)) + assert.Equal(t, "anthropic-loopback", firstRequestHeaderValue(t, authPolicies[1].Params)) +} + func TestGetUpstreamAuthApikeyPolicyParams_Extended(t *testing.T) { t.Run("Valid parameters", func(t *testing.T) { params, err := GetUpstreamAuthApikeyPolicyParams("Authorization", "Bearer token123") @@ -215,6 +328,21 @@ func TestGetUpstreamAuthApikeyPolicyParams_Extended(t *testing.T) { }) } +func firstRequestHeaderValue(t *testing.T, params *map[string]interface{}) string { + t.Helper() + require.NotNil(t, params) + request, ok := (*params)["request"].(map[string]interface{}) + require.True(t, ok) + headers, ok := request["headers"].([]interface{}) + require.True(t, ok) + require.NotEmpty(t, headers) + header, ok := headers[0].(map[string]interface{}) + require.True(t, ok) + value, ok := header["value"].(string) + require.True(t, ok) + return value +} + func TestGetHostAdditionPolicyParams(t *testing.T) { t.Run("Valid host value", func(t *testing.T) { params, err := GetHostAdditionPolicyParams("api.example.com") diff --git a/gateway/sample-policies/slugify-body/policy-definition.yaml b/gateway/sample-policies/slugify-body/policy-definition.yaml index f19a90c78c..68882e2e99 100644 --- a/gateway/sample-policies/slugify-body/policy-definition.yaml +++ b/gateway/sample-policies/slugify-body/policy-definition.yaml @@ -27,22 +27,9 @@ name: slugify-body version: v1.0.0 displayName: Slugify Body description: | - Extracts a string value from the JSON request body at the configured JSONPath, - converts it into a URL-friendly slug using the `python-slugify` library, and - writes the slug back into the payload before forwarding to the upstream - service. - - Typical use cases include: - - Generating URL slugs from user-supplied titles or names. - - Normalising free-text identifiers into safe, lowercase, ASCII strings. - - Cleaning up input data at the gateway layer before it reaches backends. - - This is a **sample policy** that demonstrates: - 1. Using a third-party PyPI dependency (`python-slugify`). - 2. Reading and modifying JSON request bodies (buffered mode). - 3. Declaring configurable parameters with JSON Schema validation. - 4. Returning modified request bodies via `UpstreamRequestModifications`. - 5. Returning early error responses via `ImmediateResponse`. + Extracts a string from the JSON request body, converts it into a URL-friendly + slug, and writes the slug back before forwarding upstream. Demonstrates a + Python policy with third-party dependencies and JSON Schema parameters. # --- User-configurable parameters -------------------------------------------- # The `parameters` section is a full JSON Schema object. Every key under diff --git a/gateway/system-policies/analytics/policy-definition.yaml b/gateway/system-policies/analytics/policy-definition.yaml index 7fe915469d..1a57a8bf06 100644 --- a/gateway/system-policies/analytics/policy-definition.yaml +++ b/gateway/system-policies/analytics/policy-definition.yaml @@ -1,10 +1,9 @@ name: wso2_apip_sys_analytics version: v1.0.0 description: | - Collects analytics data from request and response flows. This gets attached even - without any policies in the chain. Inspects headers and, optionally, bodies to - populate analytics metadata that is emitted via Envoy dynamic metadata and consumed - by the policy engine analytics pipeline and publishers. + Collects request and response analytics metadata from gateway traffic, + including optional payload capture, for the policy engine analytics pipeline + and publishers. parameters: type: object diff --git a/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go b/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go index 046f492af5..b4b29e2ee4 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go +++ b/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go @@ -32,6 +32,23 @@ type LLMProxyProvider struct { Auth *LLMUpstreamAuth `json:"auth,omitempty"` } +// LLMProxyAdditionalProvider references an additional LlmProvider that this +// proxy can route to by policy-selected upstream name. +type LLMProxyAdditionalProvider struct { + // Id is the LlmProvider handle (metadata.name). + // +kubebuilder:validation:Required + Id string `json:"id"` + + // As is the logical upstream name used by policies. Defaults to Id. + // +optional + As *string `json:"as,omitempty"` + + // Auth optionally configures credentials for proxy-to-provider loopback + // calls when the referenced provider is protected by an auth policy. + // +optional + Auth *LLMUpstreamAuth `json:"auth,omitempty"` +} + // LLMProxyConfigData mirrors the management-API LLMProxyConfigData payload. type LLMProxyConfigData struct { // DisplayName is a human-readable LLM proxy name. @@ -47,6 +64,11 @@ type LLMProxyConfigData struct { // +kubebuilder:validation:Required Provider LLMProxyProvider `json:"provider"` + // AdditionalProviders are extra LLM providers attached as selectable + // upstreams for multi-provider routing. + // +optional + AdditionalProviders []LLMProxyAdditionalProvider `json:"additionalProviders,omitempty"` + // Context is the base path for routes (must start with /). // +optional // +kubebuilder:validation:Pattern=`^/[a-zA-Z0-9\-._~!$&'()*+,;=:@%/]*[^/]$` diff --git a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go index b6163d5fd5..cfeda2ef43 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -965,6 +965,13 @@ func (in *LLMProviderUpstream) DeepCopy() *LLMProviderUpstream { func (in *LLMProxyConfigData) DeepCopyInto(out *LLMProxyConfigData) { *out = *in in.Provider.DeepCopyInto(&out.Provider) + if in.AdditionalProviders != nil { + in, out := &in.AdditionalProviders, &out.AdditionalProviders + *out = make([]LLMProxyAdditionalProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Context != nil { in, out := &in.Context, &out.Context *out = new(string) @@ -999,6 +1006,31 @@ func (in *LLMProxyConfigData) DeepCopy() *LLMProxyConfigData { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LLMProxyAdditionalProvider) DeepCopyInto(out *LLMProxyAdditionalProvider) { + *out = *in + if in.As != nil { + in, out := &in.As, &out.As + *out = new(string) + **out = **in + } + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(LLMUpstreamAuth) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LLMProxyAdditionalProvider. +func (in *LLMProxyAdditionalProvider) DeepCopy() *LLMProxyAdditionalProvider { + if in == nil { + return nil + } + out := new(LLMProxyAdditionalProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LLMProxyProvider) DeepCopyInto(out *LLMProxyProvider) { *out = *in diff --git a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml index dd5a2a5579..db0259fde6 100644 --- a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml +++ b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml @@ -55,6 +55,85 @@ spec: displayName: description: DisplayName is a human-readable LLM proxy name. type: string + additionalProviders: + description: |- + AdditionalProviders are extra LLM providers attached as selectable + upstreams for multi-provider routing. + items: + description: |- + LLMProxyAdditionalProvider references an additional LlmProvider that this + proxy can route to by policy-selected upstream name. + properties: + as: + description: As is the logical upstream name used by policies. + Defaults to Id. + type: string + auth: + description: |- + Auth optionally configures credentials for proxy-to-provider loopback + calls when the referenced provider is protected by an auth policy. + properties: + header: + description: |- + Header is the HTTP header to set on outbound requests when the auth + type uses headers (e.g. api-key). + type: string + type: + description: Type identifies the auth scheme. Only api-key + is currently supported. + enum: + - api-key + type: string + value: + description: |- + Value sources the credential. Exactly one of value or valueFrom must + be set. + properties: + value: + description: |- + Value is the inline plaintext value. Avoid for production use; prefer + valueFrom. + type: string + valueFrom: + description: ValueFrom references a Kubernetes Secret key. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: exactly one of value or valueFrom must be + set + rule: has(self.value) != has(self.valueFrom) + required: + - type + - value + type: object + id: + description: Id is the LlmProvider handle (metadata.name). + type: string + required: + - id + type: object + type: array policies: description: Policies is the list of policies applied to this LLM proxy. diff --git a/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go b/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go index 987b84a056..2d68ea5986 100644 --- a/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go +++ b/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go @@ -159,20 +159,38 @@ func (a *llmProxyAdapter) Deploy(ctx context.Context, k8sClient client.Client, g } specPayload := interface{}(spec) - if spec.Provider.Auth != nil { - resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.Provider.Auth, "spec.provider.auth.value") - if err != nil { - return DeployResult{}, err - } - if resolved == nil { - return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: provider.auth resolution produced nil")} - } + if spec.Provider.Auth != nil || len(spec.AdditionalProviders) > 0 { m, err := specToJSONMap(spec) if err != nil { return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} } - if err := flattenUpstreamAuthCredentialValue(m, "provider", *resolved); err != nil { - return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + if spec.Provider.Auth != nil { + resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.Provider.Auth, "spec.provider.auth.value") + if err != nil { + return DeployResult{}, err + } + if resolved == nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: provider.auth resolution produced nil")} + } + if err := flattenUpstreamAuthCredentialValue(m, "provider", *resolved); err != nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + } + } + for i := range spec.AdditionalProviders { + if spec.AdditionalProviders[i].Auth == nil { + continue + } + fieldPath := fmt.Sprintf("spec.additionalProviders[%d].auth.value", i) + resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.AdditionalProviders[i].Auth, fieldPath) + if err != nil { + return DeployResult{}, err + } + if resolved == nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: additionalProviders[%d].auth resolution produced nil", i)} + } + if err := flattenAdditionalProviderAuthCredentialValue(m, i, *resolved); err != nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + } } specPayload = m } diff --git a/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go b/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go index 6703f9c3dd..f6cec1c13c 100644 --- a/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go +++ b/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go @@ -93,3 +93,85 @@ func TestLlmProxyDeploy_ResolvesPolicyParamsValueFrom(t *testing.T) { require.True(t, strings.Contains(got, "name: api-key-auth")) } +func TestLlmProxyDeploy_ResolvesAdditionalProviderAuthValueFrom(t *testing.T) { + scheme := runtime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(apiv1.AddToScheme(scheme)) + + sec := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: "apigateway-demo", Name: "provider-secrets"}, + Data: map[string][]byte{ + "primary": []byte("Bearer primary-key"), + "additional": []byte("Bearer additional-key"), + }, + } + k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sec).Build() + + header := "Authorization" + authType := "api-key" + cr := &apiv1.LlmProxy{ + ObjectMeta: metav1.ObjectMeta{Name: "demo-llm-proxy", Namespace: "apigateway-demo"}, + Spec: apiv1.LLMProxyConfigData{ + DisplayName: "Lifecycle LLM Proxy", + Version: "v1.0", + Provider: apiv1.LLMProxyProvider{ + Id: "openai-provider", + Auth: &apiv1.LLMUpstreamAuth{ + Type: authType, + Header: &header, + Value: apiv1.SecretValueSource{ValueFrom: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "provider-secrets"}, + Key: "primary", + }}, + }, + }, + AdditionalProviders: []apiv1.LLMProxyAdditionalProvider{{ + Id: "anthropic-provider", + Auth: &apiv1.LLMUpstreamAuth{ + Type: authType, + Header: &header, + Value: apiv1.SecretValueSource{ValueFrom: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "provider-secrets"}, + Key: "additional", + }}, + }, + }}, + }, + } + + var ( + mu sync.Mutex + payload string + ) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + if r.Method == http.MethodPost { + b, _ := io.ReadAll(r.Body) + mu.Lock() + payload = string(b) + mu.Unlock() + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("{}")) + return + } + w.WriteHeader(http.StatusMethodNotAllowed) + })) + defer srv.Close() + + adapter := &llmProxyAdapter{} + _, err := adapter.Deploy(context.Background(), k8sClient, srv.URL, cr, nil) + require.NoError(t, err) + + mu.Lock() + got := payload + mu.Unlock() + require.Contains(t, got, "value: Bearer primary-key") + require.Contains(t, got, "value: Bearer additional-key") + require.Contains(t, got, "additionalProviders:") + require.NotContains(t, got, "secretKeyRef") + require.NotContains(t, got, "valueFrom") +} diff --git a/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go b/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go index e32c01e772..ffe5f609f9 100644 --- a/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go +++ b/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go @@ -42,3 +42,23 @@ func flattenUpstreamAuthCredentialValue(specMap map[string]interface{}, parentKe auth["value"] = plaintext return nil } + +func flattenAdditionalProviderAuthCredentialValue(specMap map[string]interface{}, index int, plaintext string) error { + additionalProviders, ok := specMap["additionalProviders"].([]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders must be an array") + } + if index < 0 || index >= len(additionalProviders) { + return fmt.Errorf("spec.additionalProviders[%d] is out of range", index) + } + parent, ok := additionalProviders[index].(map[string]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders[%d] must be an object", index) + } + auth, ok := parent["auth"].(map[string]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders[%d].auth must be an object", index) + } + auth["value"] = plaintext + return nil +} diff --git a/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go b/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go index 92e26fe455..7760063f47 100644 --- a/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go +++ b/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go @@ -218,6 +218,14 @@ func llmProxyReferencesSecret(cr *apiv1.LlmProxy, secretNS, secretName string) b return true } } + for i := range cr.Spec.AdditionalProviders { + auth := cr.Spec.AdditionalProviders[i].Auth + if auth != nil && auth.Value.ValueFrom != nil { + if cr.Namespace == secretNS && strings.TrimSpace(auth.Value.ValueFrom.Name) == secretName { + return true + } + } + } return llmProxyReferencesValueFromKind(cr, secretKeyRefKey, secretNS, secretName) } @@ -240,4 +248,3 @@ func llmProxyReferencesValueFromKind(cr *apiv1.LlmProxy, kind, targetNS, targetN } return false } - diff --git a/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go b/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go index 2c6a19068a..16bd4a42dd 100644 --- a/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go +++ b/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go @@ -110,6 +110,11 @@ func llmProxyExternalDepsFingerprint(ctx context.Context, c client.Client, cr *a if cr.Spec.Provider.Auth != nil { accumulateBackingFromSecretValueSource(cr.Spec.Provider.Auth.Value, cr.Namespace, backing) } + for i := range cr.Spec.AdditionalProviders { + if cr.Spec.AdditionalProviders[i].Auth != nil { + accumulateBackingFromSecretValueSource(cr.Spec.AdditionalProviders[i].Auth.Value, cr.Namespace, backing) + } + } for i := range cr.Spec.Policies { for j := range cr.Spec.Policies[i].Paths { if p := cr.Spec.Policies[i].Paths[j].Params; p != nil { @@ -139,4 +144,3 @@ func resolveRawExtensionValueFrom(ctx context.Context, c client.Client, defaultN raw.Raw = out return nil } - diff --git a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml index dd5a2a5579..db0259fde6 100644 --- a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml +++ b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml @@ -55,6 +55,85 @@ spec: displayName: description: DisplayName is a human-readable LLM proxy name. type: string + additionalProviders: + description: |- + AdditionalProviders are extra LLM providers attached as selectable + upstreams for multi-provider routing. + items: + description: |- + LLMProxyAdditionalProvider references an additional LlmProvider that this + proxy can route to by policy-selected upstream name. + properties: + as: + description: As is the logical upstream name used by policies. + Defaults to Id. + type: string + auth: + description: |- + Auth optionally configures credentials for proxy-to-provider loopback + calls when the referenced provider is protected by an auth policy. + properties: + header: + description: |- + Header is the HTTP header to set on outbound requests when the auth + type uses headers (e.g. api-key). + type: string + type: + description: Type identifies the auth scheme. Only api-key + is currently supported. + enum: + - api-key + type: string + value: + description: |- + Value sources the credential. Exactly one of value or valueFrom must + be set. + properties: + value: + description: |- + Value is the inline plaintext value. Avoid for production use; prefer + valueFrom. + type: string + valueFrom: + description: ValueFrom references a Kubernetes Secret key. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: exactly one of value or valueFrom must be + set + rule: has(self.value) != has(self.valueFrom) + required: + - type + - value + type: object + id: + description: Id is the LlmProvider handle (metadata.name). + type: string + required: + - id + type: object + type: array policies: description: Policies is the list of policies applied to this LLM proxy. diff --git a/mistral-proxy.yaml b/mistral-proxy.yaml new file mode 100644 index 0000000000..536a17c5b8 --- /dev/null +++ b/mistral-proxy.yaml @@ -0,0 +1,246 @@ +# ==================================================================== +# Mistral — Provider + Single Proxy + Consumer Key (docs 1-4) +# Plus a MULTI-PROVIDER proxy sample at the bottom (docs 5-6) +# +# THREE auth hops exist — keep them straight: +# (A) provider.upstream.auth -> provider -> Mistral (the Mistral key) +# (B) proxy api-key-auth -> client -> proxy (consumer key) +# (C) proxy.provider.auth -> proxy -> provider (loopback, optional) +# +# For every auth block: type MUST be "api-key" (never "x-api-key"); +# "x-api-key" is only ever a HEADER NAME, never a type. value non-empty. +# ==================================================================== + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: mistral-provider +spec: + displayName: Mistral Provider + version: v1.0 + template: mistralai + upstream: + url: https://api.mistral.ai + auth: + type: api-key # (A) only valid type for an LlmProvider + header: Authorization # Mistral expects: Authorization: Bearer + value: Bearer # <-- put your real Mistral key here + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/chat/completions + methods: [POST] + - path: /v1/embeddings + methods: [POST] + - path: /v1/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: mistral-provider + displayName: Mistral provider loopback key + apiKey: + value: mistral_provider_loopback_key_1234567890abcdef + maskedApiKey: mistral_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days + +--- +# Second provider used only by the MULTI-PROVIDER sample below. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: anthropic-provider +spec: + displayName: Anthropic Provider + version: v1.0 + template: anthropic + upstream: + url: https://api.anthropic.com + auth: + type: api-key # type is still "api-key"... + header: x-api-key # ...but Anthropic's HEADER is x-api-key (no Bearer) + value: + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/messages + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/messages + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: anthropic-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: anthropic-provider + displayName: Anthropic provider loopback key + apiKey: + value: anthropic_provider_loopback_key_1234567890abcdef + maskedApiKey: anthropic_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days + +--- +# ----- SINGLE-provider proxy ----------------------------------------- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-mistral +spec: + displayName: OpenAI to Mistral Proxy + version: v1.0 + context: /openai-mistral + provider: + id: mistral-provider + # (C) OPTIONAL proxy->provider loopback auth. A SEPARATE hop, NOT the + # Mistral key. Only needed if the provider API itself is secured. + # Corrected form: type "api-key" (NOT "x-api-key"), value non-empty. + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + policies: + - name: api-key-auth # (B) client -> proxy consumer key + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + +--- +# Consumer key for the single proxy. spec.apiKey omitted -> gateway generates it. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-to-mistral + displayName: Mistral proxy consumer key + expiresIn: + duration: 30 + unit: days + +--- +# ----- MULTI-provider proxy sample ----------------------------------- +# One OpenAI-shaped endpoint that fans out to Mistral OR Anthropic based +# on the `x-provider` request header (read by the openai-header-router +# policy). Each provider has its own LlmProvider ApiKey above; the proxy +# sends those loopback keys via provider.auth/additionalProviders[].auth. +# +# Routing: header x-provider: mistral -> mistral-provider +# header x-provider: anthropic -> anthropic-provider +# (absent/unmatched) -> defaultProvider (mistral) +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-multi +spec: + displayName: OpenAI Multi-Provider Proxy + version: v1.0 + context: /openai-multi + provider: + id: mistral-provider # primary/default provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + additionalProviders: + - id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + # 1) Selector: reads x-provider header, picks the provider. + - name: openai-header-router + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + headerName: x-provider + defaultProvider: mistral-provider + mappings: + - headerValue: mistral + provider: mistral-provider + - headerValue: anthropic + provider: anthropic-provider + # 2) Per-provider translators. Each runs only when its id is selected. + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + id: mistral-provider + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 + id: anthropic-provider + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-multi-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-multi + displayName: OpenAI multi-proxy consumer key + expiresIn: + duration: 30 + unit: days diff --git a/platform-api/src/api/generated.go b/platform-api/src/api/generated.go index 2420211e64..05a3d25c8a 100644 --- a/platform-api/src/api/generated.go +++ b/platform-api/src/api/generated.go @@ -1812,6 +1812,9 @@ type LLMProviderTemplateResourceMappings struct { // LLMProxy defines model for LLMProxy. type LLMProxy struct { + // AdditionalProviders Optional list of additional LLM providers attached to this proxy as selectable upstreams. Policies route requests to any of these by setting the upstream name. The primary `provider` field above remains the default upstream and the FK target. + AdditionalProviders *[]LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" yaml:"additionalProviders,omitempty"` + // Context Base path for all REST API routes (must start with /, no trailing slash) Context *string `json:"context,omitempty" yaml:"context,omitempty"` @@ -1862,6 +1865,15 @@ type LLMProxyAPIKeyListResponse struct { Items []APIKeyItem `binding:"required" json:"items" yaml:"items"` } +// LLMProxyAdditionalProvider Additional LLM provider attached to this proxy as a selectable upstream. Policies route to it by referring to the `as` name (defaults to `id`). +type LLMProxyAdditionalProvider struct { + // As Logical upstream name used by policies to select this provider. Must be unique within the proxy. Defaults to `id` when omitted. + As *string `json:"as,omitempty" yaml:"as,omitempty"` + + // Id Unique id of a deployed llm provider + Id string `binding:"required" json:"id" yaml:"id"` +} + // LLMProxyListItem defines model for LLMProxyListItem. type LLMProxyListItem struct { // Context Context path where the proxy is exposed diff --git a/platform-api/src/internal/dto/llm_deployment.go b/platform-api/src/internal/dto/llm_deployment.go index 9fc0c7a8a6..f8891ee962 100644 --- a/platform-api/src/internal/dto/llm_deployment.go +++ b/platform-api/src/internal/dto/llm_deployment.go @@ -59,15 +59,21 @@ type LLMProxyDeploymentYAML struct { // LLMProxyDeploymentSpec represents the spec section for LLM proxy deployments type LLMProxyDeploymentSpec struct { - DisplayName string `yaml:"displayName"` - Version string `yaml:"version"` - Context string `yaml:"context,omitempty"` - VHost string `yaml:"vhost,omitempty"` - Provider LLMProxyDeploymentProvider `yaml:"provider"` - Policies []api.LLMPolicy `yaml:"policies,omitempty"` + DisplayName string `yaml:"displayName"` + Version string `yaml:"version"` + Context string `yaml:"context,omitempty"` + VHost string `yaml:"vhost,omitempty"` + Provider LLMProxyDeploymentProvider `yaml:"provider"` + AdditionalProviders []LLMProxyDeploymentAdditionalProvider `yaml:"additionalProviders,omitempty"` + Policies []api.LLMPolicy `yaml:"policies,omitempty"` } type LLMProxyDeploymentProvider struct { ID string `yaml:"id"` Auth *api.UpstreamAuth `yaml:"auth,omitempty"` } + +type LLMProxyDeploymentAdditionalProvider struct { + ID string `yaml:"id"` + As string `yaml:"as,omitempty"` +} diff --git a/platform-api/src/internal/model/llm.go b/platform-api/src/internal/model/llm.go index 2332da5638..0b24f12e64 100644 --- a/platform-api/src/internal/model/llm.go +++ b/platform-api/src/internal/model/llm.go @@ -207,14 +207,24 @@ type LLMProxy struct { } type LLMProxyConfig struct { - Name string `json:"name,omitempty" db:"-"` - Version string `json:"version,omitempty" db:"-"` - Context *string `json:"context,omitempty" db:"-"` - Vhost *string `json:"vhost,omitempty" db:"-"` - Provider string `json:"provider,omitempty" db:"-"` - UpstreamAuth *UpstreamAuth `json:"upstreamAuth,omitempty" db:"-"` - Policies []LLMPolicy `json:"policies,omitempty" db:"-"` - Security *SecurityConfig `json:"security,omitempty" db:"-"` + Name string `json:"name,omitempty" db:"-"` + Version string `json:"version,omitempty" db:"-"` + Context *string `json:"context,omitempty" db:"-"` + Vhost *string `json:"vhost,omitempty" db:"-"` + Provider string `json:"provider,omitempty" db:"-"` + UpstreamAuth *UpstreamAuth `json:"upstreamAuth,omitempty" db:"-"` + AdditionalProviders []LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" db:"-"` + Policies []LLMPolicy `json:"policies,omitempty" db:"-"` + Security *SecurityConfig `json:"security,omitempty" db:"-"` +} + +// LLMProxyAdditionalProvider is an additional LLM provider attached to a proxy +// as a selectable upstream. Policies route to it by the `As` name (which +// defaults to `ID` when empty). Provider upstream auth is taken from the +// referenced LlmProvider's own configuration. +type LLMProxyAdditionalProvider struct { + ID string `json:"id" db:"-"` + As string `json:"as,omitempty" db:"-"` } type SecurityConfig struct { diff --git a/platform-api/src/internal/service/llm.go b/platform-api/src/internal/service/llm.go index 1c112626e8..2ed710213b 100644 --- a/platform-api/src/internal/service/llm.go +++ b/platform-api/src/internal/service/llm.go @@ -630,12 +630,13 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( OpenAPISpec: utils.ValueOrEmpty(req.Openapi), Status: llmStatusPending, Configuration: model.LLMProxyConfig{ - Context: &contextValue, - Vhost: req.Vhost, - Provider: req.Provider.Id, - UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), - Policies: mapPoliciesAPIToModel(req.Policies), - Security: mapSecurityAPIToModel(req.Security), + Context: &contextValue, + Vhost: req.Vhost, + Provider: req.Provider.Id, + UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), + AdditionalProviders: mapAdditionalProvidersAPIToModel(req.AdditionalProviders), + Policies: mapPoliciesAPIToModel(req.Policies), + Security: mapSecurityAPIToModel(req.Security), }, } @@ -842,12 +843,13 @@ func (s *LLMProxyService) Update(orgUUID, handle string, req *api.LLMProxy) (*ap OpenAPISpec: utils.ValueOrEmpty(req.Openapi), Status: llmStatusPending, Configuration: model.LLMProxyConfig{ - Context: &contextValue, - Vhost: req.Vhost, - Provider: req.Provider.Id, - UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), - Policies: mapPoliciesAPIToModel(req.Policies), - Security: mapSecurityAPIToModel(req.Security), + Context: &contextValue, + Vhost: req.Vhost, + Provider: req.Provider.Id, + UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), + AdditionalProviders: mapAdditionalProvidersAPIToModel(req.AdditionalProviders), + Policies: mapPoliciesAPIToModel(req.Policies), + Security: mapSecurityAPIToModel(req.Security), }, } @@ -1048,6 +1050,36 @@ func mapUpstreamAuthAPIToModel(in *api.UpstreamAuth) *model.UpstreamAuth { } } +func mapAdditionalProvidersAPIToModel(in *[]api.LLMProxyAdditionalProvider) []model.LLMProxyAdditionalProvider { + if in == nil || len(*in) == 0 { + return nil + } + out := make([]model.LLMProxyAdditionalProvider, 0, len(*in)) + for _, p := range *in { + out = append(out, model.LLMProxyAdditionalProvider{ + ID: p.Id, + As: utils.ValueOrEmpty(p.As), + }) + } + return out +} + +func mapAdditionalProvidersModelToAPI(in []model.LLMProxyAdditionalProvider) *[]api.LLMProxyAdditionalProvider { + if len(in) == 0 { + return nil + } + out := make([]api.LLMProxyAdditionalProvider, 0, len(in)) + for _, p := range in { + entry := api.LLMProxyAdditionalProvider{Id: p.ID} + if p.As != "" { + as := p.As + entry.As = &as + } + out = append(out, entry) + } + return &out +} + func normalizeUpstreamAuthType(authType string) string { normalized := strings.TrimSpace(authType) if normalized == "" { @@ -1857,6 +1889,9 @@ func mapProxyModelToAPI(m *model.LLMProxy) *api.LLMProxy { Value: nil, // Redact auth credential value } } + if extra := mapAdditionalProvidersModelToAPI(m.Configuration.AdditionalProviders); extra != nil { + out.AdditionalProviders = extra + } if len(m.Configuration.Policies) > 0 { policyList := make([]api.LLMPolicy, 0, len(m.Configuration.Policies)) for _, p := range m.Configuration.Policies { diff --git a/platform-api/src/internal/service/llm_deployment.go b/platform-api/src/internal/service/llm_deployment.go index 29cfa6523f..9c5edba7e2 100644 --- a/platform-api/src/internal/service/llm_deployment.go +++ b/platform-api/src/internal/service/llm_deployment.go @@ -1681,7 +1681,8 @@ func generateLLMProxyDeploymentYAML(proxy *model.LLMProxy) (string, error) { Provider: dto.LLMProxyDeploymentProvider{ ID: proxy.Configuration.Provider, }, - Policies: policies, + AdditionalProviders: mapAdditionalProvidersModelToDeployment(proxy.Configuration.AdditionalProviders), + Policies: policies, }, } @@ -1719,6 +1720,20 @@ func mapModelUpstreamAuthToAPI(auth *model.UpstreamAuth) *api.UpstreamAuth { return mapModelAuthToAPI(auth) } +func mapAdditionalProvidersModelToDeployment(in []model.LLMProxyAdditionalProvider) []dto.LLMProxyDeploymentAdditionalProvider { + if len(in) == 0 { + return nil + } + out := make([]dto.LLMProxyDeploymentAdditionalProvider, 0, len(in)) + for _, p := range in { + out = append(out, dto.LLMProxyDeploymentAdditionalProvider{ + ID: p.ID, + As: p.As, + }) + } + return out +} + // orderLLMPolicies ensures llm-cost-based-ratelimit always precedes llm-cost in the policy list. // All other policies retain their relative positions. func orderLLMPolicies(policies []api.LLMPolicy) []api.LLMPolicy { diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index f49addf529..d7db41261f 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -10266,6 +10266,15 @@ components: example: "api.openai" provider: $ref: '#/components/schemas/LLMProxyProvider' + additionalProviders: + type: array + description: > + Optional list of additional LLM providers attached to this proxy as + selectable upstreams. Policies route requests to any of these by + setting the upstream name. The primary `provider` field above + remains the default upstream and the FK target. + items: + $ref: '#/components/schemas/LLMProxyAdditionalProvider' openapi: type: string description: OpenAPI specification (JSON or YAML) for the proxy endpoint @@ -10370,6 +10379,28 @@ components: auth: $ref: '#/components/schemas/UpstreamAuth' + LLMProxyAdditionalProvider: + type: object + required: + - id + description: > + Additional LLM provider attached to this proxy as a selectable upstream. + Policies route to it by referring to the `as` name (defaults to `id`). + properties: + id: + type: string + description: Unique id of a deployed llm provider + example: anthropic-provider + as: + type: string + description: > + Logical upstream name used by policies to select this provider. Must + be unique within the proxy. Defaults to `id` when omitted. + pattern: '^[a-zA-Z0-9\-_]+$' + minLength: 1 + maxLength: 100 + example: anthropic-upstream + CreateLLMProviderAPIKeyRequest: type: object properties: From 36db0520a011091684bee70289ac6be41da3941f Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Mon, 22 Jun 2026 13:52:34 +0530 Subject: [PATCH 2/9] feat(policy): add openai-to-gemini translation policy with request and response transformations --- .../default-policies/openai-to-gemini.yaml | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 event-gateway/default-policies/openai-to-gemini.yaml diff --git a/event-gateway/default-policies/openai-to-gemini.yaml b/event-gateway/default-policies/openai-to-gemini.yaml new file mode 100644 index 0000000000..48fe4b6fb9 --- /dev/null +++ b/event-gateway/default-policies/openai-to-gemini.yaml @@ -0,0 +1,86 @@ +name: openai-to-gemini +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the Gemini + generateContent API format and forwards it to a Gemini-compatible + upstream. + + Request transformations: + - system/developer messages are lifted into the top-level + "systemInstruction" field. + - messages[] is converted to Gemini contents[] with role/parts blocks; + assistant tool_calls become functionCall parts and tool-role messages + become functionResponse parts. + - tools[] are converted to functionDeclarations and tool_choice to a + functionCallingConfig. + - sampling fields (temperature, top_p, max_tokens, stop, etc.) are mapped + into generationConfig; image_url parts become inlineData/fileData. + - The request path is rewritten to + /{apiVersion}/models/{model}:generateContent. + + Response transformations: + - The Gemini generateContent response body is rewritten into the OpenAI + ChatCompletion shape (id, object, created, model, choices, usage). + - text parts become choice.message.content; functionCall parts become + choice.message.tool_calls; finishReason is mapped to finish_reason and + usageMetadata token counts are carried in usage. + - Streaming (Server-Sent Events) responses are passed through untouched + in this policy version; clients that need true streaming should request + non-streaming (omit "stream": true) until a streaming variant ships. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests + pass through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Gemini API key must be configured on the upstream + LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Gemini model name used in the translated request (for example + gemini-2.5-pro). Overrides the OpenAI "model" field and is used in + the rewritten path. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + apiVersion: + type: string + x-wso2-policy-advanced-param: true + description: | + Gemini API version segment used in the rewritten path + (/{apiVersion}/models/{model}:generateContent). + default: "v1beta" + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} \ No newline at end of file From f871799fc5501e83e0a2d81b83c2357c7d3e8f91 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Mon, 22 Jun 2026 14:58:30 +0530 Subject: [PATCH 3/9] added build golang build file --- .../pkg/api/management/generated.go | 549 +++++++++--------- 1 file changed, 277 insertions(+), 272 deletions(-) diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index 2eb32544e0..fc5adeac85 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -5207,278 +5207,283 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "h8xIt8//JwAA//9q4TK55lQCAA==", + "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", + "9UNmpNvn/xMAAP//sKJAsX9ZAgA=", } // GetSwagger returns the content of the embedded swagger specification file From 7342ca6d114fbaf33d23c79ec76f7a49353cfd4f Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Tue, 23 Jun 2026 10:09:47 +0530 Subject: [PATCH 4/9] fix(examples): align api-key-auth paths with accessControl exceptions in provider examples All allowed paths in accessControl.exceptions now have matching api-key-auth entries so loopback callers must present an API key for every exposed endpoint, not just chat/completions. --- gateway/examples/azure-openai-provider.yaml | 15 +++++++++++++++ gateway/examples/mistral-provider.yaml | 10 ++++++++++ gateway/examples/openai-provider.yaml | 20 ++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/gateway/examples/azure-openai-provider.yaml b/gateway/examples/azure-openai-provider.yaml index e1345ac3c4..54f227392f 100644 --- a/gateway/examples/azure-openai-provider.yaml +++ b/gateway/examples/azure-openai-provider.yaml @@ -37,6 +37,21 @@ spec: params: key: X-API-Key in: header + - path: /openai/deployments/{deployment}/completions + methods: [POST] + params: + key: X-API-Key + in: header + - path: /openai/deployments/{deployment}/embeddings + methods: [POST] + params: + key: X-API-Key + in: header + - path: /openai/models + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: diff --git a/gateway/examples/mistral-provider.yaml b/gateway/examples/mistral-provider.yaml index 67ec1c2654..18a96bebdd 100644 --- a/gateway/examples/mistral-provider.yaml +++ b/gateway/examples/mistral-provider.yaml @@ -35,6 +35,16 @@ spec: params: key: X-API-Key in: header + - path: /v1/embeddings + methods: [POST] + params: + key: X-API-Key + in: header + - path: /v1/models + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: diff --git a/gateway/examples/openai-provider.yaml b/gateway/examples/openai-provider.yaml index b206cff3f2..483a5a3c42 100644 --- a/gateway/examples/openai-provider.yaml +++ b/gateway/examples/openai-provider.yaml @@ -35,6 +35,26 @@ spec: params: key: X-API-Key in: header + - path: /completions + methods: [POST] + params: + key: X-API-Key + in: header + - path: /embeddings + methods: [POST] + params: + key: X-API-Key + in: header + - path: /models + methods: [GET] + params: + key: X-API-Key + in: header + - path: /models/{modelId} + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: From 601bc8482a9b12c84001651a4388602a044a40d6 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Tue, 23 Jun 2026 10:41:06 +0530 Subject: [PATCH 5/9] fix(examples): add missing loopback auth and align api-key-auth with access control exceptions - azure-openai-proxy: add provider.auth so the proxy can authenticate against the provider's api-key-auth policy on the loopback hop - gemini-provider: add streamGenerateContent to api-key-auth paths to match the accessControl exceptions list --- gateway/examples/azure-openai-proxy.yaml | 4 ++++ gateway/examples/gemini-provider.yaml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/gateway/examples/azure-openai-proxy.yaml b/gateway/examples/azure-openai-proxy.yaml index 0466a62e72..96199518ab 100644 --- a/gateway/examples/azure-openai-proxy.yaml +++ b/gateway/examples/azure-openai-proxy.yaml @@ -26,6 +26,10 @@ spec: context: /azure-openai provider: id: azure-openai-provider + auth: + type: api-key + header: X-API-Key + value: azure_openai_provider_loopback_key_1234567890abcdef policies: - name: openai-to-azure-openai version: v1 diff --git a/gateway/examples/gemini-provider.yaml b/gateway/examples/gemini-provider.yaml index 833bc962e5..8abcaccd05 100644 --- a/gateway/examples/gemini-provider.yaml +++ b/gateway/examples/gemini-provider.yaml @@ -32,6 +32,11 @@ spec: params: key: X-API-Key in: header + - path: /v1beta/models/{model}:streamGenerateContent + methods: [POST] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: From a8ca55291b325359fb87498e7ea84d05e0e54e1e Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Tue, 23 Jun 2026 11:11:55 +0530 Subject: [PATCH 6/9] fix(examples): fix azure-openai-provider url placeholder and loopback auth, align gemini api-key-auth paths - azure-openai-provider: replace null url comment with a valid placeholder - azure-openai-proxy: add missing provider.auth for loopback authentication - gemini-provider: add streamGenerateContent to api-key-auth to match accessControl exceptions --- gateway/examples/azure-openai-provider.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/examples/azure-openai-provider.yaml b/gateway/examples/azure-openai-provider.yaml index 54f227392f..c1e5e998d3 100644 --- a/gateway/examples/azure-openai-provider.yaml +++ b/gateway/examples/azure-openai-provider.yaml @@ -23,7 +23,7 @@ spec: version: v1.0 template: azure-openai upstream: - url: #endpoint url + url: https://REPLACE_WITH_AZURE_OPENAI_ENDPOINT auth: type: api-key header: api-key From 918f345975ac295eb514cac1c36660427c07ca68 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Tue, 23 Jun 2026 11:16:35 +0530 Subject: [PATCH 7/9] fix(examples): replace dev placeholder with Bearer-formatted API key in mistral-provider --- gateway/examples/mistral-provider.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/examples/mistral-provider.yaml b/gateway/examples/mistral-provider.yaml index 18a96bebdd..d3c5776aec 100644 --- a/gateway/examples/mistral-provider.yaml +++ b/gateway/examples/mistral-provider.yaml @@ -25,7 +25,7 @@ spec: auth: type: api-key header: Authorization - value: $dxxxd + value: Bearer REPLACE_WITH_MISTRAL_API_KEY policies: - name: api-key-auth version: v1 From 0698cbcfc8d589aa8d1d88038c5654eb7fd819b7 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Thu, 25 Jun 2026 16:05:12 +0530 Subject: [PATCH 8/9] Multi provider proxy gemini addition --- .../examples/openai-multi-provider-proxy.yaml | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/gateway/examples/openai-multi-provider-proxy.yaml b/gateway/examples/openai-multi-provider-proxy.yaml index a8ba025970..ffdc6e0382 100644 --- a/gateway/examples/openai-multi-provider-proxy.yaml +++ b/gateway/examples/openai-multi-provider-proxy.yaml @@ -36,6 +36,11 @@ spec: type: api-key header: X-API-Key value: anthropic_provider_loopback_key_1234567890abcdef + transformer: + type: openai-to-anthropic + version: v1 + params: + model: claude-sonnet-4-5-20250929 - id: azure-openai-provider auth: type: api-key @@ -47,11 +52,11 @@ spec: header: X-API-Key value: mistral_provider_loopback_key_1234567890abcdef #mistral_provider_loopback_key_1234567890abcdef - # - id: gemini-provider - # auth: - # type: api-key - # header: X-API-Key - # value: gemini_provider_loopback_key_1234567890abcdef + - id: gemini-provider + auth: + type: api-key + header: X-API-Key + value: gemini_provider_loopback_key_1234567890abcdef policies: - name: api-key-auth @@ -79,8 +84,8 @@ spec: provider: azure-openai-provider - headerValue: mistral provider: mistral-provider - # - headerValue: gemini - # provider: gemini-provider + - headerValue: gemini + provider: gemini-provider # - name: token-based-ratelimit # version: v1 @@ -95,6 +100,7 @@ spec: # backend: memory - name: openai-to-anthropic + executionCondition: metadata["selected_provider"] == "anthropic-provider" version: v1 paths: - path: /chat/completions @@ -122,15 +128,15 @@ spec: model: mistral-large-latest id: mistral-provider - # - name: openai-to-gemini - # version: v1 - # paths: - # - path: /chat/completions - # methods: [POST] - # params: - # model: gemini-2.5-flash - # id: gemini-provider - # apiVersion: v1beta + - name: openai-to-gemini + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: gemini-2.5-flash + id: gemini-provider + apiVersion: v1beta --- # Client API key for this proxy. The api-key-auth policy above validates From 97e231e3f5b0b66d915df41616968afb61ff1d21 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Thu, 2 Jul 2026 11:46:46 +0530 Subject: [PATCH 9/9] feat(gateway): move LLM proxy translator config inline onto additionalProviders Declares each provider's translator as additionalProviders[].transformer instead of a standalone top-level policy, so provider auth and its translator live together and the provider id no longer needs to be duplicated in the translator's params. Adds validation for missing type/version and IT coverage for routing, validation, and persistence. --- .../examples/openai-multi-provider-proxy.yaml | 59 +- .../api/management-openapi.yaml | 31 + .../pkg/api/management/generated.go | 573 +++++++++--------- .../pkg/config/llm_validator.go | 21 + .../pkg/utils/llm_transformer.go | 70 ++- .../pkg/utils/llm_transformer_test.go | 96 +++ gateway/it/features/llm-proxies.feature | 345 +++++++++++ 7 files changed, 867 insertions(+), 328 deletions(-) diff --git a/gateway/examples/openai-multi-provider-proxy.yaml b/gateway/examples/openai-multi-provider-proxy.yaml index ffdc6e0382..c501a76bd0 100644 --- a/gateway/examples/openai-multi-provider-proxy.yaml +++ b/gateway/examples/openai-multi-provider-proxy.yaml @@ -46,18 +46,33 @@ spec: type: api-key header: X-API-Key value: azure_openai_provider_loopback_key_1234567890abcdef + transformer: + type: openai-to-azure-openai + version: v1 + params: + model: gpt-4o + apiVersion: "2024-02-15-preview" - id: mistral-provider auth: type: api-key header: X-API-Key value: mistral_provider_loopback_key_1234567890abcdef - #mistral_provider_loopback_key_1234567890abcdef + transformer: + type: openai-to-mistral + version: v1 + params: + model: mistral-large-latest - id: gemini-provider auth: type: api-key header: X-API-Key value: gemini_provider_loopback_key_1234567890abcdef - + transformer: + type: openai-to-gemini + version: v1 + params: + model: gemini-2.5-flash + apiVersion: v1beta policies: - name: api-key-auth version: v1 @@ -98,46 +113,6 @@ spec: # duration: "1m" # algorithm: fixed-window # backend: memory - - - name: openai-to-anthropic - executionCondition: metadata["selected_provider"] == "anthropic-provider" - version: v1 - paths: - - path: /chat/completions - methods: [POST] - params: - model: claude-sonnet-4-5-20250929 - id: anthropic-provider - - - name: openai-to-azure-openai - version: v1 - paths: - - path: /chat/completions - methods: [POST] - params: - apiVersion: "2024-02-15-preview" - model: gpt-4o - id: azure-openai-provider - - - name: openai-to-mistral - version: v1 - paths: - - path: /chat/completions - methods: [POST] - params: - model: mistral-large-latest - id: mistral-provider - - - name: openai-to-gemini - version: v1 - paths: - - path: /chat/completions - methods: [POST] - params: - model: gemini-2.5-flash - id: gemini-provider - apiVersion: v1beta - --- # Client API key for this proxy. The api-key-auth policy above validates # the X-API-Key header against the key the gateway issues for this resource. diff --git a/gateway/gateway-controller/api/management-openapi.yaml b/gateway/gateway-controller/api/management-openapi.yaml index 7dd022450a..00df4e30f1 100644 --- a/gateway/gateway-controller/api/management-openapi.yaml +++ b/gateway/gateway-controller/api/management-openapi.yaml @@ -5851,6 +5851,37 @@ components: example: anthropic-upstream auth: $ref: '#/components/schemas/LLMUpstreamAuth' + transformer: + $ref: '#/components/schemas/LLMProxyTransformer' + + LLMProxyTransformer: + type: object + required: + - type + - version + description: > + Request/response translator applied when this provider is the selected + upstream. The proxy injects the translator as a conditional policy whose + execution condition matches this provider, so it runs only when the + provider is selected. The provider's `as` name (defaults to `id`) is + passed to the translator as its target upstream. + properties: + type: + type: string + description: Translator policy name (for example openai-to-anthropic). + minLength: 1 + example: openai-to-anthropic + version: + type: string + description: > + Major-only translator policy version (for example v1). The Gateway + Controller resolves it to the installed full version. + minLength: 1 + example: v1 + params: + type: object + description: Translator-specific parameters (for example model, apiVersion). + additionalProperties: true LLMAccessControl: type: object diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index fc5adeac85..efbcf3ce26 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -925,6 +925,9 @@ type LLMProxyAdditionalProvider struct { // Id Unique id of a deployed llm provider Id string `json:"id" yaml:"id"` + + // Transformer Request/response translator applied when this provider is the selected upstream. The proxy injects the translator as a conditional policy whose execution condition matches this provider, so it runs only when the provider is selected. The provider's `as` name (defaults to `id`) is passed to the translator as its target upstream. + Transformer *LLMProxyTransformer `json:"transformer,omitempty" yaml:"transformer,omitempty"` } // LLMProxyConfigData defines model for LLMProxyConfigData. @@ -1000,6 +1003,18 @@ type LLMProxyProvider struct { Id string `json:"id" yaml:"id"` } +// LLMProxyTransformer Request/response translator applied when this provider is the selected upstream. The proxy injects the translator as a conditional policy whose execution condition matches this provider, so it runs only when the provider is selected. The provider's `as` name (defaults to `id`) is passed to the translator as its target upstream. +type LLMProxyTransformer struct { + // Params Translator-specific parameters (for example model, apiVersion). + Params *map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"` + + // Type Translator policy name (for example openai-to-anthropic). + Type string `json:"type" yaml:"type"` + + // Version Major-only translator policy version (for example v1). The Gateway Controller resolves it to the installed full version. + Version string `json:"version" yaml:"version"` +} + // LLMUpstreamAuth defines model for LLMUpstreamAuth. type LLMUpstreamAuth struct { Header *string `json:"header,omitempty" yaml:"header,omitempty"` @@ -5207,283 +5222,287 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "9UNmpNvn/xMAAP//sKJAsX9ZAgA=", + "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", + "k9+PZ05dAgA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/gateway/gateway-controller/pkg/config/llm_validator.go b/gateway/gateway-controller/pkg/config/llm_validator.go index cb44132b63..107481c7c8 100644 --- a/gateway/gateway-controller/pkg/config/llm_validator.go +++ b/gateway/gateway-controller/pkg/config/llm_validator.go @@ -608,12 +608,33 @@ func (v *LLMValidator) validateProxyData(spec *api.LLMProxyConfigData) []Validat if provider.Auth != nil { errors = append(errors, v.validateLLMUpstreamAuth(fieldPrefix+".auth", provider.Auth)...) } + + if provider.Transformer != nil { + errors = append(errors, v.validateLLMProxyTransformer(fieldPrefix+".transformer", provider.Transformer)...) + } } } return errors } +func (v *LLMValidator) validateLLMProxyTransformer(fieldPrefix string, transformer *api.LLMProxyTransformer) []ValidationError { + var errors []ValidationError + if transformer.Type == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".type", + Message: "Transformer type is required", + }) + } + if transformer.Version == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".version", + Message: "Transformer version is required", + }) + } + return errors +} + func (v *LLMValidator) validateLLMUpstreamAuth(fieldPrefix string, auth *api.LLMUpstreamAuth) []ValidationError { var errors []ValidationError if auth.Type == "" { diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer.go b/gateway/gateway-controller/pkg/utils/llm_transformer.go index 701114746c..b9de661cd0 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer.go @@ -226,7 +226,10 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration } // Step 3.5: Apply proxy-level provider auth for proxy->provider loopback upstream + // and inline translators declared per additional provider. Both are attached as + // conditional policies so they run only when their provider is selected. var upstreamAuthPolicies []api.Policy + var transformerPolicies []api.Policy if proxy.Spec.Provider.Auth != nil { pol, err := t.proxyUpstreamAuthPolicy(proxy.Spec.Provider.Auth, "provider.auth") if err != nil { @@ -238,20 +241,28 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration } if proxy.Spec.AdditionalProviders != nil { for _, ap := range *proxy.Spec.AdditionalProviders { - if ap.Auth == nil { - continue - } name := ap.Id if ap.As != nil && *ap.As != "" { name = *ap.As } - pol, err := t.proxyUpstreamAuthPolicy(ap.Auth, fmt.Sprintf("additionalProviders[%s].auth", name)) - if err != nil { - return nil, err + + if ap.Auth != nil { + pol, err := t.proxyUpstreamAuthPolicy(ap.Auth, fmt.Sprintf("additionalProviders[%s].auth", name)) + if err != nil { + return nil, err + } + condition := selectedProviderExecutionCondition(name, false) + pol.ExecutionCondition = &condition + upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) + } + + if ap.Transformer != nil { + pol, err := t.proxyTransformerPolicy(ap.Transformer, name, fmt.Sprintf("additionalProviders[%s].transformer", name)) + if err != nil { + return nil, err + } + transformerPolicies = append(transformerPolicies, *pol) } - condition := selectedProviderExecutionCondition(name, false) - pol.ExecutionCondition = &condition - upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) } } @@ -324,6 +335,15 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration ops = append(ops, *op) } ops = sortOperationsBySpecificity(ops) + // Translators must run before upstream auth so the request is rewritten into + // the selected provider's shape before the upstream key is added. + if len(transformerPolicies) > 0 { + for i := range ops { + for _, transformerPolicy := range transformerPolicies { + appendOperationPolicy(&ops[i], transformerPolicy) + } + } + } if len(upstreamAuthPolicies) > 0 { for i := range ops { for _, upstreamAuthPolicy := range upstreamAuthPolicies { @@ -693,6 +713,38 @@ func (t *LLMProviderTransformer) proxyUpstreamAuthPolicy(auth *api.LLMUpstreamAu } } +// proxyTransformerPolicy builds a translator policy for an additional provider's +// inline transformer. The provider's upstream name is passed to the translator as +// its "id" param so it targets the correct upstream, and gates execution so the +// translator runs only when this provider is the selected upstream. +func (t *LLMProviderTransformer) proxyTransformerPolicy(transformer *api.LLMProxyTransformer, name, field string) (*api.Policy, error) { + if transformer == nil { + return nil, nil + } + if transformer.Type == "" { + return nil, fmt.Errorf("%s.type is required", field) + } + if transformer.Version == "" { + return nil, fmt.Errorf("%s.version is required", field) + } + + params := map[string]interface{}{} + if transformer.Params != nil { + for k, v := range *transformer.Params { + params[k] = v + } + } + params["id"] = name + + condition := selectedProviderExecutionCondition(name, false) + return &api.Policy{ + Name: transformer.Type, + Version: transformer.Version, + Params: ¶ms, + ExecutionCondition: &condition, + }, nil +} + func selectedProviderExecutionCondition(providerName string, includeDefault bool) string { selectedExpr := fmt.Sprintf("request.Metadata['selected_provider'] == '%s'", providerName) if includeDefault { diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go index 5bc2aaad8a..c78894b069 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go @@ -313,6 +313,102 @@ func TestLLMProviderTransformer_TransformProxy_AdditionalProviderAuthIsCondition assert.Equal(t, "anthropic-loopback", firstRequestHeaderValue(t, authPolicies[1].Params)) } +func TestLLMProviderTransformer_TransformProxy_AdditionalProviderTransformerIsConditional(t *testing.T) { + store := storage.NewConfigStore() + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + db := newTestSQLiteStorage(t, logger) + + template := &models.StoredLLMProviderTemplate{ + UUID: "0000-db-template-id-0000-000000000003", + Configuration: api.LLMProviderTemplate{ + ApiVersion: api.LLMProviderTemplateApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderTemplateKindLlmProviderTemplate, + Metadata: api.Metadata{Name: "openai"}, + Spec: api.LLMProviderTemplateData{DisplayName: "openai"}, + }, + } + require.NoError(t, db.SaveLLMProviderTemplate(template)) + + saveProvider := func(name, context string) { + providerSourceConfig := api.LLMProviderConfiguration{ + ApiVersion: api.LLMProviderConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderConfigurationKindLlmProvider, + Metadata: api.Metadata{Name: name}, + Spec: api.LLMProviderConfigData{ + DisplayName: name, + Version: "v1.0", + Context: stringPtr(context), + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://example.com")}, + AccessControl: api.LLMAccessControl{Mode: api.AllowAll}, + }, + } + require.NoError(t, db.SaveConfig(&models.StoredConfig{ + UUID: name + "-uuid", + Kind: string(api.LLMProviderConfigurationKindLlmProvider), + Handle: name, + DisplayName: name, + Version: "v1.0", + SourceConfiguration: providerSourceConfig, + DesiredState: models.StateDeployed, + })) + } + saveProvider("openai-provider", "/openai-provider") + saveProvider("anthropic-provider", "/anthropic-provider") + + transformer := NewLLMProviderTransformer(store, db, &config.RouterConfig{ListenerPort: 8080}, newTestPolicyVersionResolver()) + + proxy := &api.LLMProxyConfiguration{ + ApiVersion: api.LLMProxyConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProxyConfigurationKindLlmProxy, + Metadata: api.Metadata{Name: "openai-multi"}, + Spec: api.LLMProxyConfigData{ + DisplayName: "openai-multi", + Version: "v1.0", + Provider: api.LLMProxyProvider{Id: "openai-provider"}, + AdditionalProviders: &[]api.LLMProxyAdditionalProvider{{ + Id: "anthropic-provider", + Transformer: &api.LLMProxyTransformer{ + Type: "openai-to-anthropic", + Version: "v1", + Params: &map[string]interface{}{ + "model": "claude-sonnet-4-5-20250929", + }, + }, + }}, + }, + } + + result, err := transformer.Transform(proxy, &api.RestAPI{}) + require.NoError(t, err) + + // The translator is attached conditionally to every operation, so locate it + // wherever it lands rather than assuming a specific route. + var transformerPolicy *api.Policy + for i := range result.Spec.Operations { + op := result.Spec.Operations[i] + if op.Policies == nil { + continue + } + for j := range *op.Policies { + if (*op.Policies)[j].Name == "openai-to-anthropic" { + transformerPolicy = &(*op.Policies)[j] + break + } + } + if transformerPolicy != nil { + break + } + } + require.NotNil(t, transformerPolicy) + assert.Equal(t, "v1", transformerPolicy.Version) + require.NotNil(t, transformerPolicy.ExecutionCondition) + assert.Contains(t, *transformerPolicy.ExecutionCondition, "anthropic-provider") + require.NotNil(t, transformerPolicy.Params) + assert.Equal(t, "anthropic-provider", (*transformerPolicy.Params)["id"]) + assert.Equal(t, "claude-sonnet-4-5-20250929", (*transformerPolicy.Params)["model"]) +} + func TestGetUpstreamAuthApikeyPolicyParams_Extended(t *testing.T) { t.Run("Valid parameters", func(t *testing.T) { params, err := GetUpstreamAuthApikeyPolicyParams("Authorization", "Bearer token123") diff --git a/gateway/it/features/llm-proxies.feature b/gateway/it/features/llm-proxies.feature index 7348d7d198..73389ba919 100644 --- a/gateway/it/features/llm-proxies.feature +++ b/gateway/it/features/llm-proxies.feature @@ -595,3 +595,348 @@ Feature: LLM Proxy Management Operations Then the response should be successful When I delete the LLM provider "multi-request-provider" Then the response status code should be 200 + + # ==================== MULTI-PROVIDER ROUTING WITH executionCondition ==================== + + Scenario: Multi-provider proxy routes to correct provider based on x-provider header + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: ec-primary-provider + spec: + displayName: EC Primary Provider + version: v1.0 + template: openai + context: /ec-primary + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: ec-secondary-provider + spec: + displayName: EC Secondary Provider + version: v1.0 + template: openai + context: /ec-secondary + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProxy + metadata: + name: ec-multi-provider-proxy + spec: + displayName: EC Multi Provider Proxy + version: v1.0 + context: /ec-multi-proxy + provider: + id: ec-primary-provider + additionalProviders: + - id: ec-secondary-provider + policies: + - name: openai-header-router + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + headerName: x-provider + defaultProvider: ec-primary-provider + mappings: + - headerValue: primary + provider: ec-primary-provider + - headerValue: secondary + provider: ec-secondary-provider + """ + Then the response status should be 201 + And I wait for 3 seconds + + # No x-provider header — falls back to the defaultProvider (ec-primary-provider) + When I set header "Content-Type" to "application/json" + And I send a POST request to "http://localhost:8080/ec-multi-proxy/chat/completions" with body: + """ + { + "model": "gpt-4", + "messages": [{"role": "user", "content": "hello"}] + } + """ + Then the response status code should be 200 + And the response should be valid JSON + And the JSON response field "object" should be "chat.completion" + + # x-provider: secondary routes to ec-secondary-provider + When I set header "x-provider" to "secondary" + And I set header "Content-Type" to "application/json" + And I send a POST request to "http://localhost:8080/ec-multi-proxy/chat/completions" with body: + """ + { + "model": "gpt-4", + "messages": [{"role": "user", "content": "hello"}] + } + """ + Then the response status code should be 200 + And the response should be valid JSON + And the JSON response field "object" should be "chat.completion" + + # x-provider: primary explicitly selects ec-primary-provider + When I set header "x-provider" to "primary" + And I set header "Content-Type" to "application/json" + And I send a POST request to "http://localhost:8080/ec-multi-proxy/chat/completions" with body: + """ + { + "model": "gpt-4", + "messages": [{"role": "user", "content": "hello"}] + } + """ + Then the response status code should be 200 + And the response should be valid JSON + And the JSON response field "object" should be "chat.completion" + + # Cleanup + When I send a DELETE request to the "gateway-controller" service at "/llm-proxies/ec-multi-provider-proxy" + Then the response should be successful + When I delete the LLM provider "ec-primary-provider" + Then the response status code should be 200 + When I delete the LLM provider "ec-secondary-provider" + Then the response status code should be 200 + + # ==================== INLINE TRANSFORMER IN ADDITIONAL PROVIDERS ==================== + + Scenario: Create LLM proxy with additionalProviders transformer missing type returns validation error + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: transformer-val-primary + spec: + displayName: Transformer Validation Primary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: transformer-val-secondary + spec: + displayName: Transformer Validation Secondary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProxy + metadata: + name: transformer-missing-type-proxy + spec: + displayName: Transformer Missing Type Proxy + version: v1.0 + provider: + id: transformer-val-primary + additionalProviders: + - id: transformer-val-secondary + transformer: + version: v1.0.0 + params: + model: claude-sonnet-4-5-20250929 + """ + Then the response should be a client error + And the response should be valid JSON + # Cleanup + When I delete the LLM provider "transformer-val-primary" + Then the response status code should be 200 + When I delete the LLM provider "transformer-val-secondary" + Then the response status code should be 200 + + Scenario: Create LLM proxy with additionalProviders transformer missing version returns validation error + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: transformer-ver-primary + spec: + displayName: Transformer Version Primary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: transformer-ver-secondary + spec: + displayName: Transformer Version Secondary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProxy + metadata: + name: transformer-missing-ver-proxy + spec: + displayName: Transformer Missing Version Proxy + version: v1.0 + provider: + id: transformer-ver-primary + additionalProviders: + - id: transformer-ver-secondary + transformer: + type: openai-to-anthropic + params: + model: claude-sonnet-4-5-20250929 + """ + Then the response should be a client error + And the response should be valid JSON + # Cleanup + When I delete the LLM provider "transformer-ver-primary" + Then the response status code should be 200 + When I delete the LLM provider "transformer-ver-secondary" + Then the response status code should be 200 + + Scenario: Create LLM proxy with valid inline transformer in additionalProviders persists config + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: inline-transformer-primary + spec: + displayName: Inline Transformer Primary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: inline-transformer-secondary + spec: + displayName: Inline Transformer Secondary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProxy + metadata: + name: inline-transformer-proxy + spec: + displayName: Inline Transformer Proxy + version: v1.0 + provider: + id: inline-transformer-primary + additionalProviders: + - id: inline-transformer-secondary + transformer: + type: openai-to-anthropic + version: v1.0.0 + params: + model: claude-sonnet-4-5-20250929 + """ + Then the response status should be 201 + And the response should be valid JSON + And the JSON response field "status" should be "success" + # Verify the transformer config is persisted + When I send a GET request to the "gateway-controller" service at "/llm-proxies/inline-transformer-proxy" + Then the response should be successful + And the response should be valid JSON + And the response body should contain "openai-to-anthropic" + And the response body should contain "inline-transformer-secondary" + # Cleanup + When I send a DELETE request to the "gateway-controller" service at "/llm-proxies/inline-transformer-proxy" + Then the response should be successful + When I delete the LLM provider "inline-transformer-primary" + Then the response status code should be 200 + When I delete the LLM provider "inline-transformer-secondary" + Then the response status code should be 200