From dddeebd60f26bccd11083de7a2d0bc4929d80de9 Mon Sep 17 00:00:00 2001
From: yxia216
Date: Tue, 16 Jun 2026 12:13:08 -0400
Subject: [PATCH 01/29] update the usage-report
Signed-off-by: yxia216
---
internal/translator/anthropic_helper.go | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/internal/translator/anthropic_helper.go b/internal/translator/anthropic_helper.go
index f454c2433f..5c3be352d6 100644
--- a/internal/translator/anthropic_helper.go
+++ b/internal/translator/anthropic_helper.go
@@ -1053,15 +1053,11 @@ func (p *anthropicStreamParser) handleAnthropicStreamEvent(eventType []byte, dat
if output, ok := usage.OutputTokens(); ok {
p.tokenUsage.AddOutputTokens(output)
}
- // Update input tokens to include any cache tokens from delta
+ // Update cache token details from delta (don't add to InputTokens — already included in message_start)
if cached, ok := usage.CachedInputTokens(); ok {
- p.tokenUsage.AddInputTokens(cached)
- // Accumulate any additional cache tokens from delta
p.tokenUsage.AddCachedInputTokens(cached)
}
if cacheCreation, ok := usage.CacheCreationInputTokens(); ok {
- p.tokenUsage.AddInputTokens(cacheCreation)
- // Accumulate cache creation tokens
p.tokenUsage.AddCacheCreationInputTokens(cacheCreation)
}
if event.Delta.StopReason != "" {
From 8c758f48cb806d93819228d4449e72c2b1c86e15 Mon Sep 17 00:00:00 2001
From: Aaron Choo
Date: Tue, 9 Jun 2026 15:29:54 -0400
Subject: [PATCH 02/29] docs: update compatibility matrix EG -> V1.8.1 (#2215)
**Description**
Update the compatibility matrix to specify EG v1.8.1+.
Signed-off-by: Aaron Choo
Signed-off-by: yxia216
---
site/docs/compatibility.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/site/docs/compatibility.md b/site/docs/compatibility.md
index ea4cc4f378..84d9836d02 100644
--- a/site/docs/compatibility.md
+++ b/site/docs/compatibility.md
@@ -10,7 +10,7 @@ This document provides compatibility information for Envoy AI Gateway releases w
| AI Gateway | Envoy Gateway | Kubernetes | Gateway API | Support Status |
| ---------- | ----------------------------- | ---------- | ----------- | -------------- |
-| main | v1.7.x+ (Envoy Proxy v1.37.x) | v1.32+ | v1.4.x | Development |
+| main | v1.8.1+ (Envoy Proxy v1.38.1) | v1.32+ | v1.4.x | Development |
| v0.6.x | v1.7.x+ (Envoy Proxy v1.37.x) | v1.32+ | v1.4.x | Supported |
| v0.5.x | v1.6.x+ (Envoy Proxy v1.35.x) | v1.32+ | v1.4.x | Supported |
| others | N/A | N/A | N/A | End of Life |
From 8073ccdaf31637cfb555f65ab26547395da83a73 Mon Sep 17 00:00:00 2001
From: Anurag Aggarwal
Date: Wed, 10 Jun 2026 16:11:06 +0530
Subject: [PATCH 03/29] fix: set SchemeHeaderTransformation.MatchUpstream on AI
Gateway listeners (#2194)
**Description**
Azure is strict about `scheme` header to be same as the TLS transport on
which the request is being made. If the listener is an http listener,
envoy forwards a request with `scheme: http` instead of `https`. This
leads to requests getting dropped.
The simple solution is to use `matchUpstream` which is what is being
done in MCP backend listeners but missed in regular AI gateway route
listeners.
**Related Issues/PRs (if applicable)**
Fixes https://github.com/envoyproxy/ai-gateway/issues/2095
Signed-off-by: Anurag Aggarwal
Signed-off-by: yxia216
---
.../extensionserver/post_translate_modify.go | 4 ++++
.../post_translate_modify_test.go | 23 +++++++++++++++++++
2 files changed, 27 insertions(+)
diff --git a/internal/extensionserver/post_translate_modify.go b/internal/extensionserver/post_translate_modify.go
index 89689e1b3d..31edc9b1e0 100644
--- a/internal/extensionserver/post_translate_modify.go
+++ b/internal/extensionserver/post_translate_modify.go
@@ -676,6 +676,10 @@ func (s *Server) insertRouterLevelAIGatewayExtProc(listener *listenerv3.Listener
if err = insertAIGatewayExtProcFilter(httpConManager, extProcFilter); err != nil {
return fmt.Errorf("failed to insert AI Gateway extproc filter: %w", err)
}
+ // Match the :scheme pseudo-header to the upstream transport protocol.
+ httpConManager.SchemeHeaderTransformation = &corev3.SchemeHeaderTransformation{
+ MatchUpstream: true,
+ }
hcAny, err := toAny(httpConManager)
if err != nil {
return fmt.Errorf("failed to marshal updated HCM to Any: %w", err)
diff --git a/internal/extensionserver/post_translate_modify_test.go b/internal/extensionserver/post_translate_modify_test.go
index 00ea2c718d..f7babc8de4 100644
--- a/internal/extensionserver/post_translate_modify_test.go
+++ b/internal/extensionserver/post_translate_modify_test.go
@@ -340,6 +340,29 @@ func Test_shouldAIGatewayExtProcBeInserted(t *testing.T) {
}
}
+func TestServer_insertRouterLevelAIGatewayExtProc_setsSchemeHeaderTransformation(t *testing.T) {
+ hcm := &httpconnectionmanagerv3.HttpConnectionManager{
+ HttpFilters: []*httpconnectionmanagerv3.HttpFilter{{Name: wellknown.Router}},
+ }
+ listener := &listenerv3.Listener{
+ DefaultFilterChain: &listenerv3.FilterChain{
+ Filters: []*listenerv3.Filter{
+ {
+ Name: wellknown.HTTPConnectionManager,
+ ConfigType: &listenerv3.Filter_TypedConfig{TypedConfig: mustToAny(t, hcm)},
+ },
+ },
+ },
+ }
+ s := &Server{log: zap.New()}
+ require.NoError(t, s.insertRouterLevelAIGatewayExtProc(listener))
+
+ updatedHCM, _, err := findHCM(listener.DefaultFilterChain)
+ require.NoError(t, err)
+ require.True(t, updatedHCM.GetSchemeHeaderTransformation().GetMatchUpstream(),
+ "SchemeHeaderTransformation.MatchUpstream must be true so :scheme matches upstream TLS transport")
+}
+
func Test_findListenerRouteConfigs(t *testing.T) {
newHCM := func(name string) *httpconnectionmanagerv3.HttpConnectionManager {
return &httpconnectionmanagerv3.HttpConnectionManager{
From 699c72cf314bc7cade39217b9d03dde4e4dd9c57 Mon Sep 17 00:00:00 2001
From: aishwaryaraimule21
Date: Wed, 10 Jun 2026 21:39:54 +0530
Subject: [PATCH 04/29] fix: ensure that plaintext API key secret ref is not
exposed in MCP backend HTTPRoute (#2134)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**Description**
When an `MCPRoute` has a `backendRef` with `securityPolicy.apiKey`
configured (either via `secretRef` or `inline`), the controller resolves
the secret and embeds the **plaintext API key** directly into the
generated `HTTPRoute` resource. This happens in two places:
1. **Header injection** — the key is written as a literal value in an
`HTTPRouteFilter` of type `RequestHeaderModifier` (e.g., `Authorization:
Bearer `).
2. **Query parameter injection** — the key is appended to the URL path
in a `URLRewrite` filter (e.g., `/mcp?api_key=`).
fixes https://github.com/envoyproxy/ai-gateway/issues/2141
**Possible Solutions**
1. This can be solved for Header injection by using
https://gateway.envoyproxy.io/docs/api/extension_types/#httpcredentialinjectionfilter.
However, APIKey for `QueryParams` will still continue to be stored in
plaintext. **Note: This PR implements this approach.**
2. By contrast, the existing `BackendSecurityPolicy` (used by
`AIGatewayRoute` / `AIServiceBackend`) stores the resolved credential
inside a Kubernetes **Secret** (the filter config secret consumed by
extproc), which benefits from RBAC, encryption at rest, and audit
logging. MCPRoute should adopt a similar approach.
3. Explore if we can add support for QueryParams in
CredentialInjectionFilter in Envoy Gateway.
**Affected code:**
- internal/controller/mcp_route.go`
**Testing**
**Created MCPRoute with credential injection into Authorization
header.**
Credential got created and HTTPRouteFilter with Credential Injection
filter got created. List tools and tool calls work as expected.
Credential and filter got deleted on removing backend and deleting
MCPRoute.
**Created MCPRoute with credential injection into a custom header.**
Credential got created and HTTPRouteFilter with Credential Injection
filter got created. List tools and tool calls work as expected.
Credential and filter got deleted on removing backend and deleting
MCPRoute.
**Created MCPRoute with inline API Key into Authorization header.**
No change in behaviour.
**Created MCPRoute with API Key injected into query param**
No change in behaviour.
---------
Signed-off-by: Aishwarya
Signed-off-by: aishwaryaraimule21
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: yxia216
---
api/v1beta1/mcp_route.go | 3 +
internal/controller/mcp_route.go | 227 +++++++++----
internal/controller/mcp_route_test.go | 321 ++++++++++++++++--
internal/internalapi/internalapi.go | 2 +
.../aigateway.envoyproxy.io_mcproutes.yaml | 3 +
site/docs/api/api.mdx | 2 +-
6 files changed, 462 insertions(+), 96 deletions(-)
diff --git a/api/v1beta1/mcp_route.go b/api/v1beta1/mcp_route.go
index 120a69186d..895674b46a 100644
--- a/api/v1beta1/mcp_route.go
+++ b/api/v1beta1/mcp_route.go
@@ -232,6 +232,9 @@ type MCPBackendAPIKey struct {
//
// Either one of Header or QueryParam can be specified to inject the API key.
//
+ // Note: Embedding credentials in URLs (including query parameters) is generally not recommended because URLs can be exposed in logs
+ // and intermediary systems; prefer header-based injection when possible.
+ //
// +kubebuilder:validation:Optional
// +kubebuilder:validation:MinLength=1
// +optional
diff --git a/internal/controller/mcp_route.go b/internal/controller/mcp_route.go
index a4908d2d39..fe10b1b92c 100644
--- a/internal/controller/mcp_route.go
+++ b/internal/controller/mcp_route.go
@@ -12,6 +12,7 @@ import (
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/go-logr/logr"
+ corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
@@ -236,7 +237,7 @@ func (c *MCPRouteController) listExistingPerBackendHTTPRoutes(ctx context.Contex
}
// deleteOrphanedPerBackendResources deletes per-backend HTTPRoutes and their corresponding
-// HTTPRouteFilters that are no longer referenced by any backendRef in the MCPRoute spec.
+// HTTPRouteFilters and credential Secrets that are no longer referenced by any backendRef in the MCPRoute spec.
func (c *MCPRouteController) deleteOrphanedPerBackendResources(ctx context.Context, mcpRoute *aigv1b1.MCPRoute, orphaned map[string]*gwapiv1.HTTPRoute) error {
for name, route := range orphaned {
c.logger.Info("Deleting orphaned per-backend HTTPRoute", "namespace", route.Namespace, "name", name)
@@ -250,6 +251,17 @@ func (c *MCPRouteController) deleteOrphanedPerBackendResources(ctx context.Conte
if err := c.client.Delete(ctx, filter); err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to delete orphaned HTTPRouteFilter %s: %w", filterName, err)
}
+
+ // Credential secrets are only created for backends with secretRef-based API keys, but we
+ // unconditionally attempt the delete to avoid an extra GET call.
+ credSecretName := internalapi.MCPPerBackendCredentialSecretPrefix + strings.TrimPrefix(name, internalapi.MCPPerBackendRefHTTPRoutePrefix)
+ if err := c.kube.CoreV1().Secrets(mcpRoute.Namespace).Delete(ctx, credSecretName, metav1.DeleteOptions{}); err != nil {
+ if !apierrors.IsNotFound(err) {
+ return fmt.Errorf("failed to delete orphaned credential secret %s: %w", credSecretName, err)
+ }
+ } else {
+ c.logger.Info("Deleted orphaned credential secret", "namespace", mcpRoute.Namespace, "name", credSecretName)
+ }
}
return nil
}
@@ -549,87 +561,102 @@ func mcpBackendRefFilterName(mcpRoute *aigv1b1.MCPRoute, backendName gwapiv1.Obj
return fmt.Sprintf("%s%s-%s", internalapi.MCPPerBackendHTTPRouteFilterPrefix, mcpRoute.Name, backendName)
}
-// mcpBackendRefToHTTPRouteRule creates an HTTPRouteRule for the given MCPRouteBackendRef.
+func mcpCredentialSecretName(mcpRoute *aigv1b1.MCPRoute, backendName gwapiv1.ObjectName) string {
+ return fmt.Sprintf("%s%s-%s", internalapi.MCPPerBackendCredentialSecretPrefix, mcpRoute.Name, backendName)
+}
+
+// mcpBackendRefToHTTPRouteRule creates a HTTPRouteRule for the given MCPRouteBackendRef.
// The rule routes requests to the specified backend using internalapi.MCPBackendHeader,
// which is set by the MCP proxy based on its routing logic.
// This route rule will eventually be moved to the backend listener in the extension server.
func (c *MCPRouteController) mcpBackendRefToHTTPRouteRule(ctx context.Context, mcpRoute *aigv1b1.MCPRoute, ref *aigv1b1.MCPRouteBackendRef) (gwapiv1.HTTPRouteRule, error) {
- // Ensure the HTTPRouteFilter for this backend with its optional security configuration.
egFilterName := mcpBackendRefFilterName(mcpRoute, ref.Name)
- err := c.ensureMCPBackendRefHTTPFilter(ctx, egFilterName, mcpRoute)
- if err != nil {
- return gwapiv1.HTTPRouteRule{}, fmt.Errorf("failed to ensure MCP backend API key HTTP filter: %w", err)
- }
- filters := []gwapiv1.HTTPRouteFilter{
- {
- Type: gwapiv1.HTTPRouteFilterExtensionRef,
- ExtensionRef: &gwapiv1.LocalObjectReference{
- Group: "gateway.envoyproxy.io",
- Kind: "HTTPRouteFilter",
- Name: gwapiv1.ObjectName(egFilterName),
- },
- },
- }
+ // Determine credential handling from the backend's security policy.
+ // - inline API keys: use RequestHeaderModifier (no security benefit from a Secret since
+ // the plaintext is already in the MCPRoute CRD manifest).
+ // - secretRef API keys: use credentialInjection via a managed Secret to keep the
+ // plaintext confined to Secret resources only.
+ // - query param API keys: embed in the URL rewrite path (There is no route filter support for query params).
+ var credentialSecretName string
+ var credentialHeader *string
+ var inlineHeaderFilter *gwapiv1.HTTPRouteFilter
fullPathPtr := ptr.Deref(ref.Path, defaultMCPPath)
- // Add credential injection if apiKey is specified.
if ref.SecurityPolicy != nil && ref.SecurityPolicy.APIKey != nil {
apiKey := ref.SecurityPolicy.APIKey
- apiKeyLiteral, err := c.readAPIKey(ctx, mcpRoute.Namespace, apiKey)
- if err != nil {
- return gwapiv1.HTTPRouteRule{}, fmt.Errorf("failed to read API key for backend %s: %w", ref.Name, err)
- }
switch {
case apiKey.QueryParam != nil:
+ // Query parameter injection cannot use Envoy Gateway's credentialInjection filter;
+ // embed directly in the URL rewrite path.
+ // TODO: evaluate alternatives to avoid embedding the secret in the HTTPRoute manifest.
+ apiKeyLiteral, err := c.readAPIKey(ctx, mcpRoute.Namespace, apiKey)
+ if err != nil {
+ return gwapiv1.HTTPRouteRule{}, fmt.Errorf("failed to read API key for backend %s: %w", ref.Name, err)
+ }
fullPathPtr = fmt.Sprintf("%s?%s=%s", fullPathPtr, *apiKey.QueryParam, apiKeyLiteral)
- case apiKey.Header != nil:
- header := *apiKey.Header
+ case apiKey.Inline != nil:
+ // Inline API key: inject via RequestHeaderModifier directly. The value is already
+ // visible in the MCPRoute manifest, so a separate Secret adds no security benefit.
+ header := ptr.Deref(apiKey.Header, "Authorization")
+ value := *apiKey.Inline
if header == "Authorization" {
- apiKeyLiteral = "Bearer " + apiKeyLiteral
+ value = "Bearer " + value
}
- filters = append(filters,
- gwapiv1.HTTPRouteFilter{
- Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
- RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
- Set: []gwapiv1.HTTPHeader{
- {Name: gwapiv1.HTTPHeaderName(header), Value: apiKeyLiteral},
- },
- },
- },
- )
- default:
- filters = append(filters,
- gwapiv1.HTTPRouteFilter{
- Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
- RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
- Set: []gwapiv1.HTTPHeader{
- {Name: "Authorization", Value: "Bearer " + apiKeyLiteral},
- },
+ inlineHeaderFilter = &gwapiv1.HTTPRouteFilter{
+ Type: gwapiv1.HTTPRouteFilterRequestHeaderModifier,
+ RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{
+ Set: []gwapiv1.HTTPHeader{
+ {Name: gwapiv1.HTTPHeaderName(header), Value: value},
},
},
- )
+ }
+ case apiKey.SecretRef != nil:
+ // SecretRef API key: create a managed credential Secret and use the
+ // HTTPRouteFilter's credentialInjection to keep plaintext out of non-Secret resources.
+ credSecretName := mcpCredentialSecretName(mcpRoute, ref.Name)
+ if err := c.ensureCredentialSecret(ctx, credSecretName, mcpRoute, apiKey); err != nil {
+ return gwapiv1.HTTPRouteRule{}, fmt.Errorf("failed to ensure credential secret for backend %s: %w", ref.Name, err)
+ }
+ credentialSecretName = credSecretName
+ credentialHeader = apiKey.Header
}
}
- filters = append(filters,
- gwapiv1.HTTPRouteFilter{
- Type: gwapiv1.HTTPRouteFilterURLRewrite,
- URLRewrite: &gwapiv1.HTTPURLRewriteFilter{
- Path: &gwapiv1.HTTPPathModifier{
- Type: gwapiv1.FullPathHTTPPathModifier,
- ReplaceFullPath: ptr.To(fullPathPtr),
- },
+ // Ensure the HTTPRouteFilter for this backend with URL rewrite and optional credential injection.
+ if err := c.ensureMCPBackendRefHTTPFilter(ctx, egFilterName, mcpRoute, credentialSecretName, credentialHeader); err != nil {
+ return gwapiv1.HTTPRouteRule{}, fmt.Errorf("failed to ensure MCP backend HTTP filter: %w", err)
+ }
+
+ filters := []gwapiv1.HTTPRouteFilter{
+ {
+ Type: gwapiv1.HTTPRouteFilterExtensionRef,
+ ExtensionRef: &gwapiv1.LocalObjectReference{
+ Group: "gateway.envoyproxy.io",
+ Kind: "HTTPRouteFilter",
+ Name: gwapiv1.ObjectName(egFilterName),
},
},
- )
+ }
+ if inlineHeaderFilter != nil {
+ filters = append(filters, *inlineHeaderFilter)
+ }
+ filters = append(filters, gwapiv1.HTTPRouteFilter{
+ Type: gwapiv1.HTTPRouteFilterURLRewrite,
+ URLRewrite: &gwapiv1.HTTPURLRewriteFilter{
+ Path: &gwapiv1.HTTPPathModifier{
+ Type: gwapiv1.FullPathHTTPPathModifier,
+ ReplaceFullPath: ptr.To(fullPathPtr),
+ },
+ },
+ })
+
return gwapiv1.HTTPRouteRule{
Matches: []gwapiv1.HTTPRouteMatch{
{
Path: &gwapiv1.HTTPPathMatch{Type: ptr.To(gwapiv1.PathMatchPathPrefix), Value: ptr.To("/")},
Headers: []gwapiv1.HTTPHeaderMatch{
- // MCPRoute doesn't support cross-namespace backend reference so just use the name.
{Name: internalapi.MCPBackendHeader, Value: string(ref.Name)},
{Name: internalapi.MCPRouteHeader, Value: mcpRouteHeaderValue(mcpRoute)},
},
@@ -648,7 +675,6 @@ func (c *MCPRouteController) mcpBackendRefToHTTPRouteRule(ctx context.Context, m
},
}},
Timeouts: &gwapiv1.HTTPRouteTimeouts{
- // TODO: make it configurable via MCPRoute.Spec?
Request: ptr.To(gwapiv1.Duration("30m")),
BackendRequest: ptr.To(gwapiv1.Duration("30m")),
},
@@ -660,16 +686,20 @@ func mcpRouteHeaderValue(mcpRoute *aigv1b1.MCPRoute) string {
}
// ensureMCPBackendRefHTTPFilter ensures that an HTTPRouteFilter exists for the given backend reference in the MCPRoute.
-func (c *MCPRouteController) ensureMCPBackendRefHTTPFilter(ctx context.Context, filterName string, mcpRoute *aigv1b1.MCPRoute) error {
- // Rewrite the hostname to the backend service name.
- // This allows Envoy to route to public MCP services with SNI matching the service name.
- // This could be a standalone filter and moved to the main mcp gateway route logic.
+// When credentialSecretName is non-empty, the filter is configured with credential injection referencing
+// the given secret (which must store the credential under the InjectedCredentialKey key). When empty, only URL
+// hostname rewrite is configured.
+func (c *MCPRouteController) ensureMCPBackendRefHTTPFilter(ctx context.Context, filterName string, mcpRoute *aigv1b1.MCPRoute,
+ credentialSecretName string, credentialHeader *string,
+) error {
filter := &egv1a1.HTTPRouteFilter{
ObjectMeta: metav1.ObjectMeta{
Name: filterName,
Namespace: mcpRoute.Namespace,
},
Spec: egv1a1.HTTPRouteFilterSpec{
+ // Rewrite the hostname to the backend service name.
+ // This allows Envoy to route to public MCP services with SNI matching the service name.
URLRewrite: &egv1a1.HTTPURLRewriteFilter{
Hostname: &egv1a1.HTTPHostnameModifier{
Type: egv1a1.BackendHTTPHostnameModifier,
@@ -677,6 +707,19 @@ func (c *MCPRouteController) ensureMCPBackendRefHTTPFilter(ctx context.Context,
},
},
}
+
+ if credentialSecretName != "" {
+ filter.Spec.CredentialInjection = &egv1a1.HTTPCredentialInjectionFilter{
+ Overwrite: ptr.To(true),
+ Header: credentialHeader,
+ Credential: egv1a1.InjectedCredential{
+ ValueRef: gwapiv1.SecretObjectReference{
+ Name: gwapiv1.ObjectName(credentialSecretName),
+ },
+ },
+ }
+ }
+
if err := ctrlutil.SetControllerReference(mcpRoute, filter, c.client.Scheme()); err != nil {
return fmt.Errorf("failed to set controller reference for HTTPRouteFilter: %w", err)
}
@@ -693,6 +736,18 @@ func (c *MCPRouteController) ensureMCPBackendRefHTTPFilter(ctx context.Context,
return fmt.Errorf("failed to create HTTPRouteFilter: %w", err)
}
} else {
+ previousCredentialSecretName := ""
+ if existingFilter.Spec.CredentialInjection != nil {
+ previousCredentialSecretName = string(existingFilter.Spec.CredentialInjection.Credential.ValueRef.Name)
+ }
+ // Delete only on credential-injection transitions to avoid per-reconcile Delete calls.
+ if previousCredentialSecretName != "" && previousCredentialSecretName != credentialSecretName {
+ deleteErr := c.kube.CoreV1().Secrets(mcpRoute.Namespace).Delete(ctx, previousCredentialSecretName, metav1.DeleteOptions{})
+ if deleteErr != nil && !apierrors.IsNotFound(deleteErr) {
+ return fmt.Errorf("failed to delete stale credential secret %s: %w", previousCredentialSecretName, deleteErr)
+ }
+ }
+
// Update existing filter unconditionally to ensure it matches the desired state.
existingFilter.Spec = filter.Spec
c.logger.Info("Updating HTTPRouteFilter", "namespace", existingFilter.Namespace, "name", existingFilter.Name)
@@ -703,6 +758,60 @@ func (c *MCPRouteController) ensureMCPBackendRefHTTPFilter(ctx context.Context,
return nil
}
+// ensureCredentialSecret creates or updates a Kubernetes Secret that holds the formatted credential
+// value under the InjectedCredentialKey key. This secret is referenced by the HTTPRouteFilter's credentialInjection,
+// keeping the plaintext API key out of the HTTPRoute manifest.
+func (c *MCPRouteController) ensureCredentialSecret(ctx context.Context, secretName string, mcpRoute *aigv1b1.MCPRoute, apiKey *aigv1b1.MCPBackendAPIKey) error {
+ apiKeyLiteral, err := c.readAPIKey(ctx, mcpRoute.Namespace, apiKey)
+ if err != nil {
+ return fmt.Errorf("failed to read API key: %w", err)
+ }
+
+ // Format the credential value. The credentialInjection filter injects
+ // this verbatim into the target header.
+ credentialValue := apiKeyLiteral
+ header := ptr.Deref(apiKey.Header, "Authorization")
+ if header == "Authorization" {
+ credentialValue = "Bearer " + apiKeyLiteral
+ }
+
+ desired := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: secretName,
+ Namespace: mcpRoute.Namespace,
+ },
+ Data: map[string][]byte{
+ egv1a1.InjectedCredentialKey: []byte(credentialValue),
+ },
+ }
+ setRefErr := ctrlutil.SetControllerReference(mcpRoute, desired, c.client.Scheme())
+ if setRefErr != nil {
+ return fmt.Errorf("failed to set controller reference for credential secret: %w", setRefErr)
+ }
+
+ existing, err := c.kube.CoreV1().Secrets(mcpRoute.Namespace).Get(ctx, secretName, metav1.GetOptions{})
+ if err != nil {
+ if !apierrors.IsNotFound(err) {
+ return fmt.Errorf("failed to get credential secret: %w", err)
+ }
+ c.logger.Info("Creating credential secret", "namespace", mcpRoute.Namespace, "name", secretName)
+ if _, err = c.kube.CoreV1().Secrets(mcpRoute.Namespace).Create(ctx, desired, metav1.CreateOptions{}); err != nil {
+ return fmt.Errorf("failed to create credential secret: %w", err)
+ }
+ return nil
+ }
+
+ // Update if the credential value changed.
+ if string(existing.Data[egv1a1.InjectedCredentialKey]) != credentialValue {
+ existing.Data = desired.Data
+ c.logger.Info("Updating credential secret", "namespace", mcpRoute.Namespace, "name", secretName)
+ if _, err = c.kube.CoreV1().Secrets(mcpRoute.Namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil {
+ return fmt.Errorf("failed to update credential secret: %w", err)
+ }
+ }
+ return nil
+}
+
func (c *MCPRouteController) readAPIKey(ctx context.Context, namespace string, apiKey *aigv1b1.MCPBackendAPIKey) (string, error) {
key := ptr.Deref(apiKey.Inline, "")
if key == "" {
diff --git a/internal/controller/mcp_route_test.go b/internal/controller/mcp_route_test.go
index 68f2e270c8..40d0b574f2 100644
--- a/internal/controller/mcp_route_test.go
+++ b/internal/controller/mcp_route_test.go
@@ -285,42 +285,55 @@ func TestMCPRouteController_mcpRuleWithAPIKeyBackendSecurity(t *testing.T) {
ctrlr := NewMCPRouteController(c, kubeClient, logr.Discard(), eventCh.Ch)
tests := []struct {
- name string
- key *aigv1b1.MCPBackendAPIKey
- expRequestHeader *internalapi.Header
- refPath *string
- expPath string
+ name string
+ key *aigv1b1.MCPBackendAPIKey
+ // expCredentialValue is the expected value stored in the credential secret's InjectedCredentialKey key.
+ // When set, the HTTPRouteFilter is expected to have credentialInjection configured (secretRef path).
+ expCredentialValue string
+ // expCredentialHeader is the expected header for the credential injection filter (secretRef path).
+ expCredentialHeader *string
+ // expInlineHeader is the expected RequestHeaderModifier header/value (inline path).
+ expInlineHeader *internalapi.Header
+ // expFilterCount is the expected number of filters on the HTTPRouteRule.
+ expFilterCount int
+ refPath *string
+ expPath string
}{
{
- name: "inline API key default header",
- key: &aigv1b1.MCPBackendAPIKey{Inline: ptr.To("inline-key")},
- expRequestHeader: &internalapi.Header{"Authorization", "Bearer inline-key"},
- expPath: "/mcp",
+ name: "inline API key default header",
+ key: &aigv1b1.MCPBackendAPIKey{Inline: ptr.To("inline-key")},
+ expInlineHeader: &internalapi.Header{"Authorization", "Bearer inline-key"},
+ expFilterCount: 3,
+ expPath: "/mcp",
},
{
- name: "inline API key custom header",
- key: &aigv1b1.MCPBackendAPIKey{Inline: ptr.To("inline-key"), Header: ptr.To("X-API-KEY")},
- expRequestHeader: &internalapi.Header{"X-API-KEY", "inline-key"},
- expPath: "/mcp",
+ name: "inline API key custom header",
+ key: &aigv1b1.MCPBackendAPIKey{Inline: ptr.To("inline-key"), Header: ptr.To("X-API-KEY")},
+ expInlineHeader: &internalapi.Header{"X-API-KEY", "inline-key"},
+ expFilterCount: 3,
+ expPath: "/mcp",
},
{
- name: "secret ref API key default header",
- key: &aigv1b1.MCPBackendAPIKey{SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret"}},
- expRequestHeader: &internalapi.Header{"Authorization", "Bearer secretvalue"},
- refPath: ptr.To("/some/path"),
- expPath: "/some/path",
+ name: "secret ref API key default header",
+ key: &aigv1b1.MCPBackendAPIKey{SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret"}},
+ expCredentialValue: "Bearer secretvalue",
+ expFilterCount: 2,
+ refPath: ptr.To("/some/path"),
+ expPath: "/some/path",
},
{
- name: "query param API key",
- key: &aigv1b1.MCPBackendAPIKey{Inline: ptr.To("inline-key"), QueryParam: ptr.To("api_key")},
- expPath: "/mcp?api_key=inline-key",
+ name: "query param API key",
+ key: &aigv1b1.MCPBackendAPIKey{Inline: ptr.To("inline-key"), QueryParam: ptr.To("api_key")},
+ expFilterCount: 2,
+ expPath: "/mcp?api_key=inline-key",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ mcpRoute := &aigv1b1.MCPRoute{ObjectMeta: metav1.ObjectMeta{Name: "route-a", Namespace: "default"}}
httpRule, err := ctrlr.mcpBackendRefToHTTPRouteRule(t.Context(),
- &aigv1b1.MCPRoute{ObjectMeta: metav1.ObjectMeta{Name: "route-a", Namespace: "default"}},
+ mcpRoute,
&aigv1b1.MCPRouteBackendRef{
BackendObjectReference: gwapiv1.BackendObjectReference{
Name: "svc-a",
@@ -340,7 +353,9 @@ func TestMCPRouteController_mcpRuleWithAPIKeyBackendSecurity(t *testing.T) {
require.Equal(t, internalapi.MCPRouteHeader, string(headers[1].Name))
require.Contains(t, headers[1].Value, "route-a")
- // The first filter is the EG extension ref filter for URL host rewrite.
+ require.Len(t, httpRule.Filters, tt.expFilterCount)
+
+ // The first filter is always the EG extension ref filter for URL host rewrite.
egFilter := httpRule.Filters[0]
require.Equal(t, gwapiv1.HTTPRouteFilterExtensionRef, egFilter.Type)
require.NotNil(t, egFilter.ExtensionRef)
@@ -354,23 +369,54 @@ func TestMCPRouteController_mcpRuleWithAPIKeyBackendSecurity(t *testing.T) {
require.NotNil(t, httpFilter.Spec.URLRewrite.Hostname)
require.Equal(t, egv1a1.BackendHTTPHostnameModifier, httpFilter.Spec.URLRewrite.Hostname.Type)
- if tt.expRequestHeader != nil {
- // The second filter is the request header modifier for API key injection.
+ switch {
+ case tt.expCredentialValue != "":
+ // SecretRef path: credentialInjection on HTTPRouteFilter, no RequestHeaderModifier.
+ require.NotNil(t, httpFilter.Spec.CredentialInjection, "expected credentialInjection on the HTTPRouteFilter")
+ credSecretName := string(httpFilter.Spec.CredentialInjection.Credential.ValueRef.Name)
+ require.Equal(t, mcpCredentialSecretName(mcpRoute, "svc-a"), credSecretName)
+ require.Equal(t, tt.expCredentialHeader, httpFilter.Spec.CredentialInjection.Header)
+ require.True(t, *httpFilter.Spec.CredentialInjection.Overwrite)
+
+ credSecret, getSecretErr := kubeClient.CoreV1().Secrets("default").Get(t.Context(), credSecretName, metav1.GetOptions{})
+ require.NoError(t, getSecretErr)
+ require.Equal(t, tt.expCredentialValue, string(credSecret.Data[egv1a1.InjectedCredentialKey]))
+
+ // No plaintext should appear in any RequestHeaderModifier filter.
+ for _, f := range httpRule.Filters {
+ if f.RequestHeaderModifier != nil {
+ for _, h := range f.RequestHeaderModifier.Set {
+ require.NotContains(t, h.Value, "secretvalue", "plaintext API key must not appear in HTTPRoute")
+ }
+ }
+ }
+ case tt.expInlineHeader != nil:
+ // Inline path: RequestHeaderModifier, no credentialInjection, no credential Secret.
+ require.Nil(t, httpFilter.Spec.CredentialInjection, "inline key should not use credentialInjection")
+
reqHeaderFilter := httpRule.Filters[1]
require.Equal(t, gwapiv1.HTTPRouteFilterRequestHeaderModifier, reqHeaderFilter.Type)
require.NotNil(t, reqHeaderFilter.RequestHeaderModifier)
found := false
for _, set := range reqHeaderFilter.RequestHeaderModifier.Set {
- if set.Name == gwapiv1.HTTPHeaderName(tt.expRequestHeader.Key()) &&
- set.Value == tt.expRequestHeader.Value() {
+ if set.Name == gwapiv1.HTTPHeaderName(tt.expInlineHeader.Key()) &&
+ set.Value == tt.expInlineHeader.Value() {
found = true
break
}
}
- require.Truef(t, found, "Expected request header modifier not found in %v", reqHeaderFilter.RequestHeaderModifier.Set)
+ require.Truef(t, found, "expected header %v in %v", tt.expInlineHeader, reqHeaderFilter.RequestHeaderModifier.Set)
+
+ // No credential Secret should be created for inline keys.
+ credSecretName := mcpCredentialSecretName(mcpRoute, "svc-a")
+ _, err = kubeClient.CoreV1().Secrets("default").Get(t.Context(), credSecretName, metav1.GetOptions{})
+ require.True(t, apierrors.IsNotFound(err), "no credential secret should exist for inline API key")
+ default:
+ // Query param path: no credentialInjection, no RequestHeaderModifier.
+ require.Nil(t, httpFilter.Spec.CredentialInjection, "expected no credentialInjection for query param case")
}
- // Verify the last filter is the path rewrite filter.
+ // The last filter is always the path rewrite filter.
pathRewriteFilter := httpRule.Filters[len(httpRule.Filters)-1]
require.Equal(t, gwapiv1.HTTPRouteFilterURLRewrite, pathRewriteFilter.Type)
require.NotNil(t, pathRewriteFilter.URLRewrite)
@@ -381,15 +427,61 @@ func TestMCPRouteController_mcpRuleWithAPIKeyBackendSecurity(t *testing.T) {
}
}
+func TestMCPRouteController_staleCredentialSecretCleanup(t *testing.T) {
+ c := requireNewFakeClientWithIndexesForMCP(t)
+ eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
+ kubeClient := fakekube.NewClientset(&corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "api-secret", Namespace: "default"},
+ Data: map[string][]byte{"apiKey": []byte("my-secret-key")},
+ })
+ ctrlr := NewMCPRouteController(c, kubeClient, logr.Discard(), eventCh.Ch)
+
+ mcpRoute := &aigv1b1.MCPRoute{ObjectMeta: metav1.ObjectMeta{Name: "route-cleanup", Namespace: "default"}}
+ backendRef := &aigv1b1.MCPRouteBackendRef{
+ BackendObjectReference: gwapiv1.BackendObjectReference{
+ Name: "svc-b",
+ Namespace: ptr.To(gwapiv1.Namespace("default")),
+ },
+ SecurityPolicy: &aigv1b1.MCPBackendSecurityPolicy{
+ APIKey: &aigv1b1.MCPBackendAPIKey{SecretRef: &gwapiv1.SecretObjectReference{Name: "api-secret"}},
+ },
+ }
+
+ // Step 1: Create an HTTPRouteRule with secretRef-based credential injection.
+ _, err := ctrlr.mcpBackendRefToHTTPRouteRule(t.Context(), mcpRoute, backendRef)
+ require.NoError(t, err)
+
+ // Verify the credential secret was created.
+ credSecretName := mcpCredentialSecretName(mcpRoute, "svc-b")
+ credSecret, err := kubeClient.CoreV1().Secrets("default").Get(t.Context(), credSecretName, metav1.GetOptions{})
+ require.NoError(t, err)
+ require.Equal(t, "Bearer my-secret-key", string(credSecret.Data[egv1a1.InjectedCredentialKey]))
+
+ // Step 2: Remove the security policy from the backend ref and reconcile again.
+ backendRef.SecurityPolicy = nil
+ _, err = ctrlr.mcpBackendRefToHTTPRouteRule(t.Context(), mcpRoute, backendRef)
+ require.NoError(t, err)
+
+ // Verify the stale credential secret was deleted.
+ _, err = kubeClient.CoreV1().Secrets("default").Get(t.Context(), credSecretName, metav1.GetOptions{})
+ require.True(t, apierrors.IsNotFound(err), "expected credential secret to be deleted, but got: %v", err)
+
+ // Step 3: Verify that calling again without a security policy is idempotent (no error on missing secret).
+ backendRef.SecurityPolicy = nil
+ _, err = ctrlr.mcpBackendRefToHTTPRouteRule(t.Context(), mcpRoute, backendRef)
+ require.NoError(t, err)
+}
+
func TestMCPRouteController_ensureMCPBackendRefHTTPFilter(t *testing.T) {
c := requireNewFakeClientWithIndexesForMCP(t)
eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
- ctrlr := NewMCPRouteController(c, fakekube.NewClientset(
+ kubeClient := fakekube.NewClientset(
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "default"},
Data: map[string][]byte{"apiKey": []byte("test-api-key")},
},
- ), logr.Discard(), eventCh.Ch)
+ )
+ ctrlr := NewMCPRouteController(c, kubeClient, logr.Discard(), eventCh.Ch)
mcpRoute := &aigv1b1.MCPRoute{
ObjectMeta: metav1.ObjectMeta{Name: "test-route", Namespace: "default"},
@@ -398,13 +490,170 @@ func TestMCPRouteController_ensureMCPBackendRefHTTPFilter(t *testing.T) {
require.NoError(t, err)
filterName := mcpBackendRefFilterName(mcpRoute, "some-name")
- err = ctrlr.ensureMCPBackendRefHTTPFilter(t.Context(), filterName, mcpRoute)
- require.NoError(t, err)
- // Verify HTTPRouteFilter was created.
- var httpFilter egv1a1.HTTPRouteFilter
- err = c.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: filterName}, &httpFilter)
+ t.Run("without credential injection", func(t *testing.T) {
+ err = ctrlr.ensureMCPBackendRefHTTPFilter(t.Context(), filterName, mcpRoute, "", nil)
+ require.NoError(t, err)
+
+ var httpFilter egv1a1.HTTPRouteFilter
+ err = c.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: filterName}, &httpFilter)
+ require.NoError(t, err)
+ require.NotNil(t, httpFilter.Spec.URLRewrite)
+ require.Nil(t, httpFilter.Spec.CredentialInjection)
+ })
+
+ t.Run("with credential injection", func(t *testing.T) {
+ customHeader := "X-Custom-Key"
+ err = ctrlr.ensureMCPBackendRefHTTPFilter(t.Context(), filterName, mcpRoute, "managed-ref-primary", &customHeader)
+ require.NoError(t, err)
+
+ var httpFilter egv1a1.HTTPRouteFilter
+ err = c.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: filterName}, &httpFilter)
+ require.NoError(t, err)
+ require.NotNil(t, httpFilter.Spec.URLRewrite)
+ require.NotNil(t, httpFilter.Spec.CredentialInjection)
+ require.Equal(t, &customHeader, httpFilter.Spec.CredentialInjection.Header)
+ require.True(t, *httpFilter.Spec.CredentialInjection.Overwrite)
+ require.Equal(t, gwapiv1.ObjectName("managed-ref-primary"), httpFilter.Spec.CredentialInjection.Credential.ValueRef.Name)
+ })
+
+ t.Run("deletes stale secret when transitioning away from credential injection", func(t *testing.T) {
+ staleRefName := "stale-managed-ref"
+ _, createErr := kubeClient.CoreV1().Secrets("default").Create(t.Context(), &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: staleRefName, Namespace: "default"},
+ Data: map[string][]byte{egv1a1.InjectedCredentialKey: []byte("stale")},
+ }, metav1.CreateOptions{})
+ require.NoError(t, createErr)
+
+ authorizationHeader := "Authorization"
+ err = ctrlr.ensureMCPBackendRefHTTPFilter(t.Context(), filterName, mcpRoute, staleRefName, &authorizationHeader)
+ require.NoError(t, err)
+
+ err = ctrlr.ensureMCPBackendRefHTTPFilter(t.Context(), filterName, mcpRoute, "", nil)
+ require.NoError(t, err)
+
+ _, getErr := kubeClient.CoreV1().Secrets("default").Get(t.Context(), staleRefName, metav1.GetOptions{})
+ require.True(t, apierrors.IsNotFound(getErr), "expected stale credential secret to be deleted")
+ })
+
+ t.Run("deletes old secret when rotating credential injection secret", func(t *testing.T) {
+ oldRefName := "old-managed-ref"
+ newRefName := "new-managed-ref"
+ _, createErr := kubeClient.CoreV1().Secrets("default").Create(t.Context(), &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: oldRefName, Namespace: "default"},
+ Data: map[string][]byte{egv1a1.InjectedCredentialKey: []byte("old")},
+ }, metav1.CreateOptions{})
+ require.NoError(t, createErr)
+
+ customHeader := "X-Custom-Key"
+ err = ctrlr.ensureMCPBackendRefHTTPFilter(t.Context(), filterName, mcpRoute, oldRefName, &customHeader)
+ require.NoError(t, err)
+
+ err = ctrlr.ensureMCPBackendRefHTTPFilter(t.Context(), filterName, mcpRoute, newRefName, &customHeader)
+ require.NoError(t, err)
+
+ _, getErr := kubeClient.CoreV1().Secrets("default").Get(t.Context(), oldRefName, metav1.GetOptions{})
+ require.True(t, apierrors.IsNotFound(getErr), "expected old credential secret to be deleted")
+ })
+}
+
+func TestMCPRouteController_credentialHelpers(t *testing.T) {
+ c := requireNewFakeClientWithIndexesForMCP(t)
+ eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
+ kubeClient := fakekube.NewClientset(
+ &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "api-secret", Namespace: "default"},
+ Data: map[string][]byte{"apiKey": []byte("initial-key")},
+ },
+ &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "missing-api-key", Namespace: "default"},
+ Data: map[string][]byte{"other": []byte("value")},
+ },
+ )
+ ctrlr := NewMCPRouteController(c, kubeClient, logr.Discard(), eventCh.Ch)
+
+ mcpRoute := &aigv1b1.MCPRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-route", Namespace: "default"},
+ }
+ err := c.Create(t.Context(), mcpRoute)
require.NoError(t, err)
+
+ t.Run("ensureCredentialSecret", func(t *testing.T) {
+ secretRefKey := &aigv1b1.MCPBackendAPIKey{
+ SecretRef: &gwapiv1.SecretObjectReference{Name: "api-secret"},
+ }
+
+ err = ctrlr.ensureCredentialSecret(t.Context(), "managed-ref-secret", mcpRoute, secretRefKey)
+ require.NoError(t, err)
+
+ credSecret, getErr := kubeClient.CoreV1().Secrets("default").Get(t.Context(), "managed-ref-secret", metav1.GetOptions{})
+ require.NoError(t, getErr)
+ require.Equal(t, "Bearer initial-key", string(credSecret.Data[egv1a1.InjectedCredentialKey]))
+
+ _, updateErr := kubeClient.CoreV1().Secrets("default").Update(t.Context(), &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "api-secret", Namespace: "default"},
+ Data: map[string][]byte{"apiKey": []byte("rotated-key")},
+ }, metav1.UpdateOptions{})
+ require.NoError(t, updateErr)
+
+ err = ctrlr.ensureCredentialSecret(t.Context(), "managed-ref-secret", mcpRoute, secretRefKey)
+ require.NoError(t, err)
+
+ credSecret, getErr = kubeClient.CoreV1().Secrets("default").Get(t.Context(), "managed-ref-secret", metav1.GetOptions{})
+ require.NoError(t, getErr)
+ require.Equal(t, "Bearer rotated-key", string(credSecret.Data[egv1a1.InjectedCredentialKey]))
+
+ header := "X-API-Key"
+ inlineKey := &aigv1b1.MCPBackendAPIKey{
+ Inline: ptr.To("inline-key"),
+ Header: &header,
+ }
+ err = ctrlr.ensureCredentialSecret(t.Context(), "managed-ref-inline", mcpRoute, inlineKey)
+ require.NoError(t, err)
+
+ credSecret, getErr = kubeClient.CoreV1().Secrets("default").Get(t.Context(), "managed-ref-inline", metav1.GetOptions{})
+ require.NoError(t, getErr)
+ require.Equal(t, "inline-key", string(credSecret.Data[egv1a1.InjectedCredentialKey]))
+ })
+
+ t.Run("readAPIKey", func(t *testing.T) {
+ tests := []struct {
+ name string
+ keySpec *aigv1b1.MCPBackendAPIKey
+ wantKey string
+ wantError string
+ }{
+ {
+ name: "inline key",
+ keySpec: &aigv1b1.MCPBackendAPIKey{Inline: ptr.To("inline-value")},
+ wantKey: "inline-value",
+ },
+ {
+ name: "secretRef key",
+ keySpec: &aigv1b1.MCPBackendAPIKey{SecretRef: &gwapiv1.SecretObjectReference{Name: "api-secret"}},
+ wantKey: "rotated-key",
+ },
+ {
+ name: "missing apiKey in secret",
+ keySpec: &aigv1b1.MCPBackendAPIKey{SecretRef: &gwapiv1.SecretObjectReference{Name: "missing-api-key"}},
+ wantError: "does not contain 'apiKey' key",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotKey, readErr := ctrlr.readAPIKey(t.Context(), "default", tt.keySpec)
+ if tt.wantError != "" {
+ require.Error(t, readErr)
+ require.Contains(t, readErr.Error(), tt.wantError)
+ return
+ }
+
+ require.NoError(t, readErr)
+ require.Equal(t, tt.wantKey, gotKey)
+ })
+ }
+ })
}
func TestMCPRouteController_syncGateways_NamespaceCrossReference(t *testing.T) {
diff --git a/internal/internalapi/internalapi.go b/internal/internalapi/internalapi.go
index 81739abffa..a42bd2bda8 100644
--- a/internal/internalapi/internalapi.go
+++ b/internal/internalapi/internalapi.go
@@ -45,6 +45,8 @@ const (
MCPPerBackendRefHTTPRoutePrefix = MCPGeneratedResourceCommonPrefix + "br-"
// MCPPerBackendHTTPRouteFilterPrefix is the prefix for the HTTP route filter names for per-backend resources.
MCPPerBackendHTTPRouteFilterPrefix = MCPGeneratedResourceCommonPrefix + "brf-"
+ // MCPPerBackendCredentialSecretPrefix is the prefix for the credential secrets created for per-backend credential injection.
+ MCPPerBackendCredentialSecretPrefix = MCPGeneratedResourceCommonPrefix + "cred-"
// MCPMetadataHeaderPrefix is the prefix for special headers used to pass metadata in the filter metadata.
// These headers are added internally to the requests to the upstream servers so they can be populated in the filter
diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_mcproutes.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_mcproutes.yaml
index d77a726f82..e7fe7102e0 100644
--- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_mcproutes.yaml
+++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_mcproutes.yaml
@@ -5742,6 +5742,9 @@ spec:
"?api_key=mysecretkey".
Either one of Header or QueryParam can be specified to inject the API key.
+
+ Note: Embedding credentials in URLs (including query parameters) is generally not recommended because URLs can be exposed in logs
+ and intermediary systems; prefer header-based injection when possible.
minLength: 1
type: string
secretRef:
diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx
index 378c4cdad0..b3dc277b90 100644
--- a/site/docs/api/api.mdx
+++ b/site/docs/api/api.mdx
@@ -4243,7 +4243,7 @@ When both `header` and `queryParam` are unspecified, the API key will be injecte
name="queryParam"
type="string"
required="false"
- description="QueryParam is the HTTP query parameter to inject the API key into.
For example, if QueryParam is set to `api_key`, and the API key is `mysecretkey`, the request URL will be modified to include
`?api_key=mysecretkey`.
Either one of Header or QueryParam can be specified to inject the API key."
+ description="QueryParam is the HTTP query parameter to inject the API key into.
For example, if QueryParam is set to `api_key`, and the API key is `mysecretkey`, the request URL will be modified to include
`?api_key=mysecretkey`.
Either one of Header or QueryParam can be specified to inject the API key.
Note: Embedding credentials in URLs (including query parameters) is generally not recommended because URLs can be exposed in logs
and intermediary systems; prefer header-based injection when possible."
/>
From 91499c8df88261d1eb7b567af3c4f050b9bd46c6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 10 Jun 2026 16:56:22 +0000
Subject: [PATCH 05/29] chore(deps): bump the go group across 1 directory with
8 updates (#2200)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the go group with 7 updates in the / directory:
| Package | From | To |
| --- | --- | --- |
|
[github.com/anthropics/anthropic-sdk-go](https://github.com/anthropics/anthropic-sdk-go)
| `1.43.0` | `1.45.0` |
|
[github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2)
| `1.32.17` | `1.32.18` |
|
[github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk)
| `1.6.0` | `1.6.1` |
| [github.com/openai/openai-go/v3](https://github.com/openai/openai-go)
| `3.35.0` | `3.37.0` |
| [github.com/tetratelabs/func-e](https://github.com/tetratelabs/func-e)
| `1.5.0` | `1.6.0` |
|
[google.golang.org/api](https://github.com/googleapis/google-api-go-client)
| `0.279.0` | `0.280.0` |
| [google.golang.org/genai](https://github.com/googleapis/go-genai) |
`1.57.0` | `1.58.0` |
Updates `github.com/anthropics/anthropic-sdk-go` from 1.43.0 to 1.45.0
Release notes
Sourced from github.com/anthropics/anthropic-sdk-go's
releases.
v1.45.0
1.45.0 (2026-05-21)
Full Changelog: v1.44.1...v1.45.0
Features
- api: Add support for thinking-token-count beta for
estimated tokens in thinking block deltas when streaming (dedeb6d)
v1.44.1
1.44.1 (2026-05-19)
Full Changelog: v1.44.0...v1.44.1
Bug Fixes
- runner: skip tool calls SessionToolRunner does not
own (93afc65)
v1.44.0
1.44.0 (2026-05-19)
Full Changelog: v1.43.0...v1.44.0
Features
- client: Add support for self-hosted sandboxes in
CMA with sandbox helpers (34354c4)
Changelog
Sourced from github.com/anthropics/anthropic-sdk-go's
changelog.
1.45.0 (2026-05-21)
Full Changelog: v1.44.1...v1.45.0
Features
- api: Add support for thinking-token-count beta for
estimated tokens in thinking block deltas when streaming (dedeb6d)
1.44.1 (2026-05-19)
Full Changelog: v1.44.0...v1.44.1
Bug Fixes
- runner: skip tool calls SessionToolRunner does not
own (93afc65)
1.44.0 (2026-05-19)
Full Changelog: v1.43.0...v1.44.0
Features
- client: Add support for self-hosted sandboxes in
CMA with sandbox helpers (34354c4)
Commits
88310cc
release: 1.45.0
4eb28e3
feat(api): Add support for thinking-token-count beta for estimated
tokens in ...
d138190
release: 1.44.1
d0a73a5
fix(runner): skip tool calls SessionToolRunner does not own
2888573
release: 1.44.0 (#340)
- See full diff in compare
view
Updates `github.com/aws/aws-sdk-go-v2/config` from 1.32.17 to 1.32.18
Commits
Updates `github.com/modelcontextprotocol/go-sdk` from 1.6.0 to 1.6.1
Release notes
Sourced from github.com/modelcontextprotocol/go-sdk's
releases.
v1.6.1
This release adds an MCPGODEBUG flag to opt out of the Content-Type
check on POST requests.
Behavior Changes
Prior to v1.6.0 (v1.4.0...v1.5.0), the Content-Type check on POST
requests was gated by the same disablecrossoriginprotection
MCPGODEBUG flag as the cross-origin protection. In v1.6.0, the
cross-origin protection was disabled by default (replaced by the opt-in
enableoriginverification flag), but the Content-Type check
was kept on unconditionally, leaving no way to disable it.
This release restores an escape hatch for both the Streamable HTTP and
SSE transports: setting
MCPGODEBUG=disablecontenttypecheck=1 skips the
Content-Type: application/json validation on POST requests.
See #957.
What's Changed
Full Changelog: https://github.com/modelcontextprotocol/go-sdk/compare/v1.6.0...v1.6.1
Commits
Updates `github.com/openai/openai-go/v3` from 3.35.0 to 3.37.0
Release notes
Sourced from github.com/openai/openai-go/v3's
releases.
v3.37.0
3.37.0 (2026-05-21)
Full Changelog: v3.36.0...v3.37.0
Features
- api: api update (7f7416e)
- api: manual updates (d646562)
- api: update OpenAPI spec or Stainless config (b34b78a)
- client: optimize json encoder for internal types
(93adc6e)
Bug Fixes
- go: format generated admin paths (1dd8f5e)
- go: format generated project permission paths (b751c37)
Chores
v3.36.0
3.36.0 (2026-05-13)
Full Changelog: v3.35.0...v3.36.0
Features
- api: add service_tier parameter to response compact
method (bacd2c0)
Bug Fixes
- go: avoid panic when http.DefaultTransport is
wrapped (95a0250)
Changelog
Sourced from github.com/openai/openai-go/v3's
changelog.
3.37.0 (2026-05-21)
Full Changelog: v3.36.0...v3.37.0
Features
- api: api update (7f7416e)
- api: manual updates (d646562)
- api: update OpenAPI spec or Stainless config (b34b78a)
- client: optimize json encoder for internal types
(93adc6e)
Bug Fixes
- go: format generated admin paths (1dd8f5e)
- go: format generated project permission paths (b751c37)
Chores
3.36.0 (2026-05-13)
Full Changelog: v3.35.0...v3.36.0
Features
- api: add service_tier parameter to response compact
method (bacd2c0)
Bug Fixes
- go: avoid panic when http.DefaultTransport is
wrapped (95a0250)
Commits
8f01a93
Merge pull request #676
from openai/release-please--branches--main--changes--...
2db0d86
release: 3.37.0
ea245d8
Merge pull request #991
from stainless-sdks/dev/xuanqi/public-api-docs-enterp...
1dd8f5e
fix(go): format generated admin paths
7f7416e
feat(api): api update
b751c37
fix(go): format generated project permission paths
08bc80e
chore(api): docs updates
d646562
feat(api): manual updates
b34b78a
feat(api): update OpenAPI spec or Stainless config
efa3cc0
codegen metadata
- Additional commits viewable in compare
view
Updates `github.com/tetratelabs/func-e` from 1.5.0 to 1.6.0
Release notes
Sourced from github.com/tetratelabs/func-e's
releases.
v1.6.0
func-e 1.6.0 gives you yesterday's envoy main build via the
"dev" version
Before, testing pre-release Envoy with func-e meant compiling it
yourself or using Docker, which only worked on Linux. Now you can
install and run dev builds on all platforms the same way you would any
tagged version.
$ ENVOY_VERSION=dev func-e run --version
downloading
https://archive.tetratelabs.io/envoy/download/dev/envoy-dev-darwin-arm64.tar.xz
starting:
/Users/codefromthecrypt/.local/share/func-e/envoy-versions/dev/bin/envoy
with logs in
/Users/codefromthecrypt/.local/state/func-e/envoy-runs/20260518_170024_807
/Users/codefromthecrypt/.local/share/func-e/envoy-versions/dev/bin/envoy
version:
abbadd905fa486ff1085cf3bbe4f3b73eb6dd8e0/1.39.0-dev/Clean/RELEASE/BoringSSL
$ func-e versions -a
dev 2026-05-18 (a8d396eb)
1.38.0 2026-04-23
1.37.2 2026-04-10
-- snip--
This works because envoy's version manifest was recently updated with
a special "dev" key backed by a daily
pipeline job, which archives linux from envoy's docker image, and
builds macos from the same SHA.
Updating the "dev" build with the "dev-latest"
alias.
dev installs on demand like all other versions and stays
put once pulled. The alias dev-latest compares the remote
release date against the local install and re-downloads only when the
build has changed. func-e use dev-latest persists
"dev" in the version file, so it is a one-shot refresh, not a
stored preference.
Why Use An Envoy "dev" Build?
Envoy releases are worth the wait, but they land about once a
quarter. The dev build is for the gap between “merged” and “released”:
you can try current Envoy mainline without building Envoy yourself or
relying on a Linux-only Docker workflow.
For example, Envoy 1.39-dev already has new dynamic-module support
for request-aware upstream selection. A dynamic module can parse a
request, write selected_backend or
selected_endpoint into request state, and have the cluster
read it while choosing the upstream host. That removes the need to pass
routing decisions through synthetic headers or route re-selection.
Use dev when you need to test an unreleased feature, validate upgrade
impact early, or give feedback before the next Envoy release is cut. Use
tagged versions for normal production runs.
Commits
Updates `google.golang.org/api` from 0.279.0 to 0.280.0
Release notes
Sourced from google.golang.org/api's
releases.
v0.280.0
Features
Changelog
Sourced from google.golang.org/api's
changelog.
Features
Commits
Updates `google.golang.org/genai` from 1.57.0 to 1.58.0
Release notes
Sourced from google.golang.org/genai's
releases.
v1.58.0
1.58.0
(2026-05-21)
Features
- add
enable_prompt_injection_detection for Computer Use
feature for the Gemini API. (19c2566)
- add new fields (1608e80)
Changelog
Sourced from google.golang.org/genai's
changelog.
1.58.0
(2026-05-21)
Features
- add
enable_prompt_injection_detection for Computer Use
feature for the Gemini API. (19c2566)
- add new fields (1608e80)
Commits
97ea31f
chore(main): release 1.58.0 (#793)
19c2566
feat: add enable_prompt_injection_detection for Computer
Use feature for th...
1608e80
feat: add new fields
843a665
chore: update comment in BatchJobOutputInfo to unblock javadoc
generation
8b28bf8
chore: Throw fatals() instead of errors() in the replay_api_client when
the i...
- See full diff in compare
view
Updates `google.golang.org/genproto/googleapis/rpc` from
0.0.0-20260427160629-7cedc36a6bc4 to 0.0.0-20260511170946-3700d4141b60
Commits
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ignasi Barrera
Signed-off-by: yxia216
---
go.mod | 20 ++++++++++----------
go.sum | 40 ++++++++++++++++++++--------------------
2 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/go.mod b/go.mod
index 67bce5632b..649c13301d 100644
--- a/go.mod
+++ b/go.mod
@@ -9,10 +9,10 @@ require (
github.com/a8m/envsubst v1.4.3
github.com/alecthomas/kong v1.15.0
github.com/andybalholm/brotli v1.2.1
- github.com/anthropics/anthropic-sdk-go v1.43.0
+ github.com/anthropics/anthropic-sdk-go v1.45.0
github.com/aws/aws-sdk-go-v2 v1.41.7
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10
- github.com/aws/aws-sdk-go-v2/config v1.32.17
+ github.com/aws/aws-sdk-go-v2/config v1.32.18
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1
github.com/bytedance/sonic v1.15.1
github.com/cenkalti/backoff/v4 v4.3.0
@@ -29,15 +29,15 @@ require (
github.com/google/jsonschema-go v0.4.3
github.com/google/uuid v1.6.0
github.com/moby/moby/api v1.54.2
- github.com/modelcontextprotocol/go-sdk v1.6.0
+ github.com/modelcontextprotocol/go-sdk v1.6.1
github.com/openai/openai-go v1.12.0
- github.com/openai/openai-go/v3 v3.35.0
+ github.com/openai/openai-go/v3 v3.37.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.67.5
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.42.0
- github.com/tetratelabs/func-e v1.5.0
+ github.com/tetratelabs/func-e v1.6.0
github.com/tidwall/gjson v1.19.0
github.com/tidwall/sjson v1.2.6-0.20251103175603-13f6455cf849
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0
@@ -57,9 +57,9 @@ require (
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/tools v0.45.0
- google.golang.org/api v0.279.0
- google.golang.org/genai v1.57.0
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4
+ google.golang.org/api v0.280.0
+ google.golang.org/genai v1.58.0
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
gopkg.in/dnaeon/go-vcr.v4 v4.0.6
@@ -90,7 +90,7 @@ require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/avast/retry-go/v5 v5.0.0 // indirect
- github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
@@ -99,7 +99,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 // indirect
github.com/aws/smithy-go v1.25.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index 2524e427f7..29b5271c7e 100644
--- a/go.sum
+++ b/go.sum
@@ -43,8 +43,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
-github.com/anthropics/anthropic-sdk-go v1.43.0 h1:ShY3C7lafzHP0ze1dCxL3ZFZzvkGfXJN91DfZTG8zLM=
-github.com/anthropics/anthropic-sdk-go v1.43.0/go.mod h1:5cEaslQ6A9ajdL5YUvhNW57LKxEz0OAZ7WEzgZWLD7k=
+github.com/anthropics/anthropic-sdk-go v1.45.0 h1:rWnpyBpm9OAm97jyH5bi6W4SRCwJeNY/RyhaJ7CHSUI=
+github.com/anthropics/anthropic-sdk-go v1.45.0/go.mod h1:bx5vWuHFuGPkELH8Z4KUiNSohFnUwScdpTyr+50myPo=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -55,10 +55,10 @@ github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6t
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
-github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
-github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
+github.com/aws/aws-sdk-go-v2/config v1.32.18 h1:Hcia46bxhGgF3BaSnG8nSNCWmqTK6bj9xN9/FJ3WK6Q=
+github.com/aws/aws-sdk-go-v2/config v1.32.18/go.mod h1:zEjCAYmxqDadH1WX8CdBvmLKhUEUVFgKRQG38zjDmrY=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.17 h1:gP2nkGsS+KMvF/jfFz2Vv2qiiOqWKyPACSzPsqHgoW8=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.17/go.mod h1:Bsew3S/moG5iT77giPj1q8wb/s0RE5/QfH+ASjYtuQc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
@@ -75,8 +75,8 @@ github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VX
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 h1:nDARhv/oF55bcxF7rCI/4PDxOKnVXVWwDuDwCs2I2SQ=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
@@ -341,8 +341,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
-github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY=
-github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
+github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
+github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -362,8 +362,8 @@ github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0=
github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
-github.com/openai/openai-go/v3 v3.35.0 h1:109x3epXMSE423KW2euR506GGFezcEt0s87MoWejpH0=
-github.com/openai/openai-go/v3 v3.35.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
+github.com/openai/openai-go/v3 v3.37.0 h1:4OG68yZgnxZpwzebO+ZDUNkFJKKwKgzilMQq30nsouE=
+github.com/openai/openai-go/v3 v3.37.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -433,8 +433,8 @@ github.com/telepresenceio/watchable v0.0.0-20220726211108-9bb86f92afa7 h1:GMw3nE
github.com/telepresenceio/watchable v0.0.0-20220726211108-9bb86f92afa7/go.mod h1:ihJ97e2gsd8GuzFF/I3B1qcik3XZLpXjumQifXi8Slg=
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
-github.com/tetratelabs/func-e v1.5.0 h1:usqnwqIlLereQdua/BYLwFNfnNX7qGOFUGPihp8KMo0=
-github.com/tetratelabs/func-e v1.5.0/go.mod h1:9d/4Wne/HSg8pM+6fNhUePCsbQLeDrajn+wwbiN5WS4=
+github.com/tetratelabs/func-e v1.6.0 h1:TlTVVCSX/I+SBg6NWtU8On7EjrGz2/kxHQUtOo5tl1U=
+github.com/tetratelabs/func-e v1.6.0/go.mod h1:9d/4Wne/HSg8pM+6fNhUePCsbQLeDrajn+wwbiN5WS4=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
@@ -622,16 +622,16 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
-google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74=
-google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
-google.golang.org/genai v1.57.0 h1:qTyG2ynz5dQy2jF4CvZdLHHVslhR0heMue+zM1a4GNM=
-google.golang.org/genai v1.57.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
+google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk=
+google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU=
+google.golang.org/genai v1.58.0 h1:MNA3ZkRyr7MnRwZ9RNZ60p4+UMKV3yYRw6pyHq4pp0U=
+google.golang.org/genai v1.58.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds=
From 7f630d3d9c9a8b378a16e461ff6d19f09adfec6a Mon Sep 17 00:00:00 2001
From: Hritik Raj
Date: Wed, 10 Jun 2026 22:51:29 +0530
Subject: [PATCH 06/29] fix: status of routes when Gateway is not found (#2180)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**Description**
When an MCPRoute/AIGW ROute references a non-existent Gateway in its
spec.parentRefs, the controller still marks the route as "Accepted" with
the message "Gateway Route reconciled successfully". The controller logs
"Gateway not found" but does not propagate this as an error, so the
reconciliation appears successful.
Root Cause:
In internal/controller/mcp_route.go, the syncGateway function was a void
function that silently returned on errors — it logged "Gateway not
found" but never returned an error to the caller. Since syncGateways
(which calls syncGateway for each parentRef) always returned nil, the
syncMCPRoute function completed successfully, and Reconcile marked the
status as ConditionTypeAccepted.
- and same goes for ai gateway route controller
Fix:
Changed syncGateway to return an error. When the referenced Gateway is
not found, it now returns a descriptive error ("gateway
/ not found"). This error propagates through
syncGateways → syncRoute → Reconcile, which then calls updateRouteStatus
with ConditionTypeNotAccepted and the error message.
During Route deletion (via the finalizer path), the error is still
logged but does not block cleanup — handleFinalizer already handles
onDeletionFn errors non-fatally.
---------
Signed-off-by: Hritik003
Signed-off-by: yxia216
---
cmd/aigw/translate.go | 30 ++++-
internal/controller/ai_gateway_route.go | 14 ++-
internal/controller/ai_gateway_route_test.go | 106 ++++++++++++++++-
internal/controller/mcp_route.go | 14 ++-
internal/controller/mcp_route_test.go | 119 ++++++++++++++++++-
5 files changed, 268 insertions(+), 15 deletions(-)
diff --git a/cmd/aigw/translate.go b/cmd/aigw/translate.go
index 5a9859c812..12f11ecbf6 100644
--- a/cmd/aigw/translate.go
+++ b/cmd/aigw/translate.go
@@ -234,21 +234,31 @@ func translateCustomResourceObjects(
userDefinedSecretKeys[fmt.Sprintf("%s/%s", s.Namespace, s.Name)] = struct{}{}
}
+ // Use buffered event channels so that controllers can push gateway sync events without blocking,
+ // This mirrors the production code at controller/controller.go:128
+ const eventChanBuffer = 100
bspC := controller.NewBackendSecurityPolicyController(fakeClient, fakeClientSet, logr.FromSlogHandler(logger.Handler()),
- make(chan event.GenericEvent), make(chan event.GenericEvent))
+ make(chan event.GenericEvent, eventChanBuffer), make(chan event.GenericEvent, eventChanBuffer))
aisbC := controller.NewAIServiceBackendController(fakeClient, fakeClientSet, logr.FromSlogHandler(logger.Handler()),
- make(chan event.GenericEvent))
+ make(chan event.GenericEvent, eventChanBuffer))
airC := controller.NewAIGatewayRouteController(fakeClient, fakeClientSet, logr.FromSlogHandler(logger.Handler()),
- make(chan event.GenericEvent), "/",
+ make(chan event.GenericEvent, eventChanBuffer), "/",
)
mcpC := controller.NewMCPRouteController(fakeClient, fakeClientSet, logr.FromSlogHandler(logger.Handler()),
- make(chan event.GenericEvent),
+ make(chan event.GenericEvent, eventChanBuffer),
)
gwC := controller.NewGatewayController(fakeClient, fakeClientSet, logr.FromSlogHandler(logger.Handler()),
"docker.io/envoyproxy/ai-gateway-extproc:latest", "debug", true, func() string {
return "aigw-translate"
}, false,
)
+ // Pre-create Gateways (without reconciling) before reconciling resources so that
+ // syncGateways can resolve the parent Gateway via the fake client.
+ // Otherwise the reconcile would fail with "gateway not found" because the
+ // Gateway is not yet present in the cache at this point.
+ for _, gw := range gws {
+ mustCreate(ctx, fakeClient, gw, logger)
+ }
// Create and reconcile the custom resources to store the translated objects.
// Note that the order of creation is important as some objects depend on others.
for _, btp := range backendTLSPolicies {
@@ -267,7 +277,7 @@ func translateCustomResourceObjects(
mustCreateAndReconcile(ctx, fakeClient, mcpRoute, mcpC, logger)
}
for _, gw := range gws {
- mustCreateAndReconcile(ctx, fakeClient, gw, gwC, logger)
+ mustReconcile(ctx, gw, gwC, logger)
}
// Now you can retrieve the translated objects from the fake client.
@@ -347,6 +357,16 @@ func mustCreateAndReconcile(
logger *slog.Logger,
) {
mustCreate(ctx, fakeClient, obj, logger)
+ mustReconcile(ctx, obj, c, logger)
+}
+
+// mustReconcile reconciles the object using the provided reconciler.
+func mustReconcile(
+ ctx context.Context,
+ obj client.Object,
+ c reconcile.TypedReconciler[reconcile.Request],
+ logger *slog.Logger,
+) {
logger.Info("Fake reconciling", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
_, err := c.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}})
if err != nil {
diff --git a/internal/controller/ai_gateway_route.go b/internal/controller/ai_gateway_route.go
index 970b4afe74..740f49e704 100644
--- a/internal/controller/ai_gateway_route.go
+++ b/internal/controller/ai_gateway_route.go
@@ -367,24 +367,30 @@ func (c *AIGatewayRouteController) syncGateways(ctx context.Context, aiGatewayRo
if p.Namespace != nil {
gwNamespace = string(*p.Namespace)
}
- c.syncGateway(ctx, gwNamespace, string(p.Name))
+ if err := c.syncGateway(ctx, gwNamespace, string(p.Name)); err != nil {
+ if aiGatewayRoute.DeletionTimestamp != nil && apierrors.IsNotFound(err) {
+ continue
+ }
+ return err
+ }
}
return nil
}
// syncGateway is a helper function for syncGateways that sends one GenericEvent to the gateway controller.
-func (c *AIGatewayRouteController) syncGateway(ctx context.Context, namespace, name string) {
+func (c *AIGatewayRouteController) syncGateway(ctx context.Context, namespace, name string) error {
var gw gwapiv1.Gateway
if err := c.client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &gw); err != nil {
if apierrors.IsNotFound(err) {
c.logger.Info("Gateway not found", "namespace", namespace, "name", name)
- return
+ return fmt.Errorf("gateway %s/%s not found: %w", namespace, name, err)
}
c.logger.Error(err, "failed to get Gateway", "namespace", namespace, "name", name)
- return
+ return fmt.Errorf("failed to get Gateway %s/%s: %w", namespace, name, err)
}
c.logger.Info("syncing Gateway", "namespace", gw.Namespace, "name", gw.Name)
c.gatewayEventChan <- event.GenericEvent{Object: &gw}
+ return nil
}
func (c *AIGatewayRouteController) backend(ctx context.Context, namespace, name string) (*aigv1b1.AIServiceBackend, error) {
diff --git a/internal/controller/ai_gateway_route_test.go b/internal/controller/ai_gateway_route_test.go
index 29a3f53cea..e3a3d58069 100644
--- a/internal/controller/ai_gateway_route_test.go
+++ b/internal/controller/ai_gateway_route_test.go
@@ -13,6 +13,7 @@ import (
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
fake2 "k8s.io/client-go/kubernetes/fake"
@@ -420,7 +421,110 @@ func TestAIGatewayRouterController_syncGateway_notFound(t *testing.T) { // This
kube := fake2.NewClientset()
eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
s := NewAIGatewayRouteController(fakeClient, kube, logr.Discard(), eventCh.Ch, "/v1")
- s.syncGateway(t.Context(), "ns", "non-exist")
+ err := s.syncGateway(context.Background(), "ns", "non-exist")
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "not found")
+}
+
+func TestAIGatewayRouteController_Reconcile_GatewayNotFound(t *testing.T) {
+ fakeClient := requireNewFakeClientWithIndexes(t)
+ kube := fake2.NewClientset()
+ eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
+ c := NewAIGatewayRouteController(fakeClient, kube, ctrl.Log, eventCh.Ch, "/v1")
+
+ // Create AIGatewayRoute referencing a non-existent gateway.
+ route := &aigv1b1.AIGatewayRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "broken-route",
+ Namespace: "default",
+ },
+ Spec: aigv1b1.AIGatewayRouteSpec{
+ ParentRefs: []gwapiv1.ParentReference{{Name: gwapiv1.ObjectName("non-existent")}},
+ },
+ }
+ err := fakeClient.Create(t.Context(), route)
+ require.NoError(t, err)
+
+ // Reconcile should fail and mark status as NotAccepted.
+ _, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "broken-route"}})
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "non-existent")
+
+ // Verify the AIGatewayRoute status is NotAccepted.
+ var current aigv1b1.AIGatewayRoute
+ err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "broken-route"}, ¤t)
+ require.NoError(t, err)
+ require.Len(t, current.Status.Conditions, 1)
+ require.Equal(t, aigv1b1.ConditionTypeNotAccepted, current.Status.Conditions[0].Type)
+ require.Contains(t, current.Status.Conditions[0].Message, "not found")
+
+ // create the gateway now so that the reconcile succeeds.
+ err = fakeClient.Create(t.Context(), &gwapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "non-existent", Namespace: "default"}})
+ require.NoError(t, err)
+
+ // Reconcile should succeed.
+ _, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "broken-route"}})
+ require.NoError(t, err)
+
+ // Verify the AIGatewayRoute status is Accepted.
+ err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "broken-route"}, ¤t)
+ require.NoError(t, err)
+ require.Len(t, current.Status.Conditions, 1)
+ require.Equal(t, aigv1b1.ConditionTypeAccepted, current.Status.Conditions[0].Type)
+ require.Contains(t, current.Status.Conditions[0].Message, "reconciled successfully")
+}
+
+func TestAIGatewayRouteController_syncGateway_DeletionWithMissingGateway(t *testing.T) {
+ fakeClient := requireNewFakeClientWithIndexes(t)
+ kube := fake2.NewClientset()
+ eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
+ c := NewAIGatewayRouteController(fakeClient, kube, ctrl.Log, eventCh.Ch, "/v1")
+
+ // Create the gateway first so that the initial reconcile succeeds.
+ err := fakeClient.Create(t.Context(), &gwapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "temp-gw", Namespace: "default"}})
+ require.NoError(t, err)
+
+ route := &aigv1b1.AIGatewayRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "route-to-delete",
+ Namespace: "default",
+ },
+ Spec: aigv1b1.AIGatewayRouteSpec{
+ ParentRefs: []gwapiv1.ParentReference{{Name: gwapiv1.ObjectName("temp-gw")}},
+ },
+ }
+ err = fakeClient.Create(t.Context(), route)
+ require.NoError(t, err)
+
+ // Initial reconcile to add the finalizer.
+ _, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "route-to-delete"}})
+ require.NoError(t, err)
+
+ // Verify finalizer is present.
+ var current aigv1b1.AIGatewayRoute
+ err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "route-to-delete"}, ¤t)
+ require.NoError(t, err)
+ require.Contains(t, current.Finalizers, aiGatewayControllerFinalizer)
+
+ // Now delete the gateway (simulating it being removed before the AIGatewayRoute).
+ err = fakeClient.Delete(t.Context(), &gwapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "temp-gw", Namespace: "default"}})
+ require.NoError(t, err)
+
+ // Delete the AIGatewayRoute.
+ err = fakeClient.Delete(t.Context(), ¤t)
+ require.NoError(t, err)
+
+ // Reconcile the deletion — should succeed even though the gateway is gone.
+ _, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "route-to-delete"}})
+ require.NoError(t, err)
+
+ // Verify the AIGatewayRoute finalizer has been removed (object should be gone or have no finalizer).
+ err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "route-to-delete"}, ¤t)
+ if err == nil {
+ require.NotContains(t, current.Finalizers, aiGatewayControllerFinalizer)
+ } else {
+ require.True(t, apierrors.IsNotFound(err))
+ }
}
func Test_newHTTPRoute_InferencePool(t *testing.T) {
diff --git a/internal/controller/mcp_route.go b/internal/controller/mcp_route.go
index fe10b1b92c..94fbeeb96f 100644
--- a/internal/controller/mcp_route.go
+++ b/internal/controller/mcp_route.go
@@ -472,24 +472,30 @@ func (c *MCPRouteController) syncGateways(ctx context.Context, mcpRoute *aigv1b1
if p.Namespace != nil {
gwNamespace = string(*p.Namespace)
}
- c.syncGateway(ctx, gwNamespace, string(p.Name))
+ if err := c.syncGateway(ctx, gwNamespace, string(p.Name)); err != nil {
+ if mcpRoute.DeletionTimestamp != nil && apierrors.IsNotFound(err) {
+ continue
+ }
+ return err
+ }
}
return nil
}
// syncGateway is a helper function for syncGateways that sends one GenericEvent to the gateway controller.
-func (c *MCPRouteController) syncGateway(ctx context.Context, namespace, name string) {
+func (c *MCPRouteController) syncGateway(ctx context.Context, namespace, name string) error {
var gw gwapiv1.Gateway
if err := c.client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &gw); err != nil {
if apierrors.IsNotFound(err) {
c.logger.Info("Gateway not found", "namespace", namespace, "name", name)
- return
+ return fmt.Errorf("gateway %s/%s not found: %w", namespace, name, err)
}
c.logger.Error(err, "failed to get Gateway", "namespace", namespace, "name", name)
- return
+ return fmt.Errorf("failed to get Gateway %s/%s: %w", namespace, name, err)
}
c.logger.Info("Syncing Gateway", "namespace", gw.Namespace, "name", gw.Name)
c.gatewayEventChan <- event.GenericEvent{Object: &gw}
+ return nil
}
// updateMCPRouteStatus updates the status of the MCPRoute.
diff --git a/internal/controller/mcp_route_test.go b/internal/controller/mcp_route_test.go
index 40d0b574f2..6179d585c1 100644
--- a/internal/controller/mcp_route_test.go
+++ b/internal/controller/mcp_route_test.go
@@ -272,7 +272,9 @@ func TestMCPRouteController_syncGateway_notFound(t *testing.T) { // coverage for
fakeClient := requireNewFakeClientWithIndexesForMCP(t)
eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
s := NewMCPRouteController(fakeClient, fakekube.NewClientset(), logr.Discard(), eventCh.Ch)
- s.syncGateway(context.Background(), "ns", "non-exist")
+ err := s.syncGateway(context.Background(), "ns", "non-exist")
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "not found")
}
func TestMCPRouteController_mcpRuleWithAPIKeyBackendSecurity(t *testing.T) {
@@ -696,3 +698,118 @@ func TestMCPRouteController_syncGateways_NamespaceCrossReference(t *testing.T) {
require.Equal(t, "gateway2", gateways[1].Name)
require.Equal(t, "other-ns", gateways[1].Namespace)
}
+
+func TestMCPRouteController_Reconcile_GatewayNotFound(t *testing.T) {
+ fakeClient := requireNewFakeClientWithIndexesForMCP(t)
+ eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
+ c := NewMCPRouteController(fakeClient, fakekube.NewClientset(), ctrl.Log, eventCh.Ch)
+
+ // Create MCPRoute referencing a non-existent gateway.
+ route := &aigv1b1.MCPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "broken-route",
+ Namespace: "default",
+ },
+ Spec: aigv1b1.MCPRouteSpec{
+ ParentRefs: []gwapiv1.ParentReference{{Name: gwapiv1.ObjectName("non-existent")}},
+ BackendRefs: []aigv1b1.MCPRouteBackendRef{
+ {
+ BackendObjectReference: gwapiv1.BackendObjectReference{
+ Name: "svc-a",
+ Namespace: ptr.To(gwapiv1.Namespace("default")),
+ },
+ },
+ },
+ },
+ }
+ err := fakeClient.Create(t.Context(), route)
+ require.NoError(t, err)
+
+ // Reconcile should fail and mark status as NotAccepted.
+ _, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "broken-route"}})
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "non-existent")
+
+ // Verify the MCPRoute status is NotAccepted.
+ var current aigv1b1.MCPRoute
+ err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "broken-route"}, ¤t)
+ require.NoError(t, err)
+ require.Len(t, current.Status.Conditions, 1)
+ require.Equal(t, aigv1b1.ConditionTypeNotAccepted, current.Status.Conditions[0].Type)
+ require.Contains(t, current.Status.Conditions[0].Message, "not found")
+
+ // create the gateway now so that the reconcile succeeds.
+ err = fakeClient.Create(t.Context(), &gwapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "non-existent", Namespace: "default"}})
+ require.NoError(t, err)
+
+ // Reconcile should succeed.
+ _, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "broken-route"}})
+ require.NoError(t, err)
+
+ // Verify the MCPRoute status is Accepted.
+ err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "broken-route"}, ¤t)
+ require.NoError(t, err)
+ require.Len(t, current.Status.Conditions, 1)
+ require.Equal(t, aigv1b1.ConditionTypeAccepted, current.Status.Conditions[0].Type)
+ require.Contains(t, current.Status.Conditions[0].Message, "reconciled successfully")
+}
+
+func TestMCPRouteController_Reconcile_DeletionWithMissingGateway(t *testing.T) {
+ fakeClient := requireNewFakeClientWithIndexesForMCP(t)
+ eventCh := internaltesting.NewControllerEventChan[*gwapiv1.Gateway]()
+ c := NewMCPRouteController(fakeClient, fakekube.NewClientset(), ctrl.Log, eventCh.Ch)
+
+ // Create the gateway first so that the initial reconcile succeeds.
+ err := fakeClient.Create(t.Context(), &gwapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "temp-gw", Namespace: "default"}})
+ require.NoError(t, err)
+
+ route := &aigv1b1.MCPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "route-to-delete",
+ Namespace: "default",
+ },
+ Spec: aigv1b1.MCPRouteSpec{
+ ParentRefs: []gwapiv1.ParentReference{{Name: gwapiv1.ObjectName("temp-gw")}},
+ BackendRefs: []aigv1b1.MCPRouteBackendRef{
+ {
+ BackendObjectReference: gwapiv1.BackendObjectReference{
+ Name: "svc-a",
+ Namespace: ptr.To(gwapiv1.Namespace("default")),
+ },
+ },
+ },
+ },
+ }
+ err = fakeClient.Create(t.Context(), route)
+ require.NoError(t, err)
+
+ // Initial reconcile to add the finalizer.
+ _, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "route-to-delete"}})
+ require.NoError(t, err)
+
+ // Verify finalizer is present.
+ var current aigv1b1.MCPRoute
+ err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "route-to-delete"}, ¤t)
+ require.NoError(t, err)
+ require.Contains(t, current.Finalizers, aiGatewayControllerFinalizer)
+
+ // Now delete the gateway (simulating it being removed before the MCPRoute).
+ err = fakeClient.Delete(t.Context(), &gwapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "temp-gw", Namespace: "default"}})
+ require.NoError(t, err)
+
+ // Delete the MCPRoute.
+ err = fakeClient.Delete(t.Context(), ¤t)
+ require.NoError(t, err)
+
+ // Reconcile the deletion — should succeed even though the gateway is gone.
+ _, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "route-to-delete"}})
+ require.NoError(t, err)
+
+ // Verify the MCPRoute finalizer has been removed (object should be gone or have no finalizer).
+ err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "route-to-delete"}, ¤t)
+ if err == nil {
+ require.NotContains(t, current.Finalizers, aiGatewayControllerFinalizer)
+ } else {
+ require.True(t, apierrors.IsNotFound(err))
+ }
+}
From c304288a84171cd1c62a93e726ac9daeb637fce1 Mon Sep 17 00:00:00 2001
From: Linus Schlumberger
Date: Wed, 10 Jun 2026 19:54:18 +0200
Subject: [PATCH 07/29] fix: promote role:system messages from messages array
to system param in Bedrock translator (#2189)
**Description**
In the Anthropic-to-AWS-Bedrock translator, Claude Code with
mid-conversation-system beta sends system prompts as {"role": "system"}
in the messages array. The SDK has no system role constant so these hit
the default error case. This promotes them to the top-level system param
before conversion.
Fixes #2206
Signed-off-by: Linus Schlumberger
Co-authored-by: Ignasi Barrera
Signed-off-by: yxia216
---
internal/translator/anthropic_awsbedrock.go | 39 ++++++-
.../translator/anthropic_awsbedrock_test.go | 103 ++++++++++++++++++
2 files changed, 139 insertions(+), 3 deletions(-)
diff --git a/internal/translator/anthropic_awsbedrock.go b/internal/translator/anthropic_awsbedrock.go
index 1266b920dd..5bcc532164 100644
--- a/internal/translator/anthropic_awsbedrock.go
+++ b/internal/translator/anthropic_awsbedrock.go
@@ -66,11 +66,16 @@ func (a *anthropicToAWSBedrockTranslator) RequestBody(_ []byte, body *anthropics
var bedrockReq awsbedrock.ConverseInput
+ // Promote any role: "system" messages from the messages array to the top-level system field.
+ // This handles clients (e.g. Claude Code with mid-conversation-system beta) that send
+ // system prompts as messages rather than the top-level parameter.
+ messages := promoteAnthropicSystemMessagesToParam(body)
+
// Convert messages.
- bedrockReq.Messages = make([]*awsbedrock.Message, 0, len(body.Messages))
- msgLen := len(body.Messages)
+ bedrockReq.Messages = make([]*awsbedrock.Message, 0, len(messages))
+ msgLen := len(messages)
for i := 0; i < msgLen; {
- msg := &body.Messages[i]
+ msg := &messages[i]
switch msg.Role {
case anthropicschema.MessageRoleUser:
bedrockMsg, convErr := a.convertUserMessage(msg)
@@ -162,6 +167,34 @@ func (a *anthropicToAWSBedrockTranslator) RequestBody(_ []byte, body *anthropics
return
}
+// promoteAnthropicSystemMessagesToParam promotes role: "system" messages from the messages
+// array to the top-level system parameter. Returns the filtered messages (without system messages).
+// This handles clients (e.g. Claude Code with mid-conversation-system beta) that send
+// system prompts as messages rather than the top-level parameter.
+func promoteAnthropicSystemMessagesToParam(body *anthropicschema.MessagesRequest) []anthropicschema.MessageParam {
+ var systemTexts []string
+ var filtered []anthropicschema.MessageParam
+ for _, msg := range body.Messages {
+ if msg.Role == "system" {
+ if msg.Content.Text != "" {
+ systemTexts = append(systemTexts, msg.Content.Text)
+ }
+ for i := range msg.Content.Array {
+ if msg.Content.Array[i].Text != nil && msg.Content.Array[i].Text.Text != "" {
+ systemTexts = append(systemTexts, msg.Content.Array[i].Text.Text)
+ }
+ }
+ } else {
+ filtered = append(filtered, msg)
+ }
+ }
+ if len(systemTexts) > 0 {
+ systemText := strings.Join(systemTexts, "\n")
+ body.System = &anthropicschema.SystemPrompt{Text: systemText}
+ }
+ return filtered
+}
+
func hasToolResult(msg *anthropicschema.MessageParam) bool {
if msg.Role != anthropicschema.MessageRoleUser {
return false
diff --git a/internal/translator/anthropic_awsbedrock_test.go b/internal/translator/anthropic_awsbedrock_test.go
index c382d4e814..29f8cb92fb 100644
--- a/internal/translator/anthropic_awsbedrock_test.go
+++ b/internal/translator/anthropic_awsbedrock_test.go
@@ -1234,3 +1234,106 @@ func TestAnthropicToAWSBedrockTranslator_ResponseBody_StreamingToolUse(t *testin
assert.Contains(t, bodyStr, `"input_json_delta"`)
assert.Contains(t, bodyStr, `"tool_use"`)
}
+
+func TestPromoteAnthropicSystemMessagesToParam(t *testing.T) {
+ t.Run("no system messages returns all messages", func(t *testing.T) {
+ body := &anthropicschema.MessagesRequest{
+ Messages: []anthropicschema.MessageParam{
+ {Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hello"}},
+ },
+ }
+ result := promoteAnthropicSystemMessagesToParam(body)
+ require.Len(t, result, 1)
+ require.Nil(t, body.System)
+ })
+
+ t.Run("single text system message is promoted", func(t *testing.T) {
+ body := &anthropicschema.MessagesRequest{
+ Messages: []anthropicschema.MessageParam{
+ {Role: "system", Content: anthropicschema.MessageContent{Text: "You are helpful."}},
+ {Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hi"}},
+ },
+ }
+ result := promoteAnthropicSystemMessagesToParam(body)
+ require.Len(t, result, 1)
+ require.Equal(t, "user", string(result[0].Role))
+ require.NotNil(t, body.System)
+ require.Equal(t, "You are helpful.", body.System.Text)
+ })
+
+ t.Run("system message with content blocks is promoted", func(t *testing.T) {
+ body := &anthropicschema.MessagesRequest{
+ Messages: []anthropicschema.MessageParam{
+ {
+ Role: "system",
+ Content: anthropicschema.MessageContent{
+ Array: []anthropicschema.ContentBlockParam{
+ {Text: &anthropicschema.TextBlockParam{Text: "You are Claude."}},
+ },
+ },
+ },
+ {Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hi"}},
+ },
+ }
+ result := promoteAnthropicSystemMessagesToParam(body)
+ require.Len(t, result, 1)
+ require.NotNil(t, body.System)
+ require.Equal(t, "You are Claude.", body.System.Text)
+ })
+
+ t.Run("multiple system messages are joined", func(t *testing.T) {
+ body := &anthropicschema.MessagesRequest{
+ Messages: []anthropicschema.MessageParam{
+ {Role: "system", Content: anthropicschema.MessageContent{Text: "Be concise."}},
+ {Role: "system", Content: anthropicschema.MessageContent{Text: "Be accurate."}},
+ {Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hello"}},
+ },
+ }
+ result := promoteAnthropicSystemMessagesToParam(body)
+ require.Len(t, result, 1)
+ require.NotNil(t, body.System)
+ require.Equal(t, "Be concise.\nBe accurate.", body.System.Text)
+ })
+
+ t.Run("system message is added to existing system param", func(t *testing.T) {
+ body := &anthropicschema.MessagesRequest{
+ System: &anthropicschema.SystemPrompt{Text: "You are Claude."},
+ Messages: []anthropicschema.MessageParam{
+ {Role: "system", Content: anthropicschema.MessageContent{Text: "Be concise."}},
+ {Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hi"}},
+ },
+ }
+ result := promoteAnthropicSystemMessagesToParam(body)
+ require.Len(t, result, 1)
+ require.NotNil(t, body.System)
+ // The existing system param is overwritten by the promoted messages.
+ require.Equal(t, "Be concise.", body.System.Text)
+ })
+
+ t.Run("integration with full request", func(t *testing.T) {
+ translator := NewAnthropicToAWSBedrockTranslator("")
+ req := &anthropicschema.MessagesRequest{
+ Model: "anthropic.claude-3-sonnet-20240229-v1:0",
+ MaxTokens: 1024,
+ Messages: []anthropicschema.MessageParam{
+ {Role: "system", Content: anthropicschema.MessageContent{Text: "You are helpful."}},
+ {Role: anthropicschema.MessageRoleUser, Content: anthropicschema.MessageContent{Text: "Hello"}},
+ },
+ }
+ rawBody, err := json.Marshal(req)
+ require.NoError(t, err)
+
+ _, body, err := translator.RequestBody(rawBody, req, false)
+ require.NoError(t, err)
+
+ var bedrockReq awsbedrock.ConverseInput
+ err = json.Unmarshal(body, &bedrockReq)
+ require.NoError(t, err)
+
+ require.Len(t, bedrockReq.Messages, 1)
+ assert.Equal(t, awsbedrock.ConversationRoleUser, bedrockReq.Messages[0].Role)
+ require.NotNil(t, bedrockReq.System)
+ require.Len(t, bedrockReq.System, 1)
+ assert.Equal(t, "You are helpful.", *bedrockReq.System[0].Text)
+ })
+}
From 5d691db974a595f4c82e90f21f9f49ae81473a9c Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 10 Jun 2026 18:13:40 +0000
Subject: [PATCH 08/29] =?UTF-8?q?translator:=20handle=20numeric=20OpenAI?=
=?UTF-8?q?=20`error.code`=20in=20Anthropic=E2=86=92OpenAI=20error=20trans?=
=?UTF-8?q?lation=20(#2161)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**Description**
Anthropic→OpenAI error translation could fail with an internal 500 when
an OpenAI-compatible upstream returned a structured JSON error where
`error.code` was numeric (e.g. `400`) instead of string. This change
makes the error model tolerant to both representations so upstream 4xx
diagnostics can still be translated and surfaced to Anthropic clients.
- **Schema decoding hardening (`internal/apischema/openai/openai.go`)**
- Added `UnmarshalJSON` for `openai.ErrorType`.
- `error.code` now accepts:
- JSON string (`"code":"400"`)
- JSON number (`"code":400`)
- `null` / omitted
- Numeric values are normalized to the existing internal `*string`
field.
- **Translator regression coverage
(`internal/translator/anthropic_openai_test.go`)**
- Extended `TestAnthropicToOpenAITranslator_ResponseError` with a JSON
error fixture containing numeric `code`.
- Confirms response still maps to Anthropic error envelope with expected
`type` and `message`.
```json
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "Bad request",
"param": null,
"code": 400
}
}
```
**Related Issues/PRs (if applicable)**
Fixes https://github.com/envoyproxy/ai-gateway/issues/2151
**Special notes for reviewers (if applicable)**
N/A
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Ignasi Barrera
Signed-off-by: yxia216
---
internal/apischema/openai/openai.go | 41 ++++++++++++++++++++
internal/translator/anthropic_openai_test.go | 7 ++++
2 files changed, 48 insertions(+)
diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go
index 865205d2ba..14fdbb6d41 100644
--- a/internal/apischema/openai/openai.go
+++ b/internal/apischema/openai/openai.go
@@ -1662,6 +1662,47 @@ type ErrorType struct {
EventID *string `json:"event_id,omitempty"`
}
+// UnmarshalJSON allows OpenAI-compatible backends to provide `error.code` as either a JSON string or number.
+func (e *ErrorType) UnmarshalJSON(data []byte) error {
+ type errorTypeAlias ErrorType
+ aux := struct {
+ errorTypeAlias
+ Code json.RawMessage `json:"code,omitempty"`
+ }{}
+
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+
+ *e = ErrorType(aux.errorTypeAlias)
+ if len(aux.Code) == 0 || string(aux.Code) == "null" {
+ e.Code = nil
+ return nil
+ }
+
+ var code string
+ if err := json.Unmarshal(aux.Code, &code); err == nil {
+ e.Code = &code
+ return nil
+ }
+
+ var codeInt int64
+ if err := json.Unmarshal(aux.Code, &codeInt); err == nil {
+ code = strconv.FormatInt(codeInt, 10)
+ e.Code = &code
+ return nil
+ }
+
+ var codeFloat float64
+ if err := json.Unmarshal(aux.Code, &codeFloat); err == nil {
+ code = strconv.FormatFloat(codeFloat, 'f', -1, 64)
+ e.Code = &code
+ return nil
+ }
+
+ return fmt.Errorf("error.code must be string or number")
+}
+
// ModelList is described in the OpenAI API documentation
// https://platform.openai.com/docs/api-reference/models/list
type ModelList struct {
diff --git a/internal/translator/anthropic_openai_test.go b/internal/translator/anthropic_openai_test.go
index 18fd4b3b77..b5ce7eb699 100644
--- a/internal/translator/anthropic_openai_test.go
+++ b/internal/translator/anthropic_openai_test.go
@@ -430,6 +430,13 @@ func TestAnthropicToOpenAITranslator_ResponseError(t *testing.T) {
wantErrType: "invalid_request_error",
wantErrMsg: "Bad request",
},
+ {
+ name: "JSON error with numeric code from OpenAI-compatible backend",
+ headers: map[string]string{contentTypeHeaderName: "application/json"},
+ body: `{"type":"error","error":{"type":"invalid_request_error","message":"Bad request","param":null,"code":400}}`,
+ wantErrType: "invalid_request_error",
+ wantErrMsg: "Bad request",
+ },
{
name: "non-JSON 400 error",
headers: map[string]string{statusHeaderName: "400", contentTypeHeaderName: "text/plain"},
From c5b3f0d546ce7bfde91871a5c162991e7ff5d53e Mon Sep 17 00:00:00 2001
From: Ignasi Barrera
Date: Wed, 10 Jun 2026 20:19:22 +0200
Subject: [PATCH 09/29] site: fix vars for 0.7 and main (#2217)
**Description**
Fix the vars for the 0.7 and main releases.
**Related Issues/PRs (if applicable)**
N/A
**Special notes for reviewers (if applicable)**
N/A
Signed-off-by: Ignasi Barrera
Signed-off-by: yxia216
---
site/docs/_vars.json | 2 +-
site/docs/compatibility.md | 3 ++-
site/versioned_docs/version-0.7/_vars.json | 6 +++---
site/versioned_docs/version-0.7/compatibility.md | 3 ++-
4 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/site/docs/_vars.json b/site/docs/_vars.json
index 8541486ead..9abc53f7bd 100644
--- a/site/docs/_vars.json
+++ b/site/docs/_vars.json
@@ -2,6 +2,6 @@
"aigwVersion": "0.0.0-latest",
"aigwGitRef": "main",
"egVersion": "0.0.0-latest",
- "egMinVersion": "1.7.0",
+ "egMinVersion": "1.8.1",
"k8sMinVersion": "1.32"
}
diff --git a/site/docs/compatibility.md b/site/docs/compatibility.md
index 84d9836d02..68c73de85e 100644
--- a/site/docs/compatibility.md
+++ b/site/docs/compatibility.md
@@ -10,7 +10,8 @@ This document provides compatibility information for Envoy AI Gateway releases w
| AI Gateway | Envoy Gateway | Kubernetes | Gateway API | Support Status |
| ---------- | ----------------------------- | ---------- | ----------- | -------------- |
-| main | v1.8.1+ (Envoy Proxy v1.38.1) | v1.32+ | v1.4.x | Development |
+| main | v1.7.x+ (Envoy Proxy v1.38.x) | v1.32+ | v1.5.x | Development |
+| v0.7.x | v1.8.x+ (Envoy Proxy v1.38.x) | v1.32+ | v1.5.x | Supported |
| v0.6.x | v1.7.x+ (Envoy Proxy v1.37.x) | v1.32+ | v1.4.x | Supported |
| v0.5.x | v1.6.x+ (Envoy Proxy v1.35.x) | v1.32+ | v1.4.x | Supported |
| others | N/A | N/A | N/A | End of Life |
diff --git a/site/versioned_docs/version-0.7/_vars.json b/site/versioned_docs/version-0.7/_vars.json
index 8541486ead..8b01e6b7b6 100644
--- a/site/versioned_docs/version-0.7/_vars.json
+++ b/site/versioned_docs/version-0.7/_vars.json
@@ -1,7 +1,7 @@
{
- "aigwVersion": "0.0.0-latest",
+ "aigwVersion": "0.7.0",
"aigwGitRef": "main",
- "egVersion": "0.0.0-latest",
- "egMinVersion": "1.7.0",
+ "egVersion": "1.8.1",
+ "egMinVersion": "1.8.1",
"k8sMinVersion": "1.32"
}
diff --git a/site/versioned_docs/version-0.7/compatibility.md b/site/versioned_docs/version-0.7/compatibility.md
index ea4cc4f378..68c73de85e 100644
--- a/site/versioned_docs/version-0.7/compatibility.md
+++ b/site/versioned_docs/version-0.7/compatibility.md
@@ -10,7 +10,8 @@ This document provides compatibility information for Envoy AI Gateway releases w
| AI Gateway | Envoy Gateway | Kubernetes | Gateway API | Support Status |
| ---------- | ----------------------------- | ---------- | ----------- | -------------- |
-| main | v1.7.x+ (Envoy Proxy v1.37.x) | v1.32+ | v1.4.x | Development |
+| main | v1.7.x+ (Envoy Proxy v1.38.x) | v1.32+ | v1.5.x | Development |
+| v0.7.x | v1.8.x+ (Envoy Proxy v1.38.x) | v1.32+ | v1.5.x | Supported |
| v0.6.x | v1.7.x+ (Envoy Proxy v1.37.x) | v1.32+ | v1.4.x | Supported |
| v0.5.x | v1.6.x+ (Envoy Proxy v1.35.x) | v1.32+ | v1.4.x | Supported |
| others | N/A | N/A | N/A | End of Life |
From 855df948d6d34d4ce86b67e04c67f20d88b3b8f3 Mon Sep 17 00:00:00 2001
From: hustxiayang
Date: Wed, 10 Jun 2026 18:25:39 -0400
Subject: [PATCH 10/29] feat: add completion tokens details for anthropic
models (#2199)
**Description**
Anthropic recently exposed the reasoning tokens via
`output_tokens_details`:
https://platform.claude.com/docs/en/api/python/messages/create. Thus we
should also added this usage information for our users.
---------
Signed-off-by: yxia216
Co-authored-by: Ignasi Barrera
Signed-off-by: yxia216
---
go.mod | 2 +-
go.sum | 4 ++--
internal/translator/anthropic_helper.go | 10 ++++++++++
internal/translator/openai_awsanthropic_test.go | 4 +++-
internal/translator/openai_gcpanthropic_test.go | 7 ++++++-
tests/data-plane/testupstream_test.go | 10 +++++-----
6 files changed, 27 insertions(+), 10 deletions(-)
diff --git a/go.mod b/go.mod
index 649c13301d..cd324a4e1e 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
github.com/a8m/envsubst v1.4.3
github.com/alecthomas/kong v1.15.0
github.com/andybalholm/brotli v1.2.1
- github.com/anthropics/anthropic-sdk-go v1.45.0
+ github.com/anthropics/anthropic-sdk-go v1.46.0
github.com/aws/aws-sdk-go-v2 v1.41.7
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10
github.com/aws/aws-sdk-go-v2/config v1.32.18
diff --git a/go.sum b/go.sum
index 29b5271c7e..381df5b9b4 100644
--- a/go.sum
+++ b/go.sum
@@ -43,8 +43,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
-github.com/anthropics/anthropic-sdk-go v1.45.0 h1:rWnpyBpm9OAm97jyH5bi6W4SRCwJeNY/RyhaJ7CHSUI=
-github.com/anthropics/anthropic-sdk-go v1.45.0/go.mod h1:bx5vWuHFuGPkELH8Z4KUiNSohFnUwScdpTyr+50myPo=
+github.com/anthropics/anthropic-sdk-go v1.46.0 h1:yl3n+el5ZfNgiCtQ7zQ7s/NXxB11YbrKXdc3uLPNWlU=
+github.com/anthropics/anthropic-sdk-go v1.46.0/go.mod h1:bx5vWuHFuGPkELH8Z4KUiNSohFnUwScdpTyr+50myPo=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
diff --git a/internal/translator/anthropic_helper.go b/internal/translator/anthropic_helper.go
index 5c3be352d6..8d77a3d9b8 100644
--- a/internal/translator/anthropic_helper.go
+++ b/internal/translator/anthropic_helper.go
@@ -864,6 +864,7 @@ func (p *anthropicStreamParser) Process(body io.Reader, endOfStream bool, span t
totalTokens, _ := p.tokenUsage.TotalTokens()
cachedTokens, _ := p.tokenUsage.CachedInputTokens()
cacheCreationTokens, _ := p.tokenUsage.CacheCreationInputTokens()
+ reasoningTokens, _ := p.tokenUsage.ReasoningTokens()
finalChunk := openai.ChatCompletionResponseChunk{
ID: p.activeMessageID,
Created: p.created,
@@ -877,6 +878,9 @@ func (p *anthropicStreamParser) Process(body io.Reader, endOfStream bool, span t
CachedTokens: int(cachedTokens),
CacheCreationTokens: int(cacheCreationTokens),
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{
+ ReasoningTokens: int(reasoningTokens),
+ },
},
Model: p.requestModel,
}
@@ -1060,6 +1064,7 @@ func (p *anthropicStreamParser) handleAnthropicStreamEvent(eventType []byte, dat
if cacheCreation, ok := usage.CacheCreationInputTokens(); ok {
p.tokenUsage.AddCacheCreationInputTokens(cacheCreation)
}
+ p.tokenUsage.SetReasoningTokens(uint32(u.OutputTokensDetails.ThinkingTokens)) //nolint:gosec
if event.Delta.StopReason != "" {
p.stopReason = event.Delta.StopReason
}
@@ -1174,11 +1179,13 @@ func messageToChatCompletion(anthropicResp *anthropic.Message, responseModel int
&usage.CacheReadInputTokens,
&usage.CacheCreationInputTokens,
)
+ tokenUsage.SetReasoningTokens(uint32(usage.OutputTokensDetails.ThinkingTokens)) //nolint:gosec
inputTokens, _ := tokenUsage.InputTokens()
outputTokens, _ := tokenUsage.OutputTokens()
totalTokens, _ := tokenUsage.TotalTokens()
cachedTokens, _ := tokenUsage.CachedInputTokens()
cacheCreationTokens, _ := tokenUsage.CacheCreationInputTokens()
+ reasoningTokens, _ := tokenUsage.ReasoningTokens()
openAIResp.Usage = openai.Usage{
CompletionTokens: int(outputTokens),
PromptTokens: int(inputTokens),
@@ -1187,6 +1194,9 @@ func messageToChatCompletion(anthropicResp *anthropic.Message, responseModel int
CachedTokens: int(cachedTokens),
CacheCreationTokens: int(cacheCreationTokens),
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{
+ ReasoningTokens: int(reasoningTokens),
+ },
}
finishReason, err := anthropicToOpenAIFinishReason(anthropicResp.StopReason)
diff --git a/internal/translator/openai_awsanthropic_test.go b/internal/translator/openai_awsanthropic_test.go
index 2af9792147..5e87f449c9 100644
--- a/internal/translator/openai_awsanthropic_test.go
+++ b/internal/translator/openai_awsanthropic_test.go
@@ -333,6 +333,7 @@ func TestOpenAIToAWSAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
PromptTokensDetails: &openai.PromptTokensDetails{
CachedTokens: 5,
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{},
},
Choices: []openai.ChatCompletionResponseChoice{
{
@@ -367,6 +368,7 @@ func TestOpenAIToAWSAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
PromptTokensDetails: &openai.PromptTokensDetails{
CachedTokens: 10,
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{},
},
Choices: []openai.ChatCompletionResponseChoice{
{
@@ -420,7 +422,7 @@ func TestOpenAIToAWSAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
int32(tt.expectedOpenAIResponse.Usage.PromptTokensDetails.CacheCreationTokens), // nolint:gosec
int32(tt.expectedOpenAIResponse.Usage.CompletionTokens), // nolint:gosec
int32(tt.expectedOpenAIResponse.Usage.TotalTokens), // nolint:gosec
- -1,
+ int32(tt.expectedOpenAIResponse.Usage.CompletionTokensDetails.ReasoningTokens), // nolint:gosec
)
require.Equal(t, expectedTokenUsage, usedToken)
diff --git a/internal/translator/openai_gcpanthropic_test.go b/internal/translator/openai_gcpanthropic_test.go
index ab18539510..769b2dd1fa 100644
--- a/internal/translator/openai_gcpanthropic_test.go
+++ b/internal/translator/openai_gcpanthropic_test.go
@@ -443,6 +443,7 @@ func TestOpenAIToGCPAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
CachedTokens: 5,
CacheCreationTokens: 3,
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{},
},
Choices: []openai.ChatCompletionResponseChoice{
{
@@ -478,6 +479,7 @@ func TestOpenAIToGCPAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
CachedTokens: 10,
CacheCreationTokens: 7,
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{},
},
Choices: []openai.ChatCompletionResponseChoice{
{
@@ -524,6 +526,7 @@ func TestOpenAIToGCPAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
PromptTokensDetails: &openai.PromptTokensDetails{
CachedTokens: 2,
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{},
},
Choices: []openai.ChatCompletionResponseChoice{
{
@@ -557,6 +560,7 @@ func TestOpenAIToGCPAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
PromptTokensDetails: &openai.PromptTokensDetails{
CachedTokens: 3,
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{},
},
Choices: []openai.ChatCompletionResponseChoice{
{
@@ -602,6 +606,7 @@ func TestOpenAIToGCPAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
PromptTokensDetails: &openai.PromptTokensDetails{
CachedTokens: 1,
},
+ CompletionTokensDetails: &openai.CompletionTokensDetails{},
},
Choices: []openai.ChatCompletionResponseChoice{
{
@@ -651,7 +656,7 @@ func TestOpenAIToGCPAnthropicTranslatorV1ChatCompletion_ResponseBody(t *testing.
int32(tt.expectedOpenAIResponse.Usage.PromptTokensDetails.CacheCreationTokens), // nolint:gosec
int32(tt.expectedOpenAIResponse.Usage.CompletionTokens), // nolint:gosec
int32(tt.expectedOpenAIResponse.Usage.TotalTokens), // nolint:gosec
- -1,
+ int32(tt.expectedOpenAIResponse.Usage.CompletionTokensDetails.ReasoningTokens), // nolint:gosec
)
require.Equal(t, expectedTokenUsage, usedToken)
diff --git a/tests/data-plane/testupstream_test.go b/tests/data-plane/testupstream_test.go
index 5011c17f81..d73bd0aafc 100644
--- a/tests/data-plane/testupstream_test.go
+++ b/tests/data-plane/testupstream_test.go
@@ -290,7 +290,7 @@ func TestWithTestUpstream(t *testing.T) {
requestBody: toolCallResultsRequestBody,
expRequestBody: `{"max_tokens":1024,"messages":[{"content":[{"text":"List the files in the /tmp directory","type":"text"}],"role":"user"},{"content":[{"id":"call_abc123","input":{"path":"/tmp"},"name":"list_files","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"call_abc123","is_error":false,"content":[{"text":"[\"foo.txt\", \"bar.log\", \"data.csv\"]","type":"text"}],"type":"tool_result"}],"role":"user"}],"anthropic_version":"vertex-2023-10-16"}`,
responseBody: `{"id":"msg_123","type":"message","role":"assistant","stop_reason": "end_turn", "content":[{"type":"text","text":"Hello from Anthropic!"}],"usage":{"input_tokens":10,"output_tokens":25,"cache_read_input_tokens":10}}`,
- expResponseBody: `{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hello from Anthropic!","role":"assistant"}}],"created":123, "id":"msg_123","model":"gpt-4-0613","object":"chat.completion","usage":{"completion_tokens":25,"prompt_tokens":20,"total_tokens":45,"prompt_tokens_details":{"cached_tokens":10}}}`,
+ expResponseBody: `{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hello from Anthropic!","role":"assistant"}}],"created":123, "id":"msg_123","model":"gpt-4-0613","object":"chat.completion","usage":{"completion_tokens":25,"completion_tokens_details":{},"prompt_tokens":20,"total_tokens":45,"prompt_tokens_details":{"cached_tokens":10}}}`,
expStatus: http.StatusOK,
},
{
@@ -358,7 +358,7 @@ func TestWithTestUpstream(t *testing.T) {
responseStatus: strconv.Itoa(http.StatusOK),
responseBody: `{"id":"msg_123","type":"message","role":"assistant","stop_reason": "end_turn", "content":[{"type":"text","text":"Hello from Anthropic!"}],"usage":{"input_tokens":10,"output_tokens":25}}`,
expStatus: http.StatusOK,
- expResponseBody: `{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hello from Anthropic!","role":"assistant"}}],"created":123, "id":"msg_123","model":"claude-3-sonnet","object":"chat.completion","usage":{"completion_tokens":25,"prompt_tokens":10,"total_tokens":35,"prompt_tokens_details":{}}}`,
+ expResponseBody: `{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hello from Anthropic!","role":"assistant"}}],"created":123, "id":"msg_123","model":"claude-3-sonnet","object":"chat.completion","usage":{"completion_tokens":25,"completion_tokens_details":{},"prompt_tokens":10,"total_tokens":35,"prompt_tokens_details":{}}}`,
},
{
name: "gcp-anthropicai - /v1/chat/completions - with cache",
@@ -372,7 +372,7 @@ func TestWithTestUpstream(t *testing.T) {
responseStatus: strconv.Itoa(http.StatusOK),
responseBody: `{"id":"msg_123","type":"message","role":"assistant","stop_reason": "end_turn", "content":[{"type":"text","text":"Hello from cached Anthropic!"}],"usage":{"input_tokens":10,"output_tokens":25, "cache_read_input_tokens": 8}}`,
expStatus: http.StatusOK,
- expResponseBody: `{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hello from cached Anthropic!","role":"assistant"}}], "created":123, "id":"msg_123", "model":"claude-3-sonnet","object":"chat.completion","usage":{"completion_tokens":25,"prompt_tokens":18,"total_tokens":43,"prompt_tokens_details":{"cached_tokens":8}}}`,
+ expResponseBody: `{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hello from cached Anthropic!","role":"assistant"}}], "created":123, "id":"msg_123", "model":"claude-3-sonnet","object":"chat.completion","usage":{"completion_tokens":25,"completion_tokens_details":{},"prompt_tokens":18,"total_tokens":43,"prompt_tokens_details":{"cached_tokens":8}}}`,
},
{
name: "modelname-override - /v1/chat/completions",
@@ -578,7 +578,7 @@ data: {"id":"msg_123","choices":[{"index":0,"delta":{"content":" due to Rayleigh
data: {"id":"msg_123","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk"}
-data: {"id":"msg_123","choices":[],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk","usage":{"prompt_tokens":25,"completion_tokens":12,"total_tokens":37,"prompt_tokens_details":{"cached_tokens":10}}}
+data: {"id":"msg_123","choices":[],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk","usage":{"prompt_tokens":25,"completion_tokens":12,"total_tokens":37,"completion_tokens_details":{},"prompt_tokens_details":{"cached_tokens":10}}}
data: [DONE]
@@ -642,7 +642,7 @@ data: {"id":"msg_123","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"i
data: {"id":"msg_123","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk"}
-data: {"id":"msg_123","choices":[],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk","usage":{"prompt_tokens":50,"completion_tokens":20,"total_tokens":70,"prompt_tokens_details":{}}}
+data: {"id":"msg_123","choices":[],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk","usage":{"prompt_tokens":50,"completion_tokens":20,"total_tokens":70,"completion_tokens_details":{},"prompt_tokens_details":{}}}
data: [DONE]
From 48f759f25f0f22683fabc8bbe97631cf4dd5716f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 11 Jun 2026 15:45:39 +0200
Subject: [PATCH 11/29] chore(deps): bump the go group across 1 directory with
12 updates (#2220)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the go group with 3 updates in the / directory:
[go.opentelemetry.io/contrib/exporters/autoexport](https://github.com/open-telemetry/opentelemetry-go-contrib),
[go.opentelemetry.io/contrib/propagators/autoprop](https://github.com/open-telemetry/opentelemetry-go-contrib)
and
[google.golang.org/api](https://github.com/googleapis/google-api-go-client).
Updates `go.opentelemetry.io/contrib/exporters/autoexport` from 0.68.0
to 0.69.0
Release notes
Sourced from go.opentelemetry.io/contrib/exporters/autoexport's
releases.
v1.44.0/v2.5.1/v0.69.0/v0.37.1/v0.24.0/v0.19.0/v0.16.1/v0.16.0
Added
- Add
error.type attribute to
http.client.request.duration for transport failures in
otelhttp. (#8801)
- Add examples for prometheus compatibility document. (#8716)
- Add support for
cardinality_limits in
PeriodicMetricReader in otelconf. (#8885)
- Add
Resource method to SDK in
go.opentelemetry.io/contrib/otelconf/x to expose the
resolved SDK resource from declarative configuration. (#8913)
- Add
go.opentelemetry.io/contrib/detectors/hetzner, a
new resource detector for Hetzner Cloud servers, ported from
github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/hetzner.
Detects cloud.provider, cloud.platform,
cloud.region, cloud.availability_zone,
host.id, and host.name. (#8979)
Changed
- Set error field as
record.SetErr instead of a plain
attribute in
go.opentelemetry.io/contrib/bridges/otellogrus. (#8776)
- Set the "error" field (e.g. created via
zap.Error) as record.SetErr instead of a plain
attribute in go.opentelemetry.io/contrib/bridges/otelzap.
(#8719)
- Set fields implementing
error interface from
slog records as record.SetErr instead of plain
attributes in go.opentelemetry.io/contrib/bridges/otelslog.
(#8774)
- Set emitted errors in
go.opentelemetry.io/contrib/bridges/otellogr as record
errors (Record.SetErr) instead of
exception.message attributes. (#8775)
Fixed
- Fix header attributes lost when using sub-spans in
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace.
(#8797)
- Validate
encoding configuration for OTLP HTTP exporters
in go.opentelemetry.io/contrib/otelconf. (#8772)
- Remove the custom body wrapper from the request's body after the
request is processed to allow body type comparisons with the original
type in
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
and
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux.
(#6914)
- Unknown or empty HTTP methods now report "_OTHER" instead
of "GET" across all HTTP instrumentations to align with
OpenTelemetry semantic conventions. (#8868)
- The default span name formatter in
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
now conforms to the OpenTelemetry HTTP semantic conventions for server
span names. (#8871)
- The default span name is now
{method} {route} (e.g.
GET /foo/{id}) when a route pattern is available, or
{method} (e.g. GET) otherwise.
Removed
- Remove the deprecated
WithSpanOptions option in
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.
(#8991)
What's Changed
... (truncated)
Changelog
Sourced from go.opentelemetry.io/contrib/exporters/autoexport's
changelog.
[1.44.0/2.5.1/0.69.0/0.37.1/0.24.0/0.19.0/0.16.1/0.16.0] -
2026-05-28
Added
- Add
error.type attribute to
http.client.request.duration for transport failures in
otelhttp. (#8801)
- Add examples for prometheus compatibility document. (#8716)
- Add support for
cardinality_limits in
PeriodicMetricReader in otelconf. (#8885)
- Add
Resource method to SDK in
go.opentelemetry.io/contrib/otelconf/x to expose the
resolved SDK resource from declarative configuration. (#8913)
- Add
go.opentelemetry.io/contrib/detectors/hetzner, a
new resource detector for Hetzner Cloud servers, ported from
github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/hetzner.
Detects cloud.provider, cloud.platform,
cloud.region, cloud.availability_zone,
host.id, and host.name. (#8979)
Changed
- Set error field as
record.SetErr instead of a plain
attribute in
go.opentelemetry.io/contrib/bridges/otellogrus. (#8776)
- Set the "error" field (e.g. created via
zap.Error) as record.SetErr instead of a plain
attribute in go.opentelemetry.io/contrib/bridges/otelzap.
(#8719)
- Set fields implementing
error interface from
slog records as record.SetErr instead of plain
attributes in go.opentelemetry.io/contrib/bridges/otelslog.
(#8774)
- Set emitted errors in
go.opentelemetry.io/contrib/bridges/otellogr as record
errors (Record.SetErr) instead of
exception.message attributes. (#8775)
Fixed
- Fix header attributes lost when using sub-spans in
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace.
(#8797)
- Validate
encoding configuration for OTLP HTTP exporters
in go.opentelemetry.io/contrib/otelconf. (#8772)
- Remove the custom body wrapper from the request's body after the
request is processed to allow body type comparisons with the original
type in
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
and
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux.
(#6914)
- Unknown or empty HTTP methods now report "_OTHER" instead
of "GET" across all HTTP instrumentations to align with
OpenTelemetry semantic conventions. (#8868)
- The default span name formatter in
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
now conforms to the OpenTelemetry HTTP semantic conventions for server
span names. (#8871)
- The default span name is now
{method} {route} (e.g.
GET /foo/{id}) when a route pattern is available, or
{method} (e.g. GET) otherwise.
Removed
- Remove the deprecated
WithSpanOptions option in
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.
(#8991)
Commits
03b2bcd
Release v1.44.0/v2.5.1/v0.69.0/v0.37.1/v0.24.0/v0.19.0/v0.16.1/v0.16.0
(#9033)
80c46d4
chore(deps): update module github.com/alecthomas/chroma/v2 to v2.26.0
(#9034)
51f2921
fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.41.2
(#9026)
db82162
fix(deps): update aws-sdk-go-v2 monorepo (#9031)
5a3e533
fix(deps): update module github.com/aws/smithy-go to v1.26.0 (#9032)
c67843c
otelhttp: Remove custom wrapper after handling request (#6914)
c0a4135
docs(otelhttptrace): add performance guidance for WithoutSubSpans (#8785)
a51a867
otelconf: implement cardinality_limits support in PeriodicMetricReader
(#8885)
dead6e5
chore(deps): update module go.yaml.in/yaml/v2 to v2.4.4 (#8994)
979ce18
chore(deps): update module github.com/jgautheron/goconst to v1.10.2 (#9030)
- Additional commits viewable in compare
view
Updates `go.opentelemetry.io/contrib/propagators/autoprop` from 0.68.0
to 0.69.0
Release notes
Sourced from go.opentelemetry.io/contrib/propagators/autoprop's
releases.
v1.44.0/v2.5.1/v0.69.0/v0.37.1/v0.24.0/v0.19.0/v0.16.1/v0.16.0
Added
- Add
error.type attribute to
http.client.request.duration for transport failures in
otelhttp. (#8801)
- Add examples for prometheus compatibility document. (#8716)
- Add support for
cardinality_limits in
PeriodicMetricReader in otelconf. (#8885)
- Add
Resource method to SDK in
go.opentelemetry.io/contrib/otelconf/x to expose the
resolved SDK resource from declarative configuration. (#8913)
- Add
go.opentelemetry.io/contrib/detectors/hetzner, a
new resource detector for Hetzner Cloud servers, ported from
github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/hetzner.
Detects cloud.provider, cloud.platform,
cloud.region, cloud.availability_zone,
host.id, and host.name. (#8979)
Changed
- Set error field as
record.SetErr instead of a plain
attribute in
go.opentelemetry.io/contrib/bridges/otellogrus. (#8776)
- Set the "error" field (e.g. created via
zap.Error) as record.SetErr instead of a plain
attribute in go.opentelemetry.io/contrib/bridges/otelzap.
(#8719)
- Set fields implementing
error interface from
slog records as record.SetErr instead of plain
attributes in go.opentelemetry.io/contrib/bridges/otelslog.
(#8774)
- Set emitted errors in
go.opentelemetry.io/contrib/bridges/otellogr as record
errors (Record.SetErr) instead of
exception.message attributes. (#8775)
Fixed
- Fix header attributes lost when using sub-spans in
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace.
(#8797)
- Validate
encoding configuration for OTLP HTTP exporters
in go.opentelemetry.io/contrib/otelconf. (#8772)
- Remove the custom body wrapper from the request's body after the
request is processed to allow body type comparisons with the original
type in
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
and
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux.
(#6914)
- Unknown or empty HTTP methods now report "_OTHER" instead
of "GET" across all HTTP instrumentations to align with
OpenTelemetry semantic conventions. (#8868)
- The default span name formatter in
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
now conforms to the OpenTelemetry HTTP semantic conventions for server
span names. (#8871)
- The default span name is now
{method} {route} (e.g.
GET /foo/{id}) when a route pattern is available, or
{method} (e.g. GET) otherwise.
Removed
- Remove the deprecated
WithSpanOptions option in
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.
(#8991)
What's Changed
... (truncated)
Changelog
Sourced from go.opentelemetry.io/contrib/propagators/autoprop's
changelog.
[1.44.0/2.5.1/0.69.0/0.37.1/0.24.0/0.19.0/0.16.1/0.16.0] -
2026-05-28
Added
- Add
error.type attribute to
http.client.request.duration for transport failures in
otelhttp. (#8801)
- Add examples for prometheus compatibility document. (#8716)
- Add support for
cardinality_limits in
PeriodicMetricReader in otelconf. (#8885)
- Add
Resource method to SDK in
go.opentelemetry.io/contrib/otelconf/x to expose the
resolved SDK resource from declarative configuration. (#8913)
- Add
go.opentelemetry.io/contrib/detectors/hetzner, a
new resource detector for Hetzner Cloud servers, ported from
github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/hetzner.
Detects cloud.provider, cloud.platform,
cloud.region, cloud.availability_zone,
host.id, and host.name. (#8979)
Changed
- Set error field as
record.SetErr instead of a plain
attribute in
go.opentelemetry.io/contrib/bridges/otellogrus. (#8776)
- Set the "error" field (e.g. created via
zap.Error) as record.SetErr instead of a plain
attribute in go.opentelemetry.io/contrib/bridges/otelzap.
(#8719)
- Set fields implementing
error interface from
slog records as record.SetErr instead of plain
attributes in go.opentelemetry.io/contrib/bridges/otelslog.
(#8774)
- Set emitted errors in
go.opentelemetry.io/contrib/bridges/otellogr as record
errors (Record.SetErr) instead of
exception.message attributes. (#8775)
Fixed
- Fix header attributes lost when using sub-spans in
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace.
(#8797)
- Validate
encoding configuration for OTLP HTTP exporters
in go.opentelemetry.io/contrib/otelconf. (#8772)
- Remove the custom body wrapper from the request's body after the
request is processed to allow body type comparisons with the original
type in
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
and
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux.
(#6914)
- Unknown or empty HTTP methods now report "_OTHER" instead
of "GET" across all HTTP instrumentations to align with
OpenTelemetry semantic conventions. (#8868)
- The default span name formatter in
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
now conforms to the OpenTelemetry HTTP semantic conventions for server
span names. (#8871)
- The default span name is now
{method} {route} (e.g.
GET /foo/{id}) when a route pattern is available, or
{method} (e.g. GET) otherwise.
Removed
- Remove the deprecated
WithSpanOptions option in
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.
(#8991)
Commits
03b2bcd
Release v1.44.0/v2.5.1/v0.69.0/v0.37.1/v0.24.0/v0.19.0/v0.16.1/v0.16.0
(#9033)
80c46d4
chore(deps): update module github.com/alecthomas/chroma/v2 to v2.26.0
(#9034)
51f2921
fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.41.2
(#9026)
db82162
fix(deps): update aws-sdk-go-v2 monorepo (#9031)
5a3e533
fix(deps): update module github.com/aws/smithy-go to v1.26.0 (#9032)
c67843c
otelhttp: Remove custom wrapper after handling request (#6914)
c0a4135
docs(otelhttptrace): add performance guidance for WithoutSubSpans (#8785)
a51a867
otelconf: implement cardinality_limits support in PeriodicMetricReader
(#8885)
dead6e5
chore(deps): update module go.yaml.in/yaml/v2 to v2.4.4 (#8994)
979ce18
chore(deps): update module github.com/jgautheron/goconst to v1.10.2 (#9030)
- Additional commits viewable in compare
view
Updates `go.opentelemetry.io/otel` from 1.43.0 to 1.44.0
Changelog
Sourced from go.opentelemetry.io/otel's
changelog.
[1.44.0/0.66.0/0.20.0/0.0.17] 2026-05-27
Added
- Add
ByteSlice and ByteSliceValue functions
for new BYTESLICE attribute type in
go.opentelemetry.io/otel/attribute. (#7948)
- Apply attribute value limit to the
KindBytes attribute
type in go.opentelemetry.io/otel/sdk/log. (#7990)
- Apply attribute value limit to the
BYTESLICE attribute
type in go.opentelemetry.io/otel/sdk/trace. (#7990)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/trace. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlptrace. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlplog. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/zipkin. (#8153)
- Add
String method for Value type in
go.opentelemetry.io/otel/attribute. (#8142)
- Add
Slice and SliceValue functions for new
SLICE attribute type in
go.opentelemetry.io/otel/attribute. (#8166)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlptrace. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlplog. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/zipkin. (#8216)
- Apply
AttributeValueLengthLimit to
attribute.SLICE type attribute values in
go.opentelemetry.io/otel/sdk/trace, recursively truncating
contained string values. (#8217)
- Add
Error field on Record type in
go.opentelemetry.io/otel/log/logtest. (#8148)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp.
(#8157)
- Add
Settable to
go.opentelemetry.io/otel/metric/x to allow reusing
attribute options. (#8178)
- Add experimental support for splitting metric data across multiple
batches in
go.opentelemetry.io/otel/sdk/metric.
Set OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=<max_size> to
enable for all periodic readers.
See go.opentelemetry.io/otel/sdk/metric/internal/x for
feature documentation. (#8071)
- Add experimental self-observability metrics in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc.
Enable with OTEL_GO_X_SELF_OBSERVABILITY=true environment
variable.
See
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x
for feature documentation. (#8192)
- Add experimental self-observability metrics in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp.
Enable with OTEL_GO_X_SELF_OBSERVABILITY=true environment
variable.
See
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x
for feature documentation. (#8194)
- Add experimental self-observability metrics in
go.opentelemetry.io/otel/exporters/stdout/stdoutlog.
Enable with OTEL_GO_X_SELF_OBSERVABILITY=true environment
variable.
See go.opentelemetry.io/otel/stdout/stdoutlog/internal/x
for feature documentation. (#8263)
- Add
WithDefaultAttributes to
go.opentelemetry.io/otel/metric/x to support setting
default attributes on instruments. (#8135)
- Add
go.opentelemetry.io/otel/semconv/v1.41.0 package.
The package contains semantic conventions from the v1.41.0
version of the OpenTelemetry Semantic Conventions.
See the migration
documentation for information on how to upgrade from
go.opentelemetry.io/otel/semconv/v1.40.0. (#8324)
- Add Observable variants of instruments to
go.opentelemetry.io/otel/semconv/v1.41.0 package. (#8350)
- Generate explicit histogram bucket boundaries from weaver
configuration for HTTP and RPC duration instruments in
go.opentelemetry.io/otel/semconv/v1.41.0. (#8002)
Changed
- ⚠️ Breaking Change:
go.opentelemetry.io/otel/sdk/metric now applies a default
cardinality limit of 2000 to comply with the Metrics SDK specification
recommendation.
New attribute sets are dropped when the cardinality limit is reached.
The measurement of these sets are aggregated into a special attribute
set containing attribute.Bool("otel.metric.overflow",
true).
... (truncated)
Commits
b62d928
Release 1.44.0 (#8376)
94132a0
chore(deps): update golang.org/x/telemetry digest to 5997936 (#8379)
6fdcf82
feat: add self-observability metrics to otlpmetricgrpc metric exporters
(#8192)
761bbfc
fix(deps): update golang.org/x (#8377)
3a91dc6
fix(deps): update googleapis to 3dc84a4 (#8375)
f593185
exporters/otlp: default max request size to 64 MiB (#8365)
f02feac
Merge commit from fork
36c2f1b
semconvkit: add invariant test for histogram-exclusion rule (#8370)
d0b6cbd
sdk/metric: document unit-sensitivity of DefaultAggregationSelector (#8224)
9a68034
add self observability for stdout exporter (#8263)
- Additional commits viewable in compare
view
Updates `go.opentelemetry.io/otel/exporters/prometheus` from 0.65.0 to
0.66.0
Release notes
Sourced from go.opentelemetry.io/otel/exporters/prometheus's
releases.
v1.44.0/v0.66.0/v0.20.0/v0.0.17
Added
- Add
ByteSlice and ByteSliceValue functions
for new BYTESLICE attribute type in
go.opentelemetry.io/otel/attribute. (#7948)
- Apply attribute value limit to the
KindBytes attribute
type in go.opentelemetry.io/otel/sdk/log. (#7990)
- Apply attribute value limit to the
BYTESLICE attribute
type in go.opentelemetry.io/otel/sdk/trace. (#7990)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/trace. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlptrace. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlplog. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/zipkin. (#8153)
- Add
String method for Value type in
go.opentelemetry.io/otel/attribute. (#8142)
- Add
Slice and SliceValue functions for new
SLICE attribute type in
go.opentelemetry.io/otel/attribute. (#8166)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlptrace. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlplog. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/zipkin. (#8216)
- Apply
AttributeValueLengthLimit to
attribute.SLICE type attribute values in
go.opentelemetry.io/otel/sdk/trace, recursively truncating
contained string values. (#8217)
- Add
Error field on Record type in
go.opentelemetry.io/otel/log/logtest. (#8148)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp.
(#8157)
- Add
Settable to
go.opentelemetry.io/otel/metric/x to allow reusing
attribute options. (#8178)
- Add experimental support for splitting metric data across multiple
batches in
go.opentelemetry.io/otel/sdk/metric.
Set OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=<max_size> to
enable for all periodic readers.
See go.opentelemetry.io/otel/sdk/metric/internal/x for
feature documentation. (#8071)
- Add experimental self-observability metrics in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc.
Enable with OTEL_GO_X_SELF_OBSERVABILITY=true environment
variable.
See
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x
for feature documentation. (#8192)
- Add experimental self-observability metrics in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp.
Enable with OTEL_GO_X_SELF_OBSERVABILITY=true environment
variable.
See
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x
for feature documentation. (#8194)
- Add experimental self-observability metrics in
go.opentelemetry.io/otel/exporters/stdout/stdoutlog.
Enable with OTEL_GO_X_SELF_OBSERVABILITY=true environment
variable.
See go.opentelemetry.io/otel/stdout/stdoutlog/internal/x
for feature documentation. (#8263)
- Add
WithDefaultAttributes to
go.opentelemetry.io/otel/metric/x to support setting
default attributes on instruments. (#8135)
- Add
go.opentelemetry.io/otel/semconv/v1.41.0 package.
The package contains semantic conventions from the v1.41.0
version of the OpenTelemetry Semantic Conventions.
See the migration
documentation for information on how to upgrade from
go.opentelemetry.io/otel/semconv/v1.40.0. (#8324)
- Add Observable variants of instruments to
go.opentelemetry.io/otel/semconv/v1.41.0 package. (#8350)
- Generate explicit histogram bucket boundaries from weaver
configuration for HTTP and RPC duration instruments in
go.opentelemetry.io/otel/semconv/v1.41.0. (#8002)
Changed
- ⚠️ Breaking Change:
go.opentelemetry.io/otel/sdk/metric now applies a default
cardinality limit of 2000 to comply with the Metrics SDK specification
recommendation.
New attribute sets are dropped when the cardinality limit is reached.
The measurement of these sets are aggregated into a special attribute
set containing attribute.Bool("otel.metric.overflow",
true).
This can break users who relied on the previous unlimited default.
... (truncated)
Changelog
Sourced from go.opentelemetry.io/otel/exporters/prometheus's
changelog.
[1.44.0/0.66.0/0.20.0/0.0.17] 2026-05-27
Added
- Add
ByteSlice and ByteSliceValue functions
for new BYTESLICE attribute type in
go.opentelemetry.io/otel/attribute. (#7948)
- Apply attribute value limit to the
KindBytes attribute
type in go.opentelemetry.io/otel/sdk/log. (#7990)
- Apply attribute value limit to the
BYTESLICE attribute
type in go.opentelemetry.io/otel/sdk/trace. (#7990)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/trace. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlptrace. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlplog. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric. (#8153)
- Support
BYTESLICE attributes in
go.opentelemetry.io/otel/exporters/zipkin. (#8153)
- Add
String method for Value type in
go.opentelemetry.io/otel/attribute. (#8142)
- Add
Slice and SliceValue functions for new
SLICE attribute type in
go.opentelemetry.io/otel/attribute. (#8166)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlptrace. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlplog. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric. (#8216)
- Support
SLICE attributes in
go.opentelemetry.io/otel/exporters/zipkin. (#8216)
- Apply
AttributeValueLengthLimit to
attribute.SLICE type attribute values in
go.opentelemetry.io/otel/sdk/trace, recursively truncating
contained string values. (#8217)
- Add
Error field on Record type in
go.opentelemetry.io/otel/log/logtest. (#8148)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc.
(#8157)
- Add
WithMaxRequestSize option in
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp.
(#8157)
- Add
Settable to
go.opentelemetry.io/otel/metric/x to allow reusing
attribute options. (#8178)
- Add experimental support for splitting metric data across multiple
batches in
go.opentelemetry.io/otel/sdk/metric.
Set OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=<max_size> to
enable for all periodic readers.
See go.opentelemetry.io/otel/sdk/metric/internal/x for
feature documentation. (
Date: Thu, 11 Jun 2026 17:00:24 +0200
Subject: [PATCH 12/29] chore(deps): bump docker/setup-qemu-action from 4.0.0
to 4.1.0 in the github-actions group (#2221)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the github-actions group with 1 update:
[docker/setup-qemu-action](https://github.com/docker/setup-qemu-action).
Updates `docker/setup-qemu-action` from 4.0.0 to 4.1.0
Release notes
Sourced from docker/setup-qemu-action's
releases.
v4.1.0
Full Changelog: https://github.com/docker/setup-qemu-action/compare/v4.0.0...v4.1.0
Commits
0611638
Merge pull request #21
from crazy-max/uninst
ce59c81
chore: update generated content
2ddad44
uninstall current emulators
8c37cd6
Merge pull request #250
from docker/dependabot/npm_and_yarn/docker/actions-to...
d1a0ff3
chore: update generated content
0a8f3dc
build(deps): bump @docker/actions-toolkit from 0.79.0 to
0.91.0
9430f61
Merge pull request #291
from docker/dependabot/npm_and_yarn/tmp-0.2.6
978bd77
chore: update generated content
3479feb
build(deps): bump tmp from 0.2.5 to 0.2.6
b113c26
Merge pull request #255
from docker/dependabot/npm_and_yarn/fast-xml-parser-5...
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: yxia216
---
.github/workflows/docker_build_job.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/docker_build_job.yaml b/.github/workflows/docker_build_job.yaml
index f465c63454..a38b44c69c 100644
--- a/.github/workflows/docker_build_job.yaml
+++ b/.github/workflows/docker_build_job.yaml
@@ -36,7 +36,7 @@ jobs:
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Set up QEMU
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
+ uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- name: Set up Docker buildx
id: buildx
From 2857986a6e3d1c29e6abc6bb6ba646b09308c9a7 Mon Sep 17 00:00:00 2001
From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>
Date: Thu, 11 Jun 2026 18:58:15 +0100
Subject: [PATCH 13/29] docs(site): adds Stacklok to Adopters Page (#2224)
**Description**
Adds stacklok to adopters page
---------
Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>
Signed-off-by: yxia216
---
site/src/data/adopters/adopters.json | 6 ++++++
site/static/img/adopters/stacklok.png | Bin 0 -> 72035 bytes
2 files changed, 6 insertions(+)
create mode 100644 site/static/img/adopters/stacklok.png
diff --git a/site/src/data/adopters/adopters.json b/site/src/data/adopters/adopters.json
index a8955b6a35..26ddc7c6e1 100644
--- a/site/src/data/adopters/adopters.json
+++ b/site/src/data/adopters/adopters.json
@@ -45,6 +45,12 @@
"logoUrl": "/img/adopters/Simplifai.png",
"url": "https://simplifai.ai",
"description": "Agentic AI platform for insurance"
+ },
+ {
+ "name": "Stacklok",
+ "logoUrl": "/img/adopters/stacklok.png",
+ "url": "https://stacklok.com",
+ "description": "Agentic AI platform"
},
{
"name": "Your Logo Here",
diff --git a/site/static/img/adopters/stacklok.png b/site/static/img/adopters/stacklok.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d983fe66b8a3333de8d29ab205ab4128dc879ab
GIT binary patch
literal 72035
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