From a7f915547c8aedff71aca673a2c556d1eac77b7e Mon Sep 17 00:00:00 2001
From: hustxiayang
Date: Mon, 8 Jun 2026 16:02:05 -0400
Subject: [PATCH 01/59] chore: update codecov version to fix ci run (#2212)
**Description**
Update codecov/codecov-action from v6.0.0 to v6.0.2 to fix GPG signature
verification failure in CI:
https://github.com/envoyproxy/ai-gateway/actions/runs/27152233348/job/80174344507?pr=2114
Signed-off-by: yxia216
---
.github/workflows/build_and_test.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml
index 2b747c7b56..cc8953ae22 100644
--- a/.github/workflows/build_and_test.yaml
+++ b/.github/workflows/build_and_test.yaml
@@ -71,7 +71,7 @@ jobs:
- run: make test-coverage GO_TEST_ARGS='-race'
- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest'
- uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2
with:
fail_ci_if_error: true
files: ./out/go-test-coverage.out
From f081c16454387783a0289a7e8ad9993c6b06ddf1 Mon Sep 17 00:00:00 2001
From: hustxiayang
Date: Mon, 8 Jun 2026 16:18:52 -0400
Subject: [PATCH 02/59] feat: add openai to gcp embedding translations for
gemini embedding 2 (#2114)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**Description**
### 1. Extend OpenAI embedding API to support multimodal embeddings
Gemini Embedding 2 is a multimodal embedding model, but the OpenAI
embedding API only supports strings or token arrays as input. To support
multimodal inputs (images, audio, video, PDF), we extended the
`/v1/embeddings` endpoint with a `messages` field, following the
https://github.com/vllm-project/vllm/blob/2a16ece2d342c0c154a4949ad317b521f8c04ec4/vllm/entrypoints/pooling/embed/protocol.py#L113
### 2. Gemini Embedding 2 `embedContent` endpoint support
Gemini Embedding 2 models (`gemini-embedding-2-*`) use the
`embedContent` endpoint:
https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-multimodal-embeddings,
unlike older models (`text-embedding-004`, `gemini-embedding-001`) which
use the `predict` endpoint. The translator auto-detects the endpoint
based on model name.
**`EmbeddingInputItem` is only supported with the predict endpoint.**
The predict endpoint supports per-instance `task_type` and `title`:
```json
{
"instances": [
{"content": "query text", "task_type": "RETRIEVAL_QUERY"},
{"content": "doc text", "task_type": "RETRIEVAL_DOCUMENT", "title": "My Doc"}
]
}
```
The `embedContent` endpoint does not support per-item task types — it
accepts a single `content` with one global `taskType` via
`embedContentConfig`. Accepting `EmbeddingInputItem` for `embedContent`
would be misleading (users would expect per-item behavior), so we reject
it with a clear error. Users should set `task_type` at the request level
instead.
**`task_type` limitation for `gemini-embedding-2`.** Per the doc:
https://docs.cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-multimodal-embeddings,
`gemini-embedding-2` does not support the `task_type` field — the task
must be included as a prompt instruction instead (e.g. `"task: search
result | query: how to reset password"`). The API silently ignores
`taskType` in `embedContentConfig`. We still send it if the user
provides it, in case future `embedContent` models support it.
**`embedContentConfig` vs deprecated top-level fields.** The REST API
reference documents `taskType`, `outputDimensionality`, `autoTruncate`
as deprecated top-level fields. The genai SDK v1.54+:
https://github.com/googleapis/go-genai/blob/v1.54.0/models.go#L727 sends
them via `embedContentConfig` instead.
### Batch processing limitation
The Vertex AI REST API does not support batch embedding for
`embedContent` models. Experimentally verified:
1. **`batchEmbedContents`** — 404 on all Vertex AI endpoints. This
endpoint only exists on the Gemini API
(`generativelanguage.googleapis.com`), not Vertex AI
(`aiplatform.googleapis.com`).
2. **`embedContent` with `content` (singular)** — works, returns one
embedding per request.
3. **`embedContent` with `contents` (plural)** — rejected.
4. **`embedContent` with multiple `parts`** — returns one aggregated
embedding, not separate embeddings per part. `parts` are designed for
combining modalities, not for batching.
A possible workaround is fan-out (splitting batch inputs into multiple
`embedContent` calls), but this is out of scope for the gateway. We
expect Vertex AI to add `batchEmbedContents` support in the future.
There should be little impact on performance: the inference engine is a
batch processing engine regardless. Whether it receives a single request
or batch requests, the engine batches internally. The only difference is
whether the requests arrive in one HTTP call or multiple.
---------
Signed-off-by: yxia216
Signed-off-by: hustxiayang
Co-authored-by: Dan Sun
Co-authored-by: Ignasi Barrera
Co-authored-by: Aaron Choo
---
internal/apischema/gcp/gcp.go | 44 +
internal/apischema/openai/openai.go | 99 +-
internal/endpointspec/endpointspec_test.go | 21 +-
.../openinference/openai/embeddings_test.go | 54 +-
.../openinference/openai/request_attrs.go | 42 +-
internal/tracing/tracing_test.go | 9 +-
.../openai_awsbedrock_embeddings.go | 8 +-
.../openai_awsbedrock_embeddings_test.go | 78 +-
.../openai_gcpvertexai_embeddings.go | 287 ++++--
.../openai_gcpvertexai_embeddings_test.go | 855 ++++++++++++++++--
.../testopenai/embeddings_requests.go | 121 +--
tests/internal/testopenai/openai_test.go | 2 +-
12 files changed, 1391 insertions(+), 229 deletions(-)
diff --git a/internal/apischema/gcp/gcp.go b/internal/apischema/gcp/gcp.go
index e8482a73f3..285225bc04 100644
--- a/internal/apischema/gcp/gcp.go
+++ b/internal/apischema/gcp/gcp.go
@@ -107,3 +107,47 @@ type Prediction struct {
type PredictResponse struct {
Predictions []*Prediction `json:"predictions"`
}
+
+// EmbedContentRequest is the request body for the embedContent endpoint used by newer embedding models
+// (e.g. gemini-embedding-2-*).
+// All input texts are packed as parts in a single Content object and we drop deprecated top-level fields
+// (taskType, outputDimensionality, etc.)
+//
+// See https://docs.cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.publishers.models/embedContent
+type EmbedContentRequest struct {
+ Content genai.Content `json:"content"`
+ Config *EmbedContentConfig `json:"embedContentConfig,omitempty"`
+}
+
+// EmbedContentConfig contains optional parameters for the embedContent method.
+// https://github.com/googleapis/go-genai/blob/v1.54.0/models.go#L727
+type EmbedContentConfig struct {
+ TaskType openai.EmbeddingTaskType `json:"taskType,omitempty"`
+ Title string `json:"title,omitempty"`
+ OutputDimensionality int `json:"outputDimensionality,omitempty"`
+ AutoTruncate *bool `json:"autoTruncate,omitempty"`
+}
+
+// EmbedContentResponse is the response from the embedContent endpoint.
+// The REST API returns a single embedding (not an array), plus usage metadata.
+// https://docs.cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.publishers.models/embedContent
+type EmbedContentResponse struct {
+ // The embedding generated from the input content (singular object, not an array).
+ Embedding *EmbedContentEmbedding `json:"embedding,omitempty"`
+ // Usage metadata about the response.
+ UsageMetadata *EmbedContentUsageMetadata `json:"usageMetadata,omitempty"`
+ // Whether the input content was truncated before generating the embedding.
+ Truncated bool `json:"truncated,omitempty"`
+}
+
+// EmbedContentEmbedding represents the embedding values from the embedContent response.
+type EmbedContentEmbedding struct {
+ // The embedding values.
+ Values []float32 `json:"values,omitempty"`
+}
+
+// EmbedContentUsageMetadata contains token usage from the embedContent response.
+type EmbedContentUsageMetadata struct {
+ PromptTokenCount int `json:"promptTokenCount,omitempty"`
+ TotalTokenCount int `json:"totalTokenCount,omitempty"`
+}
diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go
index cba870878c..865205d2ba 100644
--- a/internal/apischema/openai/openai.go
+++ b/internal/apischema/openai/openai.go
@@ -1684,15 +1684,8 @@ type Model struct {
OwnedBy string `json:"owned_by"`
}
-// EmbeddingRequest represents a request structure for embeddings API.
-type EmbeddingRequest struct {
- // Input: Input text to embed, encoded as a string or array of tokens.
- // To embed multiple inputs in a single request, pass an array of strings or array of token arrays.
- // The input must not exceed the max input tokens for the model (8192 tokens for text-embedding-ada-002),
- // cannot be an empty string, and any array must be 2048 dimensions or less.
- // Docs: https://platform.openai.com/docs/api-reference/embeddings/create#embeddings-create-input
- Input EmbeddingRequestInput `json:"input"`
-
+// EmbeddingBaseRequest holds fields shared by both embedding request variants.
+type EmbeddingBaseRequest struct {
// Model: ID of the model to use.
// Docs: https://platform.openai.com/docs/api-reference/embeddings/create#embeddings-create-model
Model string `json:"model"`
@@ -1714,6 +1707,76 @@ type EmbeddingRequest struct {
*GCPVertexAIEmbeddingVendorFields `json:",inline,omitempty"`
}
+// EmbeddingCompletionRequest is the text-only embedding request (classic OpenAI style).
+// https://developers.openai.com/api/reference/resources/embeddings/methods/create
+type EmbeddingCompletionRequest struct {
+ EmbeddingBaseRequest
+ // Input: Input text to embed, encoded as a string or array of tokens.
+ // To embed multiple inputs in a single request, pass an array of strings or array of token arrays.
+ // The input must not exceed the max input tokens for the model (8192 tokens for text-embedding-ada-002),
+ // cannot be an empty string, and any array must be 2048 dimensions or less.
+ Input EmbeddingRequestInput `json:"input"`
+}
+
+// EmbeddingChatRequest is the chat-style embedding request (supports multimodal).
+// Following vLLM convention: https://github.com/vllm-project/vllm/blob/2a16ece2d342c0c154a4949ad317b521f8c04ec4/vllm/entrypoints/pooling/embed/protocol.py#L83
+type EmbeddingChatRequest struct {
+ EmbeddingBaseRequest
+ // Messages: Chat messages for multimodal embedding.
+ // Uses the same chat message format as /v1/chat/completions.
+ // Only user messages are processed; system/assistant/tool messages are ignored.
+ Messages []ChatCompletionMessageParamUnion `json:"messages"`
+}
+
+// EmbeddingRequest is a discriminated union: exactly one of OfCompletion or OfChat is set.
+// Discrimination is by presence of "input" (→ completion) vs "messages" (→ chat) in JSON.
+type EmbeddingRequest struct {
+ EmbeddingBaseRequest // promoted shared fields
+ OfCompletion *EmbeddingCompletionRequest // set when JSON has "input"
+ OfChat *EmbeddingChatRequest // set when JSON has "messages"
+}
+
+// UnmarshalJSON discriminates between completion and chat embedding requests.
+func (r *EmbeddingRequest) UnmarshalJSON(data []byte) error {
+ hasInput := gjson.GetBytes(data, "input").Exists()
+ hasMessages := gjson.GetBytes(data, "messages").Exists()
+
+ if hasInput && hasMessages {
+ return fmt.Errorf("embedding request must have either 'input' or 'messages', not both")
+ }
+
+ if hasMessages {
+ var chat EmbeddingChatRequest
+ if err := json.Unmarshal(data, &chat); err != nil {
+ return err
+ }
+ r.EmbeddingBaseRequest = chat.EmbeddingBaseRequest
+ r.OfChat = &chat
+ return nil
+ }
+
+ // Default to completion (input-based) — this handles both explicit "input" and legacy cases.
+ var comp EmbeddingCompletionRequest
+ if err := json.Unmarshal(data, &comp); err != nil {
+ return err
+ }
+ r.EmbeddingBaseRequest = comp.EmbeddingBaseRequest
+ r.OfCompletion = &comp
+ return nil
+}
+
+// MarshalJSON delegates to the active variant.
+func (r EmbeddingRequest) MarshalJSON() ([]byte, error) {
+ if r.OfChat != nil {
+ return json.Marshal(r.OfChat)
+ }
+ if r.OfCompletion != nil {
+ return json.Marshal(r.OfCompletion)
+ }
+ // Fallback: marshal just the base fields.
+ return json.Marshal(r.EmbeddingBaseRequest)
+}
+
type EmbeddingTaskType string
const (
@@ -1727,16 +1790,20 @@ const (
EmbeddingTaskTypeCodeRetrievalQuery EmbeddingTaskType = "CODE_RETRIEVAL_QUERY"
)
-// GCPVertexAIEmbeddingVendorFields contains GCP Vertex AI (Gemini) vendor-specific fields for embeddings.
+// GCPVertexAIEmbeddingVendorFields contains GCP Vertex AI vendor-specific fields for embeddings.
+// The translator maps these to the appropriate wire format per endpoint:
+// - predict: parameters.auto_truncate, instances[].task_type, instances[].title
+// - embedContent: embedContentConfig.autoTruncate, embedContentConfig.taskType, embedContentConfig.title
type GCPVertexAIEmbeddingVendorFields struct {
- // When set to true, input text will be truncated. When set to false, an error is returned if the input text is longer than the maximum length supported by the model. Defaults to true.
- // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api#parameter-list
+ // Auto-truncate input text if it exceeds the model's max length. Defaults to true.
+ AutoTruncate *bool `json:"auto_truncate,omitempty"`
- AutoTruncate bool `json:"auto_truncate,omitempty"`
-
- // This is global task_type set, which is convenient for users. If left blank, the default used is RETRIEVAL_QUERY.
- // For more information about task types, see https://docs.cloud.google.com/vertex-ai/generative-ai/docs/embeddings/task-types
+ // Global task type for the request. Defaults to RETRIEVAL_QUERY if unset.
+ // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/embeddings/task-types
TaskType EmbeddingTaskType `json:"task_type,omitempty"`
+
+ // Title for the embedding content. Helps the model produce better embeddings.
+ Title string `json:"title,omitempty"`
}
// EmbeddingResponse represents a response from /v1/embeddings.
diff --git a/internal/endpointspec/endpointspec_test.go b/internal/endpointspec/endpointspec_test.go
index 9ab213e67a..9c71908dc3 100644
--- a/internal/endpointspec/endpointspec_test.go
+++ b/internal/endpointspec/endpointspec_test.go
@@ -145,8 +145,14 @@ func TestEmbeddingsEndpointSpec_ParseBody(t *testing.T) {
require.ErrorContains(t, err, "malformed request")
})
- t.Run("success", func(t *testing.T) {
- req := openai.EmbeddingRequest{Model: "text-embedding-3-large", Input: openai.EmbeddingRequestInput{Value: "input"}}
+ t.Run("success with input", func(t *testing.T) {
+ req := openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-large"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-large"},
+ Input: openai.EmbeddingRequestInput{Value: "input"},
+ },
+ }
body, err := json.Marshal(req)
require.NoError(t, err)
@@ -157,6 +163,17 @@ func TestEmbeddingsEndpointSpec_ParseBody(t *testing.T) {
require.NotNil(t, parsed)
require.Nil(t, mutated)
})
+
+ t.Run("success with messages", func(t *testing.T) {
+ body := []byte(`{"model":"gemini-embedding-2","messages":[{"role":"user","content":"embed this"}]}`)
+
+ model, parsed, stream, mutated, err := spec.ParseBody(body, false)
+ require.NoError(t, err)
+ require.Equal(t, "gemini-embedding-2", model)
+ require.False(t, stream)
+ require.NotNil(t, parsed)
+ require.Nil(t, mutated)
+ })
}
func TestEmbeddingsEndpointSpec_GetTranslator(t *testing.T) {
diff --git a/internal/tracing/openinference/openai/embeddings_test.go b/internal/tracing/openinference/openai/embeddings_test.go
index 3726391f4e..cd2a8a58ae 100644
--- a/internal/tracing/openinference/openai/embeddings_test.go
+++ b/internal/tracing/openinference/openai/embeddings_test.go
@@ -24,23 +24,46 @@ import (
// Test data.
var (
basicEmbeddingReq = &openai.EmbeddingRequest{
- Model: "text-embedding-3-small",
- Input: openai.EmbeddingRequestInput{Value: "How do I reset my password?"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-small"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-small"},
+ Input: openai.EmbeddingRequestInput{Value: "How do I reset my password?"},
+ },
}
basicEmbeddingReqBody = []byte(`{"model":"text-embedding-3-small","input":"How do I reset my password?"}`)
multiInputEmbeddingReq = &openai.EmbeddingRequest{
- Model: "text-embedding-3-small",
- Input: openai.EmbeddingRequestInput{Value: []string{"How", "do", "I", "reset", "my", "password?"}},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-small"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-small"},
+ Input: openai.EmbeddingRequestInput{Value: []string{"How", "do", "I", "reset", "my", "password?"}},
+ },
}
multiInputEmbeddingReqBody = []byte(`{"model":"text-embedding-3-small","input":["How","do","I","reset","my","password?"]}`)
tokenInputEmbeddingReq = &openai.EmbeddingRequest{
- Model: "text-embedding-3-small",
- Input: openai.EmbeddingRequestInput{Value: []int{14438, 656, 358, 7738, 856, 3636, 30}},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-small"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-small"},
+ Input: openai.EmbeddingRequestInput{Value: []int64{4438, 656, 358, 7738, 856, 3636, 30}},
+ },
}
tokenInputEmbeddingReqBody = []byte(`{"model":"text-embedding-3-small","input":[4438,656,358,7738,856,3636,30]}`)
+ chatEmbeddingReq = &openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2"},
+ OfChat: &openai.EmbeddingChatRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2"},
+ Messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "embed this text"},
+ }},
+ },
+ },
+ }
+ chatEmbeddingReqBody = []byte(`{"model":"gemini-embedding-2","messages":[{"role":"user","content":"embed this text"}]}`)
+
basicEmbeddingResp = &openai.EmbeddingResponse{
Model: "text-embedding-3-small",
Usage: openai.EmbeddingUsage{
@@ -95,6 +118,12 @@ func TestEmbeddingsRecorder_StartParams(t *testing.T) {
reqBody: multiInputEmbeddingReqBody,
expectedSpanName: "CreateEmbeddings",
},
+ {
+ name: "chat embedding request",
+ req: chatEmbeddingReq,
+ reqBody: chatEmbeddingReqBody,
+ expectedSpanName: "CreateEmbeddings",
+ },
}
for _, tt := range tests {
@@ -172,6 +201,19 @@ func TestEmbeddingsRecorder_RecordRequest(t *testing.T) {
attribute.String(openinference.EmbeddingInvocationParameters, `{"model":"text-embedding-3-small"}`),
},
},
+ {
+ name: "chat embedding request",
+ req: chatEmbeddingReq,
+ reqBody: chatEmbeddingReqBody,
+ config: &openinference.TraceConfig{},
+ expectedAttrs: []attribute.KeyValue{
+ attribute.String(openinference.SpanKind, openinference.SpanKindEmbedding),
+ attribute.String(openinference.InputValue, string(chatEmbeddingReqBody)),
+ attribute.String(openinference.InputMimeType, openinference.MimeTypeJSON),
+ attribute.String(openinference.EmbeddingInvocationParameters, `{"model":"gemini-embedding-2"}`),
+ attribute.String(openinference.EmbeddingTextAttribute(0), "embed this text"),
+ },
+ },
{
name: "hidden invocation parameters",
req: basicEmbeddingReq,
diff --git a/internal/tracing/openinference/openai/request_attrs.go b/internal/tracing/openinference/openai/request_attrs.go
index e8063f9fa5..5980f28a4a 100644
--- a/internal/tracing/openinference/openai/request_attrs.go
+++ b/internal/tracing/openinference/openai/request_attrs.go
@@ -256,16 +256,40 @@ func buildEmbeddingsRequestAttributes(embRequest *openai.EmbeddingRequest, body
// 4. Azure deployments don't affect this (they only host OpenAI models with cl100k_base)
// Following OpenInference spec guidance to only record human-readable text.
if !config.HideInputs && !config.HideEmbeddingsText {
- switch input := embRequest.Input.Value.(type) {
- case string:
- attrs = append(attrs, attribute.String(openinference.EmbeddingTextAttribute(0), input))
- case []string:
- for i, text := range input {
- attrs = append(attrs, attribute.String(openinference.EmbeddingTextAttribute(i), text))
+ switch {
+ case embRequest.OfCompletion != nil:
+ switch input := embRequest.OfCompletion.Input.Value.(type) {
+ case string:
+ attrs = append(attrs, attribute.String(openinference.EmbeddingTextAttribute(0), input))
+ case []string:
+ for i, text := range input {
+ attrs = append(attrs, attribute.String(openinference.EmbeddingTextAttribute(i), text))
+ }
+ // Token inputs are not recorded to reduce span size.
+ case []int64:
+ case [][]int64:
+ }
+ case embRequest.OfChat != nil:
+ idx := 0
+ for _, msg := range embRequest.OfChat.Messages {
+ if msg.OfUser == nil {
+ continue
+ }
+ switch content := msg.OfUser.Content.Value.(type) {
+ case string:
+ if content != "" {
+ attrs = append(attrs, attribute.String(openinference.EmbeddingTextAttribute(idx), content))
+ idx++
+ }
+ case []openai.ChatCompletionContentPartUserUnionParam:
+ for _, part := range content {
+ if part.OfText != nil && part.OfText.Text != "" {
+ attrs = append(attrs, attribute.String(openinference.EmbeddingTextAttribute(idx), part.OfText.Text))
+ idx++
+ }
+ }
+ }
}
- // Token inputs are not recorded to reduce span size.
- case []int64:
- case [][]int64:
}
}
diff --git a/internal/tracing/tracing_test.go b/internal/tracing/tracing_test.go
index 22cba11473..b7caf5dd1e 100644
--- a/internal/tracing/tracing_test.go
+++ b/internal/tracing/tracing_test.go
@@ -775,9 +775,12 @@ func TestNewTracingFromEnv_Embeddings_Redaction(t *testing.T) {
// Create a test request with sensitive data.
req := &openai.EmbeddingRequest{
- Model: "text-embedding-3-small",
- Input: openai.EmbeddingRequestInput{
- Value: "Sensitive embedding text",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-small"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-3-small"},
+ Input: openai.EmbeddingRequestInput{
+ Value: "Sensitive embedding text",
+ },
},
}
reqBody := []byte(`{"input":"Sensitive embedding text","model":"text-embedding-3-small"}`)
diff --git a/internal/translator/openai_awsbedrock_embeddings.go b/internal/translator/openai_awsbedrock_embeddings.go
index 1592899337..7e81d480a3 100644
--- a/internal/translator/openai_awsbedrock_embeddings.go
+++ b/internal/translator/openai_awsbedrock_embeddings.go
@@ -41,8 +41,12 @@ func (o *openAIToAWSBedrockTranslatorV1Embedding) RequestBody(_ []byte, req *ope
}
o.requestModel = model
+ if req.OfCompletion == nil {
+ return nil, nil, fmt.Errorf("%w: AWS Bedrock Titan requires an input-based embedding request (messages not supported)", internalapi.ErrInvalidRequestBody)
+ }
+
var inputText string
- switch v := req.Input.Value.(type) {
+ switch v := req.OfCompletion.Input.Value.(type) {
case string:
inputText = v
case []string:
@@ -52,7 +56,7 @@ func (o *openAIToAWSBedrockTranslatorV1Embedding) RequestBody(_ []byte, req *ope
}
inputText = v[0]
default:
- return nil, nil, fmt.Errorf("%w: unsupported input type %T", internalapi.ErrInvalidRequestBody, req.Input.Value)
+ return nil, nil, fmt.Errorf("%w: unsupported input type %T", internalapi.ErrInvalidRequestBody, req.OfCompletion.Input.Value)
}
bedrockReq := awsbedrock.TitanEmbeddingRequest{
diff --git a/internal/translator/openai_awsbedrock_embeddings_test.go b/internal/translator/openai_awsbedrock_embeddings_test.go
index e48afc32df..714a32db02 100644
--- a/internal/translator/openai_awsbedrock_embeddings_test.go
+++ b/internal/translator/openai_awsbedrock_embeddings_test.go
@@ -35,8 +35,11 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody(t *testing.T) {
// v1 only supports inputText; dimensions/normalize/embeddingTypes must NOT appear in the body.
name: "v1 model - only inputText sent",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v1:2",
- Input: openai.EmbeddingRequestInput{Value: "hello world"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v1:2"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v1:2"},
+ Input: openai.EmbeddingRequestInput{Value: "hello world"},
+ },
},
wantPath: "/model/amazon.titan-embed-text-v1:2/invoke",
wantBodyContains: []string{`"inputText":"hello world"`},
@@ -44,8 +47,11 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody(t *testing.T) {
{
name: "string input",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v2:0",
- Input: openai.EmbeddingRequestInput{Value: "hello world"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ Input: openai.EmbeddingRequestInput{Value: "hello world"},
+ },
},
wantPath: "/model/amazon.titan-embed-text-v2:0/invoke",
wantBodyContains: []string{`"inputText":"hello world"`},
@@ -53,8 +59,11 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody(t *testing.T) {
{
name: "single-element string slice input",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v2:0",
- Input: openai.EmbeddingRequestInput{Value: []string{"hello world"}},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ Input: openai.EmbeddingRequestInput{Value: []string{"hello world"}},
+ },
},
wantPath: "/model/amazon.titan-embed-text-v2:0/invoke",
wantBodyContains: []string{`"inputText":"hello world"`},
@@ -62,24 +71,33 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody(t *testing.T) {
{
name: "batch input rejected",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v2:0",
- Input: openai.EmbeddingRequestInput{Value: []string{"first", "second"}},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ Input: openai.EmbeddingRequestInput{Value: []string{"first", "second"}},
+ },
},
wantErr: true,
},
{
name: "empty string slice rejected",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v2:0",
- Input: openai.EmbeddingRequestInput{Value: []string{}},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ Input: openai.EmbeddingRequestInput{Value: []string{}},
+ },
},
wantErr: true,
},
{
name: "unsupported input type rejected",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v2:0",
- Input: openai.EmbeddingRequestInput{Value: 42},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ Input: openai.EmbeddingRequestInput{Value: 42},
+ },
},
wantErr: true,
},
@@ -87,8 +105,11 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody(t *testing.T) {
// v1 model - when no dimensions are set, the dimensions field must be omitted entirely.
name: "v1 model - dimensions field omitted from body",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v1:2",
- Input: openai.EmbeddingRequestInput{Value: "test"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v1:2"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v1:2"},
+ Input: openai.EmbeddingRequestInput{Value: "test"},
+ },
},
wantPath: "/model/amazon.titan-embed-text-v1:2/invoke",
wantBodyContains: []string{`"inputText":"test"`},
@@ -98,9 +119,11 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody(t *testing.T) {
// v2 model - dimensions is forwarded.
name: "v2 model - dimensions forwarded to Titan",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v2:0",
- Input: openai.EmbeddingRequestInput{Value: "test"},
- Dimensions: &[]int{256}[0],
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0", Dimensions: &[]int{256}[0]},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0", Dimensions: &[]int{256}[0]},
+ Input: openai.EmbeddingRequestInput{Value: "test"},
+ },
},
wantPath: "/model/amazon.titan-embed-text-v2:0/invoke",
wantBodyContains: []string{`"inputText":"test"`, `"dimensions":256`},
@@ -109,8 +132,11 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody(t *testing.T) {
name: "model name override applied",
modelNameOverride: "amazon.titan-embed-text-v1:2",
input: openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v2:0",
- Input: openai.EmbeddingRequestInput{Value: "test"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ Input: openai.EmbeddingRequestInput{Value: "test"},
+ },
},
wantPath: "/model/amazon.titan-embed-text-v1:2/invoke",
wantBodyContains: []string{`"inputText":"test"`},
@@ -118,8 +144,11 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody(t *testing.T) {
{
name: "model with spaces is path-escaped",
input: openai.EmbeddingRequest{
- Model: "my model v1",
- Input: openai.EmbeddingRequestInput{Value: "escape test"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "my model v1"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "my model v1"},
+ Input: openai.EmbeddingRequestInput{Value: "escape test"},
+ },
},
wantPath: "/model/my%20model%20v1/invoke",
wantBodyContains: []string{`"inputText":"escape test"`},
@@ -164,8 +193,11 @@ func TestEmbeddingOpenAIToAWSBedrockTranslator_RequestBody_MarshalError(t *testi
translator := NewEmbeddingOpenAIToAWSBedrockTranslator("")
req := openai.EmbeddingRequest{
- Model: "amazon.titan-embed-text-v2:0",
- Input: openai.EmbeddingRequestInput{Value: "test"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "amazon.titan-embed-text-v2:0"},
+ Input: openai.EmbeddingRequestInput{Value: "test"},
+ },
}
_, _, err := translator.RequestBody(nil, &req, false)
require.ErrorContains(t, err, "failed to marshal body")
diff --git a/internal/translator/openai_gcpvertexai_embeddings.go b/internal/translator/openai_gcpvertexai_embeddings.go
index b73f5b472b..4badee4049 100644
--- a/internal/translator/openai_gcpvertexai_embeddings.go
+++ b/internal/translator/openai_gcpvertexai_embeddings.go
@@ -9,6 +9,9 @@ import (
"fmt"
"io"
"strconv"
+ "strings"
+
+ "google.golang.org/genai"
"github.com/envoyproxy/ai-gateway/internal/apischema/gcp"
"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
@@ -19,7 +22,8 @@ import (
)
const (
- gcpMethodPredict = "predict"
+ gcpMethodPredict = "predict"
+ gcpMethodEmbedContent = "embedContent"
)
// NewEmbeddingOpenAIToGCPVertexAITranslator implements [Factory] for OpenAI to GCP VertexAI translation
@@ -32,11 +36,15 @@ func NewEmbeddingOpenAIToGCPVertexAITranslator(requestModel internalapi.RequestM
}
// openAIToGCPVertexAITranslatorV1Embedding translates OpenAI Embeddings API to GCP Vertex AI Gemini Embeddings API.
-// Note: This uses the Gemini native API (predict endpoint), not Vertex AI's OpenAI-compatible API:
+// It auto-detects the endpoint based on model name:
+// - Older models (text-embedding-004, gemini-embedding-001): predict endpoint
+// - Newer models (gemini-embedding-2-*): embedContent endpoint
+//
// https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api
type openAIToGCPVertexAITranslatorV1Embedding struct {
requestModel internalapi.RequestModel
modelNameOverride internalapi.ModelNameOverride
+ useEmbedContent bool
}
// createInstancesFromEmbeddingInputItem converts an EmbeddingInputItem to GCP Instance(s).
@@ -99,15 +107,18 @@ func setInstances(input openai.EmbeddingRequestInput, instances []*gcp.Instance)
}
return instances, nil
default:
- return nil, fmt.Errorf("unsupported input type for embedding: %T (supported: string, []string, EmbeddingInputItem, []EmbeddingInputItem)", v)
+ return nil, fmt.Errorf("%w: unsupported input type for embedding: %T (supported: string, []string, EmbeddingInputItem, []EmbeddingInputItem)", internalapi.ErrInvalidRequestBody, v)
}
}
-// openAIEmbeddingToGeminiMessage converts an OpenAI EmbeddingRequest to a GCP PredictRequest.
+// openAIEmbeddingToGeminiMessage converts an OpenAI EmbeddingCompletionRequest to a GCP PredictRequest.
func openAIEmbeddingToGeminiMessage(openAIReq *openai.EmbeddingRequest) (*gcp.PredictRequest, error) {
+ if openAIReq.OfCompletion == nil {
+ return nil, fmt.Errorf("%w: model %s does not support multimodal embedding via messages", internalapi.ErrInvalidRequestBody, openAIReq.Model)
+ }
// Convert OpenAI EmbeddingRequest's input to Gemini instances.
var instances []*gcp.Instance
- instances, err := setInstances(openAIReq.Input, instances)
+ instances, err := setInstances(openAIReq.OfCompletion.Input, instances)
if err != nil {
return nil, err
}
@@ -123,8 +134,8 @@ func openAIEmbeddingToGeminiMessage(openAIReq *openai.EmbeddingRequest) (*gcp.Pr
// Apply vendor-specific fields if present.
if openAIReq.GCPVertexAIEmbeddingVendorFields != nil {
// Set auto truncate if specified.
- if openAIReq.AutoTruncate {
- parameters.AutoTruncate = openAIReq.AutoTruncate
+ if openAIReq.AutoTruncate != nil {
+ parameters.AutoTruncate = *openAIReq.AutoTruncate
}
// Apply global task type to all instances if specified.
@@ -134,6 +145,12 @@ func openAIEmbeddingToGeminiMessage(openAIReq *openai.EmbeddingRequest) (*gcp.Pr
instance.TaskType = openAIReq.TaskType
}
}
+ // Apply global title to all instances if specified.
+ if openAIReq.Title != "" {
+ for _, instance := range instances {
+ instance.Title = openAIReq.Title
+ }
+ }
}
// Build the request using gcp.PredictRequest.
@@ -145,6 +162,124 @@ func openAIEmbeddingToGeminiMessage(openAIReq *openai.EmbeddingRequest) (*gcp.Pr
return gcr, nil
}
+// isEmbedContentModel returns true if the model should use the embedContent endpoint
+// instead of the predict endpoint.
+// Reference: https://github.com/googleapis/go-genai/blob/v1.54.0/transformer.go#L565
+// Check and update this function when new Gemini embedding model versions are released.
+func isEmbedContentModel(model string) bool {
+ return strings.Contains(model, "gemini") && model != "gemini-embedding-001"
+}
+
+// collectInputTexts extracts all input text strings from an OpenAI EmbeddingRequestInput.
+// Only plain string inputs are supported for the embedContent endpoint.
+// EmbeddingInputItem (with per-item task_type/title) is rejected — use global vendor fields instead.
+func collectInputTexts(input openai.EmbeddingRequestInput) ([]string, error) {
+ switch v := input.Value.(type) {
+ case string:
+ return []string{v}, nil
+ case []string:
+ return v, nil
+ case openai.EmbeddingInputItem:
+ return nil, fmt.Errorf("%w: object input with per-item task_type/title is not supported for this model; use plain string input and set task_type at the request level", internalapi.ErrInvalidRequestBody)
+ case []openai.EmbeddingInputItem:
+ return nil, fmt.Errorf("%w: object input with per-item task_type/title is not supported for this model; use plain string input and set task_type at the request level", internalapi.ErrInvalidRequestBody)
+ default:
+ return nil, fmt.Errorf("%w: unsupported input type for embedding: %T", internalapi.ErrInvalidRequestBody, v)
+ }
+}
+
+// collectPartsFromMessages extracts genai.Part objects from chat messages for embedding.
+// Only user messages are processed; system/assistant/tool/developer messages are skipped.
+// Supported content types: text and images (URL or data URI).
+// Audio/video/PDF would require extending the OpenAI chat message schema.
+func collectPartsFromMessages(messages []openai.ChatCompletionMessageParamUnion, requestModel internalapi.RequestModel) ([]*genai.Part, error) {
+ var parts []*genai.Part
+ for _, msg := range messages {
+ if msg.OfUser == nil {
+ continue
+ }
+ msgParts, err := userMsgToGeminiParts(*msg.OfUser, requestModel)
+ if err != nil {
+ return nil, err
+ }
+ parts = append(parts, msgParts...)
+ }
+ if len(parts) == 0 {
+ return nil, fmt.Errorf("%w: no user messages found in embedding request", internalapi.ErrInvalidRequestBody)
+ }
+ return parts, nil
+}
+
+// openAIEmbeddingToEmbedContentRequest converts an OpenAI EmbeddingRequest to a GCP EmbedContentRequest.
+// Each input text becomes a separate Part in a single Content object.
+// When messages are provided, multimodal content parts are extracted from user messages.
+func openAIEmbeddingToEmbedContentRequest(openAIReq *openai.EmbeddingRequest, requestModel internalapi.RequestModel) (*gcp.EmbedContentRequest, error) {
+ var parts []*genai.Part
+
+ switch {
+ case openAIReq.OfChat != nil:
+ // Multimodal path: convert chat messages to genai parts.
+ var err error
+ parts, err = collectPartsFromMessages(openAIReq.OfChat.Messages, requestModel)
+ if err != nil {
+ return nil, err
+ }
+ case openAIReq.OfCompletion != nil:
+ // Text-only path: existing collectInputTexts logic.
+ texts, err := collectInputTexts(openAIReq.OfCompletion.Input)
+ if err != nil {
+ return nil, err
+ }
+ if len(texts) > 1 {
+ return nil, fmt.Errorf("%w: model %s does not support batch embeddings; send one input per request", internalapi.ErrInvalidRequestBody, requestModel)
+ }
+ parts = make([]*genai.Part, len(texts))
+ for i, text := range texts {
+ parts[i] = genai.NewPartFromText(text)
+ }
+ default:
+ return nil, fmt.Errorf("%w: embedding request must have either input or messages", internalapi.ErrInvalidRequestBody)
+ }
+
+ req := &gcp.EmbedContentRequest{
+ Content: genai.Content{Parts: parts},
+ }
+
+ // Config fields are sent via "embedContentConfig" (not deprecated top-level fields).
+ // https://github.com/googleapis/go-genai/blob/v1.54.0/models.go#L727
+ var config gcp.EmbedContentConfig
+ hasConfig := false
+
+ if openAIReq.Dimensions != nil && *openAIReq.Dimensions > 0 {
+ config.OutputDimensionality = *openAIReq.Dimensions
+ hasConfig = true
+ }
+
+ if openAIReq.GCPVertexAIEmbeddingVendorFields != nil {
+ if openAIReq.AutoTruncate != nil {
+ config.AutoTruncate = openAIReq.AutoTruncate
+ hasConfig = true
+ }
+ // NOTE: gemini-embedding-2 silently ignores taskType — task must be included as a prompt
+ // instruction instead. We still send it in case future embedContent models support it.
+ // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-multimodal-embeddings
+ if openAIReq.TaskType != "" {
+ config.TaskType = openAIReq.TaskType
+ hasConfig = true
+ }
+ if openAIReq.Title != "" {
+ config.Title = openAIReq.Title
+ hasConfig = true
+ }
+ }
+
+ if hasConfig {
+ req.Config = &config
+ }
+
+ return req, nil
+}
+
// RequestBody implements [OpenAIEmbeddingTranslator.RequestBody] for GCP Gemini.
// This method translates an OpenAI Embedding request to a GCP Gemini Embeddings API request.
func (o *openAIToGCPVertexAITranslatorV1Embedding) RequestBody(_ []byte, req *openai.EmbeddingRequest, _ bool) (
@@ -156,18 +291,35 @@ func (o *openAIToGCPVertexAITranslatorV1Embedding) RequestBody(_ []byte, req *op
o.requestModel = o.modelNameOverride
}
- // Use the predict endpoint for text embeddings in Vertex AI.
- // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api#curl
- path := buildGCPModelPathSuffix(gcpModelPublisherGoogle, o.requestModel, gcpMethodPredict)
+ var path string
- var gcpReq *gcp.PredictRequest
+ if isEmbedContentModel(o.requestModel) {
+ o.useEmbedContent = true
+ path = buildGCPModelPathSuffix(gcpModelPublisherGoogle, o.requestModel, gcpMethodEmbedContent)
- gcpReq, err = openAIEmbeddingToGeminiMessage(req)
- if err != nil {
- return nil, nil, fmt.Errorf("error converting EmbeddingRequest: %w", err)
+ var gcpReq *gcp.EmbedContentRequest
+ gcpReq, err = openAIEmbeddingToEmbedContentRequest(req, o.requestModel)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error converting EmbeddingRequest: %w", err)
+ }
+ newBody, err = json.Marshal(gcpReq)
+ } else {
+ o.useEmbedContent = false
+
+ if req.OfChat != nil {
+ return nil, nil, fmt.Errorf("%w: model %s does not support multimodal embedding via messages", internalapi.ErrInvalidRequestBody, o.requestModel)
+ }
+
+ path = buildGCPModelPathSuffix(gcpModelPublisherGoogle, o.requestModel, gcpMethodPredict)
+
+ var gcpReq *gcp.PredictRequest
+ gcpReq, err = openAIEmbeddingToGeminiMessage(req)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error converting EmbeddingRequest: %w", err)
+ }
+ newBody, err = json.Marshal(gcpReq)
}
- newBody, err = json.Marshal(gcpReq)
if err != nil {
return nil, nil, fmt.Errorf("error marshaling Gemini request: %w", err)
}
@@ -197,46 +349,70 @@ func (o *openAIToGCPVertexAITranslatorV1Embedding) ResponseBody(_ map[string]str
return nil, nil, tokenUsage, "", fmt.Errorf("failed to read gemini embedding response body: %w", err)
}
- // Unmarshal as GCP PredictResponse.
- var gcpResp gcp.PredictResponse
- err = json.Unmarshal(respBody, &gcpResp)
+ var openaiResp openai.EmbeddingResponse
+ var promptTokens int
+
+ if o.useEmbedContent {
+ openaiResp, promptTokens, err = o.parseEmbedContentResponse(respBody)
+ } else {
+ openaiResp, promptTokens, err = o.parsePredictResponse(respBody)
+ }
if err != nil {
- return nil, nil, tokenUsage, "", fmt.Errorf("failed to unmarshal response: %w", err)
+ return nil, nil, tokenUsage, "", err
+ }
+
+ // Set token usage from accumulated values.
+ openaiResp.Usage.PromptTokens = promptTokens
+ openaiResp.Usage.TotalTokens = promptTokens
+
+ // Marshal the OpenAI response.
+ newBody, err = json.Marshal(openaiResp)
+ if err != nil {
+ return nil, nil, tokenUsage, "", fmt.Errorf("failed to marshal OpenAI response: %w", err)
+ }
+
+ // Update token usage metrics.
+ tokenUsage.SetInputTokens(uint32(promptTokens)) //nolint:gosec
+ tokenUsage.SetTotalTokens(uint32(promptTokens)) //nolint:gosec
+
+ // Record the response in the span for tracing.
+ if span != nil {
+ span.RecordResponse(&openaiResp)
+ }
+
+ newHeaders = []internalapi.Header{{contentLengthHeaderName, strconv.Itoa(len(newBody))}}
+ responseModel = openaiResp.Model
+ return
+}
+
+// parsePredictResponse parses a GCP PredictResponse and converts it to the OpenAI format.
+func (o *openAIToGCPVertexAITranslatorV1Embedding) parsePredictResponse(respBody []byte) (openai.EmbeddingResponse, int, error) {
+ var gcpResp gcp.PredictResponse
+ if err := json.Unmarshal(respBody, &gcpResp); err != nil {
+ return openai.EmbeddingResponse{}, 0, fmt.Errorf("failed to unmarshal response: %w", err)
}
- // Convert GCP response to OpenAI format.
openaiResp := openai.EmbeddingResponse{
Object: "list",
Model: o.requestModel,
- Usage: openai.EmbeddingUsage{
- PromptTokens: 0, // Will be set from token usage
- TotalTokens: 0, // Will be set from token usage
- },
}
var promptTokens int
- // Convert embedding vectors from GCP predictions to OpenAI embeddings.
if len(gcpResp.Predictions) > 0 {
openaiResp.Data = make([]openai.Embedding, len(gcpResp.Predictions))
for i, prediction := range gcpResp.Predictions {
if prediction != nil {
- // Convert float32 slice to float64 slice for OpenAI format.
float64Values := make([]float64, len(prediction.Embeddings.Values))
for j, v := range prediction.Embeddings.Values {
float64Values[j] = float64(v)
}
-
openaiResp.Data[i] = openai.Embedding{
Object: "embedding",
Index: i,
Embedding: openai.EmbeddingUnion{Value: float64Values},
}
-
- // Extract token count from statistics if available.
if prediction.Embeddings.Statistics != nil {
- // Accumulate token counts across all predictions.
promptTokens += prediction.Embeddings.Statistics.TokenCount
- // Propagate truncation information to the response.
openaiResp.Data[i].Truncated = prediction.Embeddings.Statistics.Truncated
}
}
@@ -245,32 +421,41 @@ func (o *openAIToGCPVertexAITranslatorV1Embedding) ResponseBody(_ map[string]str
openaiResp.Data = []openai.Embedding{}
}
- // Set token usage from accumulated values.
- // Embeddings only consume input tokens, so total equals prompt tokens.
- openaiResp.Usage.PromptTokens = promptTokens
- openaiResp.Usage.TotalTokens = promptTokens
+ return openaiResp, promptTokens, nil
+}
- // Marshal the OpenAI response.
- newBody, err = json.Marshal(openaiResp)
- if err != nil {
- return nil, nil, tokenUsage, "", fmt.Errorf("failed to marshal OpenAI response: %w", err)
+// parseEmbedContentResponse parses a GCP EmbedContentResponse and converts it to the OpenAI format.
+func (o *openAIToGCPVertexAITranslatorV1Embedding) parseEmbedContentResponse(respBody []byte) (openai.EmbeddingResponse, int, error) {
+ var gcpResp gcp.EmbedContentResponse
+ if err := json.Unmarshal(respBody, &gcpResp); err != nil {
+ return openai.EmbeddingResponse{}, 0, fmt.Errorf("failed to unmarshal embedContent response: %w", err)
}
- // Update token usage metrics.
- // Embeddings don't return output tokens; populate input and total when provided.
- tokenUsage.SetInputTokens(uint32(promptTokens)) //nolint:gosec
- tokenUsage.SetTotalTokens(uint32(promptTokens)) //nolint:gosec
-
- // Record the response in the span for tracing.
- if span != nil {
- span.RecordResponse(&openaiResp)
+ openaiResp := openai.EmbeddingResponse{
+ Object: "list",
+ Model: o.requestModel,
}
- newHeaders = []internalapi.Header{{contentLengthHeaderName, strconv.Itoa(len(newBody))}}
-
- responseModel = openaiResp.Model
+ var promptTokens int
+ if gcpResp.Embedding != nil {
+ float64Values := make([]float64, len(gcpResp.Embedding.Values))
+ for j, v := range gcpResp.Embedding.Values {
+ float64Values[j] = float64(v)
+ }
+ openaiResp.Data = []openai.Embedding{{
+ Object: "embedding",
+ Index: 0,
+ Embedding: openai.EmbeddingUnion{Value: float64Values},
+ Truncated: gcpResp.Truncated,
+ }}
+ if gcpResp.UsageMetadata != nil {
+ promptTokens = gcpResp.UsageMetadata.PromptTokenCount
+ }
+ } else {
+ openaiResp.Data = []openai.Embedding{}
+ }
- return
+ return openaiResp, promptTokens, nil
}
// ResponseError implements [OpenAIEmbeddingTranslator.ResponseError].
diff --git a/internal/translator/openai_gcpvertexai_embeddings_test.go b/internal/translator/openai_gcpvertexai_embeddings_test.go
index a3776d7032..45493f0f3c 100644
--- a/internal/translator/openai_gcpvertexai_embeddings_test.go
+++ b/internal/translator/openai_gcpvertexai_embeddings_test.go
@@ -22,20 +22,24 @@ import (
func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
tests := []struct {
- name string
- modelNameOverride internalapi.ModelNameOverride
- input openai.EmbeddingRequest
- onRetry bool
- wantError bool
- wantPath string
- wantBodyContains []string // Substrings that should be present in the request body
+ name string
+ modelNameOverride internalapi.ModelNameOverride
+ input openai.EmbeddingRequest
+ onRetry bool
+ wantError bool
+ wantPath string
+ wantBodyContains []string // Substrings that should be present in the request body
+ wantBodyNotContain []string // Substrings that must NOT be present in the request body
}{
{
name: "embedding request with string input",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: "This is a test text for embedding",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Input: openai.EmbeddingRequestInput{
+ Value: "This is a test text for embedding",
+ },
},
},
wantPath: "publishers/google/models/text-embedding-004:predict",
@@ -44,14 +48,18 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
`"content":"This is a test text for embedding"`,
`"parameters"`,
},
+ wantBodyNotContain: []string{`"parts"`},
},
{
name: "embedding request with model override",
modelNameOverride: "custom-embedding-model",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: "Test text",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Input: openai.EmbeddingRequestInput{
+ Value: "Test text",
+ },
},
},
wantPath: "publishers/google/models/custom-embedding-model:predict",
@@ -64,12 +72,15 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
{
name: "embedding request with EmbeddingInputItem and task_type",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: openai.EmbeddingInputItem{
- Content: openai.EmbeddingContent{Value: "This is a document for retrieval"},
- TaskType: "RETRIEVAL_DOCUMENT",
- Title: "Document Title",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Input: openai.EmbeddingRequestInput{
+ Value: openai.EmbeddingInputItem{
+ Content: openai.EmbeddingContent{Value: "This is a document for retrieval"},
+ TaskType: "RETRIEVAL_DOCUMENT",
+ Title: "Document Title",
+ },
},
},
},
@@ -85,17 +96,20 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
{
name: "embedding request with array of EmbeddingInputItem",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: []openai.EmbeddingInputItem{
- {
- Content: openai.EmbeddingContent{Value: "Query about cats"},
- TaskType: "RETRIEVAL_QUERY",
- },
- {
- Content: openai.EmbeddingContent{Value: "Document about dogs"},
- TaskType: "RETRIEVAL_DOCUMENT",
- Title: "Dog Info",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Input: openai.EmbeddingRequestInput{
+ Value: []openai.EmbeddingInputItem{
+ {
+ Content: openai.EmbeddingContent{Value: "Query about cats"},
+ TaskType: "RETRIEVAL_QUERY",
+ },
+ {
+ Content: openai.EmbeddingContent{Value: "Document about dogs"},
+ TaskType: "RETRIEVAL_DOCUMENT",
+ Title: "Dog Info",
+ },
},
},
},
@@ -114,11 +128,13 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
{
name: "embedding request with dimensions parameter",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: "Text for dimension testing",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004", Dimensions: &[]int{256}[0]},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004", Dimensions: &[]int{256}[0]},
+ Input: openai.EmbeddingRequestInput{
+ Value: "Text for dimension testing",
+ },
},
- Dimensions: &[]int{256}[0],
},
wantPath: "publishers/google/models/text-embedding-004:predict",
wantBodyContains: []string{
@@ -131,12 +147,15 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
{
name: "embedding request with SEMANTIC_SIMILARITY without title",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: openai.EmbeddingInputItem{
- Content: openai.EmbeddingContent{Value: "Text for similarity check"},
- TaskType: "SEMANTIC_SIMILARITY",
- Title: "This title should not appear",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Input: openai.EmbeddingRequestInput{
+ Value: openai.EmbeddingInputItem{
+ Content: openai.EmbeddingContent{Value: "Text for similarity check"},
+ TaskType: "SEMANTIC_SIMILARITY",
+ Title: "This title should not appear",
+ },
},
},
},
@@ -147,16 +166,17 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
`"task_type":"SEMANTIC_SIMILARITY"`,
`"parameters"`,
},
+ wantBodyNotContain: []string{`"This title should not appear"`, `"title"`},
},
{
name: "embedding request with auto_truncate vendor field",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: "Test text for auto truncate",
- },
- GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{
- AutoTruncate: true,
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004", GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{AutoTruncate: &[]bool{true}[0]}},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004", GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{AutoTruncate: &[]bool{true}[0]}},
+ Input: openai.EmbeddingRequestInput{
+ Value: "Test text for auto truncate",
+ },
},
},
wantPath: "publishers/google/models/text-embedding-004:predict",
@@ -170,22 +190,22 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
{
name: "embedding request with global task_type overriding individual task_type",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: []openai.EmbeddingInputItem{
- {
- Content: openai.EmbeddingContent{Value: "Query text"},
- TaskType: "RETRIEVAL_DOCUMENT", // This should be overridden
- },
- {
- Content: openai.EmbeddingContent{Value: "Another text"},
- // No task type specified
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004", GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{TaskType: "RETRIEVAL_QUERY"}},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004", GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{TaskType: "RETRIEVAL_QUERY"}},
+ Input: openai.EmbeddingRequestInput{
+ Value: []openai.EmbeddingInputItem{
+ {
+ Content: openai.EmbeddingContent{Value: "Query text"},
+ TaskType: "RETRIEVAL_DOCUMENT", // This should be overridden
+ },
+ {
+ Content: openai.EmbeddingContent{Value: "Another text"},
+ // No task type specified
+ },
},
},
},
- GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{
- TaskType: "RETRIEVAL_QUERY", // Global task type
- },
},
wantPath: "publishers/google/models/text-embedding-004:predict",
wantBodyContains: []string{
@@ -199,9 +219,12 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
{
name: "embedding request with array of strings",
input: openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{
- Value: []string{"First text", "Second text", "Third text"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Input: openai.EmbeddingRequestInput{
+ Value: []string{"First text", "Second text", "Third text"},
+ },
},
},
wantPath: "publishers/google/models/text-embedding-004:predict",
@@ -213,6 +236,353 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
`"parameters"`,
},
},
+ // embedContent path tests
+ {
+ name: "embedContent: single string input",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Input: openai.EmbeddingRequestInput{
+ Value: "hello world",
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"parts"`,
+ `"text":"hello world"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
+ {
+ name: "embedContent: multiple string inputs rejected (batch not supported)",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Input: openai.EmbeddingRequestInput{
+ Value: []string{"a", "b", "c"},
+ },
+ },
+ },
+ wantError: true,
+ },
+ {
+ name: "embedContent: with dimensions",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview", Dimensions: &[]int{256}[0]},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview", Dimensions: &[]int{256}[0]},
+ Input: openai.EmbeddingRequestInput{
+ Value: "test",
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"embedContentConfig"`,
+ `"outputDimensionality":256`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
+ {
+ name: "embedContent: with vendor taskType (sent but may be ignored by gemini-embedding-2)",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview", GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{TaskType: "RETRIEVAL_QUERY"}},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview", GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{TaskType: "RETRIEVAL_QUERY"}},
+ Input: openai.EmbeddingRequestInput{
+ Value: "query text",
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"embedContentConfig"`,
+ `"taskType":"RETRIEVAL_QUERY"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
+ {
+ name: "embedContent: EmbeddingInputItem is rejected (per-item task_type/title not supported)",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Input: openai.EmbeddingRequestInput{
+ Value: openai.EmbeddingInputItem{
+ Content: openai.EmbeddingContent{Value: "doc for retrieval"},
+ TaskType: "RETRIEVAL_DOCUMENT",
+ Title: "Document Title",
+ },
+ },
+ },
+ },
+ wantError: true,
+ },
+ {
+ name: "embedContent: auto_truncate vendor field uses camelCase",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview", GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{AutoTruncate: &[]bool{true}[0]}},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview", GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{AutoTruncate: &[]bool{true}[0]}},
+ Input: openai.EmbeddingRequestInput{
+ Value: "text for auto truncate",
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"embedContentConfig"`,
+ `"autoTruncate":true`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`, `"auto_truncate"`},
+ },
+ {
+ name: "embedContent: array of EmbeddingInputItem is rejected",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Input: openai.EmbeddingRequestInput{
+ Value: []openai.EmbeddingInputItem{
+ {Content: openai.EmbeddingContent{Value: "first item"}},
+ {Content: openai.EmbeddingContent{Value: "second item"}},
+ },
+ },
+ },
+ },
+ wantError: true,
+ },
+ {
+ name: "gemini-embedding-001 still uses predict",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-001"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-001"},
+ Input: openai.EmbeddingRequestInput{
+ Value: "test",
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-001:predict",
+ wantBodyContains: []string{
+ `"instances"`,
+ `"content":"test"`,
+ `"parameters"`,
+ },
+ wantBodyNotContain: []string{`"parts"`},
+ },
+ {
+ name: "embedContent: model override to gemini-embedding-2-preview",
+ modelNameOverride: "gemini-embedding-2-preview",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Input: openai.EmbeddingRequestInput{
+ Value: "override test",
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"parts"`,
+ `"text":"override test"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
+ // Multimodal messages path tests
+ {
+ name: "embedContent: single text message via messages",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfChat: &openai.EmbeddingChatRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "hello from messages"},
+ }},
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"parts"`,
+ `"text":"hello from messages"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
+ {
+ name: "embedContent: image URL message via messages",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfChat: &openai.EmbeddingChatRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: []openai.ChatCompletionContentPartUserUnionParam{
+ {OfImageURL: &openai.ChatCompletionContentPartImageParam{
+ Type: "image_url",
+ ImageURL: openai.ChatCompletionContentPartImageImageURLParam{
+ URL: "https://example.com/image.png",
+ },
+ }},
+ }},
+ }},
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"parts"`,
+ `"fileUri":"https://example.com/image.png"`,
+ `"mimeType":"image/png"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
+ {
+ name: "embedContent: data URI image via messages",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfChat: &openai.EmbeddingChatRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: []openai.ChatCompletionContentPartUserUnionParam{
+ {OfImageURL: &openai.ChatCompletionContentPartImageParam{
+ Type: "image_url",
+ ImageURL: openai.ChatCompletionContentPartImageImageURLParam{
+ URL: "data:image/png;base64,iVBORw0KGgo=",
+ },
+ }},
+ }},
+ }},
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"parts"`,
+ `"inlineData"`,
+ `"mimeType":"image/png"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`, `"fileURI"`},
+ },
+ {
+ name: "embedContent: mixed text + image in one user message",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfChat: &openai.EmbeddingChatRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: []openai.ChatCompletionContentPartUserUnionParam{
+ {OfText: &openai.ChatCompletionContentPartTextParam{
+ Type: "text",
+ Text: "describe this image",
+ }},
+ {OfImageURL: &openai.ChatCompletionContentPartImageParam{
+ Type: "image_url",
+ ImageURL: openai.ChatCompletionContentPartImageImageURLParam{
+ URL: "https://example.com/photo.jpg",
+ },
+ }},
+ }},
+ }},
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"parts"`,
+ `"text":"describe this image"`,
+ `"fileUri":"https://example.com/photo.jpg"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
+ {
+ name: "embedContent: multiple user messages accumulate parts",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfChat: &openai.EmbeddingChatRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "first message"},
+ }},
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "second message"},
+ }},
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"parts"`,
+ `"text":"first message"`,
+ `"text":"second message"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
+ {
+ name: "messages with predict-path model returns error",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfChat: &openai.EmbeddingChatRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "test"},
+ }},
+ },
+ },
+ },
+ wantError: true,
+ },
+ {
+ name: "embedContent: messages with dimensions and taskType",
+ input: openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview", Dimensions: &[]int{128}[0], GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{TaskType: "RETRIEVAL_QUERY"}},
+ OfChat: &openai.EmbeddingChatRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview", Dimensions: &[]int{128}[0], GCPVertexAIEmbeddingVendorFields: &openai.GCPVertexAIEmbeddingVendorFields{TaskType: "RETRIEVAL_QUERY"}},
+ Messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "query text"},
+ }},
+ },
+ },
+ },
+ wantPath: "publishers/google/models/gemini-embedding-2-preview:embedContent",
+ wantBodyContains: []string{
+ `"content"`,
+ `"embedContentConfig"`,
+ `"parts"`,
+ `"text":"query text"`,
+ `"outputDimensionality":128`,
+ `"taskType":"RETRIEVAL_QUERY"`,
+ },
+ wantBodyNotContain: []string{`"instances"`, `"parameters"`},
+ },
}
for _, tc := range tests {
@@ -244,6 +614,9 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_RequestBody(t *testing.T) {
for _, substr := range tc.wantBodyContains {
require.Contains(t, bodyStr, substr)
}
+ for _, substr := range tc.wantBodyNotContain {
+ require.NotContains(t, bodyStr, substr, "body should not contain %q", substr)
+ }
})
}
}
@@ -636,6 +1009,233 @@ func TestOpenAIToGCPVertexAITranslatorV1Embedding_ResponseError(t *testing.T) {
}
}
+func TestOpenAIToGCPVertexAITranslatorV1Embedding_EmbedContentResponseBody(t *testing.T) {
+ tests := []struct {
+ name string
+ gcpResponse string
+ wantError bool
+ wantTokenUsage metrics.TokenUsage
+ wantResponseBody openai.EmbeddingResponse
+ }{
+ {
+ name: "embedContent response with embedding and usageMetadata",
+ gcpResponse: `{
+ "embedding": {"values": [0.1, 0.2, 0.3]},
+ "usageMetadata": {"promptTokenCount": 3, "totalTokenCount": 3}
+ }`,
+ wantTokenUsage: tokenUsageFrom(3, -1, -1, -1, 3, -1),
+ wantResponseBody: openai.EmbeddingResponse{
+ Object: "list",
+ Model: "gemini-embedding-2-preview",
+ Data: []openai.Embedding{
+ {
+ Object: "embedding",
+ Index: 0,
+ Embedding: openai.EmbeddingUnion{Value: []float64{0.1, 0.2, 0.3}},
+ Truncated: false,
+ },
+ },
+ Usage: openai.EmbeddingUsage{
+ PromptTokens: 3,
+ TotalTokens: 3,
+ },
+ },
+ },
+ {
+ name: "embedContent response with truncated flag",
+ gcpResponse: `{
+ "embedding": {"values": [0.4, 0.5, 0.6]},
+ "usageMetadata": {"promptTokenCount": 7, "totalTokenCount": 7},
+ "truncated": true
+ }`,
+ wantTokenUsage: tokenUsageFrom(7, -1, -1, -1, 7, -1),
+ wantResponseBody: openai.EmbeddingResponse{
+ Object: "list",
+ Model: "gemini-embedding-2-preview",
+ Data: []openai.Embedding{
+ {
+ Object: "embedding",
+ Index: 0,
+ Embedding: openai.EmbeddingUnion{Value: []float64{0.4, 0.5, 0.6}},
+ Truncated: true,
+ },
+ },
+ Usage: openai.EmbeddingUsage{
+ PromptTokens: 7,
+ TotalTokens: 7,
+ },
+ },
+ },
+ {
+ name: "embedContent response with no embedding",
+ gcpResponse: `{}`,
+ wantTokenUsage: tokenUsageFrom(0, -1, -1, -1, 0, -1),
+ wantResponseBody: openai.EmbeddingResponse{
+ Object: "list",
+ Model: "gemini-embedding-2-preview",
+ Data: []openai.Embedding{},
+ Usage: openai.EmbeddingUsage{
+ PromptTokens: 0,
+ TotalTokens: 0,
+ },
+ },
+ },
+ {
+ name: "embedContent response with invalid JSON",
+ gcpResponse: `{"embedding": invalid json}`,
+ wantError: true,
+ },
+ {
+ name: "embedContent response without usageMetadata",
+ gcpResponse: `{
+ "embedding": {"values": [0.1, 0.2, 0.3]}
+ }`,
+ wantTokenUsage: tokenUsageFrom(0, -1, -1, -1, 0, -1),
+ wantResponseBody: openai.EmbeddingResponse{
+ Object: "list",
+ Model: "gemini-embedding-2-preview",
+ Data: []openai.Embedding{
+ {
+ Object: "embedding",
+ Index: 0,
+ Embedding: openai.EmbeddingUnion{Value: []float64{0.1, 0.2, 0.3}},
+ Truncated: false,
+ },
+ },
+ Usage: openai.EmbeddingUsage{
+ PromptTokens: 0,
+ TotalTokens: 0,
+ },
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ translator := NewEmbeddingOpenAIToGCPVertexAITranslator("gemini-embedding-2-preview", "").(*openAIToGCPVertexAITranslatorV1Embedding)
+ translator.requestModel = "gemini-embedding-2-preview"
+ translator.useEmbedContent = true
+
+ headerMut, bodyMut, tokenUsage, responseModel, err := translator.ResponseBody(
+ map[string]string{"content-type": "application/json"},
+ strings.NewReader(tc.gcpResponse),
+ true,
+ nil,
+ )
+
+ if tc.wantError {
+ require.Error(t, err)
+ return
+ }
+
+ require.NoError(t, err)
+ require.NotNil(t, headerMut)
+ require.Len(t, headerMut, 1)
+ require.Equal(t, contentLengthHeaderName, headerMut[0].Key())
+ require.NotNil(t, bodyMut)
+ require.Equal(t, "gemini-embedding-2-preview", responseModel)
+
+ if diff := cmp.Diff(tc.wantTokenUsage, tokenUsage, cmp.AllowUnexported(metrics.TokenUsage{})); diff != "" {
+ t.Errorf("TokenUsage mismatch (-want +got):\n%s", diff)
+ }
+
+ var actualResponse openai.EmbeddingResponse
+ err = json.Unmarshal(bodyMut, &actualResponse)
+ require.NoError(t, err)
+
+ require.Equal(t, tc.wantResponseBody.Object, actualResponse.Object)
+ require.Equal(t, tc.wantResponseBody.Model, actualResponse.Model)
+ require.Equal(t, tc.wantResponseBody.Usage, actualResponse.Usage)
+ require.Len(t, actualResponse.Data, len(tc.wantResponseBody.Data))
+
+ for idx := range tc.wantResponseBody.Data {
+ require.Equal(t, tc.wantResponseBody.Data[idx].Object, actualResponse.Data[idx].Object)
+ require.Equal(t, tc.wantResponseBody.Data[idx].Index, actualResponse.Data[idx].Index)
+ require.Equal(t, tc.wantResponseBody.Data[idx].Truncated, actualResponse.Data[idx].Truncated)
+
+ wantEmbedding := tc.wantResponseBody.Data[idx].Embedding.Value.([]float64)
+ actualEmbedding := actualResponse.Data[idx].Embedding.Value.([]float64)
+ require.Len(t, actualEmbedding, len(wantEmbedding))
+
+ for i, wantVal := range wantEmbedding {
+ require.InDelta(t, wantVal, actualEmbedding[i], 1e-6, "Embedding %d value at index %d", idx, i)
+ }
+ }
+ })
+ }
+}
+
+func TestIsEmbedContentModel(t *testing.T) {
+ tests := []struct {
+ model string
+ want bool
+ }{
+ {"gemini-embedding-2-preview", true},
+ {"gemini-embedding-2-preview-0514", true},
+ {"gemini-embedding-exp-03-07", true},
+ {"gemini-embedding-001", false},
+ {"text-embedding-004", false},
+ {"text-embedding-005", false},
+ {"some-maas-model", false},
+ {"maas-embedding", false},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.model, func(t *testing.T) {
+ require.Equal(t, tc.want, isEmbedContentModel(tc.model))
+ })
+ }
+}
+
+// TestResponseModel_GCPVertexAIEmbeddings_EmbedContent tests the end-to-end flow for the embedContent path:
+// RequestBody sets useEmbedContent, then ResponseBody uses it to parse the response correctly.
+func TestResponseModel_GCPVertexAIEmbeddings_EmbedContent(t *testing.T) {
+ modelName := "gemini-embedding-2-preview"
+ translator := NewEmbeddingOpenAIToGCPVertexAITranslator(modelName, "")
+
+ // Build request through RequestBody to set internal state.
+ req := &openai.EmbeddingRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "gemini-embedding-2-preview"},
+ Input: openai.EmbeddingRequestInput{Value: "hello world"},
+ },
+ }
+ reqBody, _ := json.Marshal(req)
+ headers, _, err := translator.RequestBody(reqBody, req, false)
+ require.NoError(t, err)
+
+ // Verify embedContent path was selected.
+ require.Equal(t, "publishers/google/models/gemini-embedding-2-preview:embedContent", headers[0].Value())
+
+ // Simulate embedContent response (REST API returns singular "embedding", not plural "embeddings").
+ embeddingResponse := `{
+ "embedding": {"values": [0.1, 0.2, 0.3]},
+ "usageMetadata": {"promptTokenCount": 5, "totalTokenCount": 5},
+ "truncated": true
+ }`
+
+ _, respBody, tokenUsage, responseModel, err := translator.ResponseBody(nil, bytes.NewReader([]byte(embeddingResponse)), true, nil)
+ require.NoError(t, err)
+ require.Equal(t, modelName, responseModel)
+
+ // Verify token usage was extracted from usageMetadata.
+ inputTokens, ok := tokenUsage.InputTokens()
+ require.True(t, ok)
+ require.Equal(t, uint32(5), inputTokens)
+ _, ok = tokenUsage.OutputTokens()
+ require.False(t, ok)
+
+ // Verify the response body contains correct data.
+ var actualResponse openai.EmbeddingResponse
+ err = json.Unmarshal(respBody, &actualResponse)
+ require.NoError(t, err)
+ require.Len(t, actualResponse.Data, 1)
+ require.True(t, actualResponse.Data[0].Truncated)
+ require.Equal(t, 5, actualResponse.Usage.PromptTokens)
+ require.Equal(t, 5, actualResponse.Usage.TotalTokens)
+}
+
// TestResponseModel_GCPVertexAIEmbeddings tests that GCP Vertex AI embeddings returns the request model
func TestResponseModel_GCPVertexAIEmbeddings(t *testing.T) {
modelName := "text-embedding-004"
@@ -643,8 +1243,11 @@ func TestResponseModel_GCPVertexAIEmbeddings(t *testing.T) {
// Initialize translator with embedding request
req := &openai.EmbeddingRequest{
- Model: "text-embedding-004",
- Input: openai.EmbeddingRequestInput{Value: "test"},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-004"},
+ Input: openai.EmbeddingRequestInput{Value: "test"},
+ },
}
reqBody, _ := json.Marshal(req)
_, _, err := translator.RequestBody(reqBody, req, false)
@@ -676,3 +1279,127 @@ func TestResponseModel_GCPVertexAIEmbeddings(t *testing.T) {
_, ok = tokenUsage.OutputTokens()
require.False(t, ok) // Output tokens not available for embeddings
}
+
+func TestCollectPartsFromMessages(t *testing.T) {
+ tests := []struct {
+ name string
+ messages []openai.ChatCompletionMessageParamUnion
+ wantParts int
+ wantError bool
+ }{
+ {
+ name: "single user message with text",
+ messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "hello"},
+ }},
+ },
+ wantParts: 1,
+ },
+ {
+ name: "multiple user messages",
+ messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "first"},
+ }},
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "second"},
+ }},
+ },
+ wantParts: 2,
+ },
+ {
+ name: "skip non-user roles",
+ messages: []openai.ChatCompletionMessageParamUnion{
+ {OfSystem: &openai.ChatCompletionSystemMessageParam{
+ Role: "system",
+ }},
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: "user text"},
+ }},
+ {OfAssistant: &openai.ChatCompletionAssistantMessageParam{
+ Role: "assistant",
+ }},
+ },
+ wantParts: 1,
+ },
+ {
+ name: "no user messages returns error",
+ messages: []openai.ChatCompletionMessageParamUnion{
+ {OfSystem: &openai.ChatCompletionSystemMessageParam{
+ Role: "system",
+ }},
+ },
+ wantError: true,
+ },
+ {
+ name: "empty messages returns error",
+ messages: []openai.ChatCompletionMessageParamUnion{},
+ wantError: true,
+ },
+ {
+ name: "user message with multimodal content",
+ messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: []openai.ChatCompletionContentPartUserUnionParam{
+ {OfText: &openai.ChatCompletionContentPartTextParam{
+ Type: "text",
+ Text: "describe this",
+ }},
+ {OfImageURL: &openai.ChatCompletionContentPartImageParam{
+ Type: "image_url",
+ ImageURL: openai.ChatCompletionContentPartImageImageURLParam{
+ URL: "https://example.com/img.jpg",
+ },
+ }},
+ }},
+ }},
+ },
+ wantParts: 2,
+ },
+ {
+ name: "user message with empty string content returns error",
+ messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: ""},
+ }},
+ },
+ wantError: true,
+ },
+ {
+ name: "user message with invalid image URL returns error",
+ messages: []openai.ChatCompletionMessageParamUnion{
+ {OfUser: &openai.ChatCompletionUserMessageParam{
+ Role: "user",
+ Content: openai.StringOrUserRoleContentUnion{Value: []openai.ChatCompletionContentPartUserUnionParam{
+ {OfImageURL: &openai.ChatCompletionContentPartImageParam{
+ Type: "image_url",
+ ImageURL: openai.ChatCompletionContentPartImageImageURLParam{
+ URL: "://invalid-url",
+ },
+ }},
+ }},
+ }},
+ },
+ wantError: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ parts, err := collectPartsFromMessages(tc.messages, "gemini-embedding-2-preview")
+ if tc.wantError {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.Len(t, parts, tc.wantParts)
+ })
+ }
+}
diff --git a/tests/internal/testopenai/embeddings_requests.go b/tests/internal/testopenai/embeddings_requests.go
index ddb56fa12e..5a2d0d18c8 100644
--- a/tests/internal/testopenai/embeddings_requests.go
+++ b/tests/internal/testopenai/embeddings_requests.go
@@ -19,89 +19,106 @@ func EmbeddingsCassettes() []Cassette {
}
var cassetteEmbeddingsBasic = &openai.EmbeddingRequest{
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{Value: "How do I reset my password?"},
- // Python SDK coerces to base64 when NumPy is installed. Make sure float works.
- EncodingFormat: ptr.To("float"),
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("float")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("float")},
+ Input: openai.EmbeddingRequestInput{Value: "How do I reset my password?"},
+ },
}
// embeddingsRequests contains the actual request body for each embeddings cassette.
var embeddingsRequests = map[Cassette]*openai.EmbeddingRequest{
CassetteEmbeddingsBasic: cassetteEmbeddingsBasic,
CassetteEmbeddingsBase64: {
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{Value: "How do I reset my password?"},
- EncodingFormat: ptr.To("base64"),
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ Input: openai.EmbeddingRequestInput{Value: "How do I reset my password?"},
+ },
},
CassetteEmbeddingsTokens: {
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{Value: []int64{4438, 656, 358, 7738, 856, 3636, 30}},
- EncodingFormat: ptr.To("base64"),
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ Input: openai.EmbeddingRequestInput{Value: []int64{4438, 656, 358, 7738, 856, 3636, 30}},
+ },
},
CassetteEmbeddingsLargeText: {
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{
- Value: "The quick brown fox jumps over the lazy dog. This pangram sentence contains every letter of the English alphabet at least once. It has been used since at least the late 19th century to test typewriters and computer keyboards, display examples of fonts, and other applications involving text where the use of all letters in the alphabet is desired. The phrase is commonly used for touch-typing practice, testing typewriters and computer keyboards, and displaying examples of fonts. It is also used in other applications involving all the letters in the English alphabet.",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ Input: openai.EmbeddingRequestInput{
+ Value: "The quick brown fox jumps over the lazy dog. This pangram sentence contains every letter of the English alphabet at least once. It has been used since at least the late 19th century to test typewriters and computer keyboards, display examples of fonts, and other applications involving text where the use of all letters in the alphabet is desired. The phrase is commonly used for touch-typing practice, testing typewriters and computer keyboards, and displaying examples of fonts. It is also used in other applications involving all the letters in the English alphabet.",
+ },
},
- EncodingFormat: ptr.To("base64"),
},
CassetteEmbeddingsUnknownModel: {
- Model: "text-embedding-4-ultra", // Non-existent model.
- Input: openai.EmbeddingRequestInput{
- Value: "Test with unknown model",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-4-ultra", EncodingFormat: ptr.To("base64")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-4-ultra", EncodingFormat: ptr.To("base64")}, // Non-existent model.
+ Input: openai.EmbeddingRequestInput{
+ Value: "Test with unknown model",
+ },
},
- EncodingFormat: ptr.To("base64"),
},
CassetteEmbeddingsDimensions: {
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{
- Value: "Generate embeddings with specific dimensions",
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, Dimensions: ptr.To(256), EncodingFormat: ptr.To("base64")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, Dimensions: ptr.To(256), EncodingFormat: ptr.To("base64")},
+ Input: openai.EmbeddingRequestInput{
+ Value: "Generate embeddings with specific dimensions",
+ },
},
- Dimensions: ptr.To(256), // Reduced dimensionality.
- EncodingFormat: ptr.To("base64"),
},
CassetteEmbeddingsMaxTokens: {
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{
- // Near 8191 token limit for openai embeddings models.
- Value: generateLongText(7500),
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ Input: openai.EmbeddingRequestInput{
+ // Near 8191 token limit for openai embeddings models.
+ Value: generateLongText(7500),
+ },
},
- EncodingFormat: ptr.To("base64"),
},
CassetteEmbeddingsMixedBatch: {
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{
- Value: []string{
- "Hello 世界! 🌍", // Mixed scripts and emoji.
- "Здравствуй мир", // Cyrillic.
- "مرحبا بالعالم", // Arabic.
- "This is a much longer piece of text that contains multiple sentences. It tests how the embedding model handles varying input lengths within the same batch. The embeddings should capture the semantic meaning despite the length differences.",
- "🚀 Space emoji and symbols ✨ § ¶ †", // Special characters.
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ Input: openai.EmbeddingRequestInput{
+ Value: []string{
+ "Hello 世界! 🌍", // Mixed scripts and emoji.
+ "Здравствуй мир", // Cyrillic.
+ "مرحبا بالعالم", // Arabic.
+ "This is a much longer piece of text that contains multiple sentences. It tests how the embedding model handles varying input lengths within the same batch. The embeddings should capture the semantic meaning despite the length differences.",
+ "🚀 Space emoji and symbols ✨ § ¶ †", // Special characters.
+ },
},
},
- EncodingFormat: ptr.To("base64"),
},
CassetteEmbeddingsWhitespace: {
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{
- Value: []string{
- " Leading spaces",
- "Trailing spaces ",
- "Multiple spaces between words",
- "\tTabs\tand\nnewlines\r\neverywhere",
- " \n \t ", // Only whitespace.
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("base64")},
+ Input: openai.EmbeddingRequestInput{
+ Value: []string{
+ " Leading spaces",
+ "Trailing spaces ",
+ "Multiple spaces between words",
+ "\tTabs\tand\nnewlines\r\neverywhere",
+ " \n \t ", // Only whitespace.
+ },
},
},
- EncodingFormat: ptr.To("base64"),
},
CassetteEmbeddingsBadRequest: {
- Model: openai.ModelTextEmbedding3Small,
- Input: openai.EmbeddingRequestInput{
- // Above maximum value 100257 (inclusive).
- Value: []int64{102257},
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("invalid_format"), Dimensions: ptr.To(-1)},
+ OfCompletion: &openai.EmbeddingCompletionRequest{
+ EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: openai.ModelTextEmbedding3Small, EncodingFormat: ptr.To("invalid_format"), Dimensions: ptr.To(-1)},
+ Input: openai.EmbeddingRequestInput{
+ // Above maximum value 100257 (inclusive).
+ Value: []int64{102257},
+ },
},
- EncodingFormat: ptr.To("invalid_format"), // Invalid encoding format.
- Dimensions: ptr.To(-1), // Invalid negative dimensions.
},
}
diff --git a/tests/internal/testopenai/openai_test.go b/tests/internal/testopenai/openai_test.go
index f8243e9a2f..9177b56c58 100644
--- a/tests/internal/testopenai/openai_test.go
+++ b/tests/internal/testopenai/openai_test.go
@@ -31,7 +31,7 @@ func TestExtractModel(t *testing.T) {
},
{
name: "embeddings request",
- request: &openai.EmbeddingRequest{Model: "text-embedding-ada-002"},
+ request: &openai.EmbeddingRequest{EmbeddingBaseRequest: openai.EmbeddingBaseRequest{Model: "text-embedding-ada-002"}},
expected: "text-embedding-ada-002",
},
}
From 1db3a156f817899620d086b8039f83a8373f1a30 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 8 Jun 2026 14:08:04 -0700
Subject: [PATCH 03/59] chore(deps): bump the github-actions group across 1
directory with 3 updates (#2213)
---
.github/workflows/build_and_test.yaml | 12 ++++++------
.github/workflows/codeql.yaml | 6 +++---
.github/workflows/docker_build_job.yaml | 6 +++---
.github/workflows/release.yaml | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml
index cc8953ae22..48bbf859b4 100644
--- a/.github/workflows/build_and_test.yaml
+++ b/.github/workflows/build_and_test.yaml
@@ -228,7 +228,7 @@ jobs:
~/go/pkg/mod
~/go/bin
key: e2e-test-${{ hashFiles('**/go.mod', '**/go.sum', '**/Makefile') }}
- - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+ - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- env:
EG_VERSION: ${{ matrix.envoy_gateway_version }}
TEST_AWS_ACCESS_KEY_ID: ${{ secrets.AWS_BEDROCK_USER_AWS_ACCESS_KEY_ID }}
@@ -266,7 +266,7 @@ jobs:
~/go/pkg/mod
~/go/bin
key: e2e-test-${{ hashFiles('**/go.mod', '**/go.sum', '**/Makefile') }}
- - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+ - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- run: make test-e2e-upgrade
env:
# We only need to test the upgrade from the latest stable version of EG.
@@ -296,7 +296,7 @@ jobs:
~/go/pkg/mod
~/go/bin
key: e2e-test-${{ hashFiles('**/go.mod', '**/go.sum', '**/Makefile') }}
- - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+ - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- run: make test-e2e-inference-extension
env:
EG_VERSION: v1.8.1
@@ -324,7 +324,7 @@ jobs:
~/go/pkg/mod
~/go/bin
key: e2e-test-${{ hashFiles('**/go.mod', '**/go.sum', '**/Makefile') }}
- - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+ - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- run: make test-e2e-namespaced
env:
# We only need to test with the latest stable version of EG, since these e2e tests
@@ -350,7 +350,7 @@ jobs:
~/go/pkg/mod
~/go/bin
key: e2e-aigw-test-${{ hashFiles('**/go.mod', '**/go.sum', '**/Makefile') }}
- - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+ - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Start Ollama server
run: |
curl -fsSL https://ollama.com/install.sh | sh && sudo systemctl stop ollama
@@ -395,7 +395,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
- name: Login into DockerHub
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
+ uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml
index 7fa69bba98..e0296ce60f 100644
--- a/.github/workflows/codeql.yaml
+++ b/.github/workflows/codeql.yaml
@@ -37,14 +37,14 @@ jobs:
cache: true
- name: Initialize CodeQL
- uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
+ uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
languages: go
- name: Autobuild
- uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
+ uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
+ uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
category: "/language:go"
diff --git a/.github/workflows/docker_build_job.yaml b/.github/workflows/docker_build_job.yaml
index 7bac19da6b..f465c63454 100644
--- a/.github/workflows/docker_build_job.yaml
+++ b/.github/workflows/docker_build_job.yaml
@@ -33,17 +33,17 @@ jobs:
~/go/bin
key: build-container-${{ hashFiles('**/go.mod', '**/go.sum', '**/Makefile') }}
- - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+ - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker buildx
id: buildx
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
+ uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Login into DockerHub
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
+ uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 86e2528669..be06deb021 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -27,7 +27,7 @@ jobs:
# To include the helm chart in the release artifact, we build and push it here instead of the separate job.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
- name: Login into DockerHub
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
+ uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
From 8bc80393701061247f4ef8a018ac6177becc9eae Mon Sep 17 00:00:00 2001
From: Ignasi Barrera
Date: Tue, 9 Jun 2026 15:40:43 +0200
Subject: [PATCH 04/59] site: remove deprecation banner from 0.7 (#2211)
**Description**
Remove the deprecation banner from the 0.7 docs.
**Related Issues/PRs (if applicable)**
Related to: https://github.com/envoyproxy/ai-gateway/pull/2177
**Special notes for reviewers (if applicable)**
N/A
Signed-off-by: Ignasi Barrera
---
site/docusaurus.config.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/site/docusaurus.config.ts b/site/docusaurus.config.ts
index a4d5f67080..6689fb8173 100644
--- a/site/docusaurus.config.ts
+++ b/site/docusaurus.config.ts
@@ -99,6 +99,11 @@ const config: Config = {
path: '/',
banner: 'none'
},
+ '0.7': {
+ label: '0.7',
+ path: '0.7',
+ banner: 'none'
+ },
'0.6': {
label: '0.6',
path: '0.6',
From ef6044f3bba500e86d1506487f3ae6ce60ab1872 Mon Sep 17 00:00:00 2001
From: Aaron Choo
Date: Tue, 9 Jun 2026 15:29:54 -0400
Subject: [PATCH 05/59] 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
---
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 41b839d45f6eb2128d25ddce8373e158042bf5b2 Mon Sep 17 00:00:00 2001
From: Anurag Aggarwal
Date: Wed, 10 Jun 2026 16:11:06 +0530
Subject: [PATCH 06/59] 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
---
.../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 8ef267712b663e88dd9b0913a11a016218851ae0 Mon Sep 17 00:00:00 2001
From: aishwaryaraimule21
Date: Wed, 10 Jun 2026 21:39:54 +0530
Subject: [PATCH 07/59] 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>
---
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 b5fd042aff07a33403dbbb307146781999cce244 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 08/59] 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
---
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 bcc8031548a5435c9dede0ad0ce173b5ba5b2778 Mon Sep 17 00:00:00 2001
From: Hritik Raj
Date: Wed, 10 Jun 2026 22:51:29 +0530
Subject: [PATCH 09/59] 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
---
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 2bb3904051d0037f29a026896b2e06881b6bef78 Mon Sep 17 00:00:00 2001
From: Linus Schlumberger
Date: Wed, 10 Jun 2026 19:54:18 +0200
Subject: [PATCH 10/59] 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
---
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 1f4d197df6a85a8e9f5a93311bf531c6e0a0ef6c 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 11/59] =?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
---
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 ff1bba40697468b511d9afd4736326f0010f7044 Mon Sep 17 00:00:00 2001
From: Ignasi Barrera
Date: Wed, 10 Jun 2026 20:19:22 +0200
Subject: [PATCH 12/59] 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
---
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 3d12ace1b2da1dfe8669721b885663d97a1b476b Mon Sep 17 00:00:00 2001
From: hustxiayang
Date: Wed, 10 Jun 2026 18:25:39 -0400
Subject: [PATCH 13/59] 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
---
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 f454c2433f..649e5a9692 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,
}
@@ -1064,6 +1068,7 @@ func (p *anthropicStreamParser) handleAnthropicStreamEvent(eventType []byte, dat
// Accumulate cache creation tokens
p.tokenUsage.AddCacheCreationInputTokens(cacheCreation)
}
+ p.tokenUsage.SetReasoningTokens(uint32(u.OutputTokensDetails.ThinkingTokens)) //nolint:gosec
if event.Delta.StopReason != "" {
p.stopReason = event.Delta.StopReason
}
@@ -1178,11 +1183,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),
@@ -1191,6 +1198,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 8813374d598ab0b1246a19f1af488f438454c2ed 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 14/59] 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 15/59] 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>
---
.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 8671f195cd2ec9ffde340cbd58f0f0322e68b39d 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 16/59] 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>
---
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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