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

0.280.0 (2026-05-19)

Features

Changelog

Sourced from google.golang.org/api's changelog.

0.280.0 (2026-05-19)

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

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/setup-qemu-action&package-manager=github_actions&previous-version=4.0.0&new-version=4.1.0)](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 zcmZsD2UL??v+hd?NUs7)mrz9r(mO~Ey{V{_01AfQ5vhWJbO}grN@x~}bPxyv(m@G| z(xn=aUP9--`Mz_`^*{G!Ems!F&bw#N%$_~-%(IO(G18%-yi5rI01aGM(+qrr0st}b z1v2mx*Rv~m;2VXf?j0Wh5Y;FABPv!-F9$y)@-fr70aOfg;J`mfozxB00iZgW>ckEL z5V0M=HPtO15&fCI_n28H8#X6y9-^9kG2t;$?>2F`JtX=f&w7vUTL1~kWeNX4yh-`o zjwl@mv8;*s+0Omoj6v(hK&HlvGL8G)$0|iETrJZkuj3L9<`t!gssw^!QrFh!?c0%) z+@rQeM{To4vkfurrxnQTn%z^SkaOt}Rpp@IBQa2_|9*wnITItg_h|*24u07kWe;xI z4nXw8vEy4m7O*@2J&5rB?S?vI)%CzDX0!Ma?D*P&ijGG?n+ok;Ll8^MP@=0TrC0o{ z@mEg06TrXx z%3_>WuptMZnv&j)2DSb_FH2qs95cFz)3P2v<%-Ws*p+V*!u8bjlx`6J&lmu(d@Tc{ zqgRh_SD)CUT}xM6V*Zi>5Y3B#B7`eAjrNN+0_<-d$xSKd{(=1U^C}BURCv@R_^DJ# z`*y5W$gC>C3;z3sCPfyc4J) znDwk`+;vy{(uvJqUm=QwbctE!Cw|^nJqi`1{DMC{oB4U4@vjk>8^XH8ZIj@qrMTl5 z)66!^EYn|KCxFVCYcZq>qs&g)RX++btT+u0QT=N`codR&PH_>Z3tHgP(&O>^HPsQW zf;OtZhRn5OJtqy$09U8=qrRm=k@~Oi5WEJu#1(%)0o!rSE2eEq{QsD5^2x2`e>s>{ECn1hNN}*6iXbj_L?+6Y>o55*R-g(T-v`M3a6bxl z5TM!%X=jMK`PYCdXVCw$e60iML)w|xUu_)s{cjsufgU(p3qAopn`GbVYwe`>w}}Vg z>WrI)pw}i0)z$=^e=_~+*?^=A@!VB{#wu2Ax|jd|_R9eUY` z@1(n~vo93uH&SG?5iJb=58paT1Z`t0wP&PrglL(k_**rbhTy`I{WoUio`DPN2hGwR zAeZ&`#@mzyN0bu+mei0P?7@F1Jvbk9I_JCL4ho|>xV=oP{tKWas0pEvI&uUj4M z`3DEhdY|>#wzIcw`<#SrqnH#UAZdh%rV z;`tZN9bU4koxU)?754Mr$<^Wd!#iQSU)uH0x6aL;&sn$B?TMXn>h#X~1kYK655ZiZ z3E0dSnRwF{85VFjz^Md`6;l){9MYp`Im5_y9%s;+p8HBr00pv8=CwszBc|Ai<@RXk ztJdARya<${X-R-EeGu0M33^5TQFi{JQ>Se~;6CVw7+0}wGJi8ygcG{@NdOLi*kkfR zDv=KT9dm~;Yn-)+%*23}w#Jh$F*SIh;U6KpPEGJF~Ai51^;TDX9H9Bm;9Z-Gr?1<0;7U4X-d{BzA0nhXNQmPGMl_M4K!(z#OBmiic zJSV;Qqcv>7?#Q1R0+0V3sNgxR7xQ#yKE ztL)sTz6zu|#utUQu5~(@OYl^=Za}+<0o+NotbuB?ycD?)%N>;Q<;>hroDHd-REcHO z2#Z4lMYVcC=MT$!L^2~i#OStTILYVYpxF!PjWF;4u;Ahjc9oJMTNk<{y|UkK{+#qh zZ%(3b-<}h5$=Y#{)5uJ2@LB;uR<9#}`UmpHF@FTvU;PPq%al1Spv~RdhEX; zg{t%x4DDupU{@$2dL!Lhr9XnUJ=;sHksq|z%&@NUW4g-f z4owQv+RiTv2$>d2^oT@LJScU;sxSl<-ItFQR?fi7df9)ZCpp)mKB8Kd0Il zB=+>Ta0?sC3&Oe)dIXCG-o1b?;t5n3*Qb!I}RSJkYgA` zc#XtePf7$^tpCB*&;9j?C(-F=2*f0Or7>S;1n>)9kVK7jZRy@1J#1TB0c(B6M&zr*b0uZFj8Bm1ORVJGw!fm(N0^R9FtAWoy*;#D9J)e;TV{n<;T{e%>s4XpK$4r4lnE|9x<@2A2ju$U70dGg1X}q`EYZv z;p6Bf;KbCz|A)#S>^i)WSWi&ineQ206$?AZo8CUd0OsRr_om3e$1c>))d2(Z$+&Y8@8@`FOLOZd)!yMEUl}wgmq&>ym|kXyI6Plyp_m}=E-*3(u=%qv|B{CUPV}TxA1z|m@>LZ z1#qX9_%ZKCBhECP7$M~UFaGQ@{ijMfl?e~&`b0+mKUYDE8Pq|cDFc%(TI>;k7ZRgCWEmM&@6)N43qwjQ1@y@Yy zRYG;$B2%l?&H}|@-(KuMKG|E|J5SJi=t`3CLy##(P`z$+=g))3q31`HP98?Yl;$0a zLjzJwcA#jX-vSjH&V9axS%ONdVwB}YevvIe zouR}-POsbvRuZ9NCUzK?a`GrR*>^Be9;p=1Y0zN9+_e5Z8XS}vP0Afhx2fzrwTf)R zytY!zopNtp*V-)4MI4M6=9f9RL4*n%xr4i7a9ur=SA;rzTYop-Hq8(uRBF_g*SmFX zC3GCLA@Hh~ao47BV9c8Rp@jhR=noi9%{vLa`__GyObba6RZ3G-QTU%|8a?VNe zjYXRlKEj@Eba#hh>s67&{i54Hc$LS)yy!+0Zca$KFXXUQg?1bA53V`;v0X*mR>vEw@=rdmTw;4s%i8BMjx>44=wQI#7faP-srHkrN;iWE zJzDdK5!QkX=4tite%~MKJ{&yIeQs@#UEju4A~00#%B?6G4+%AWtUaAT_y^w7ZygK(T|>ZCjG8*kSAk(X2;@lktcrmYg3HVwdfj` z7cLpvhN9+4owwxzUENt(SyS(v$%CHpB>Q?PU!LfDMa+*H+2ozBVxfWkT6-3tV$KY< z`{F4d%g@xVqP#*c&{JN{_RNcBYKgJsUuu|t2_a-R9i5r8Qvc#r4+ed+?7t3UfGWWlp zGgOg+5CKQmv7FNEbCnpyniC&N6r$6Bwf`e=u3=Z;-82@JFG8n^+?saVj||9ZfMs5` z{`bOQ1zzzwIg?ZpCnmN}f-uUkCvK;kX1jQ$S_-FJpRs;3zcdJ~dQqUGw)2g2M%1Ko zquoc|x|(`o7e6qsZV&R@F%H;xdm}V?37_pFGvNDjj-AE1cO#TTG%W)9od-*bEs;<7 zh#tbHYJs%_ s@cIgL_SsRZ498(G=HSi7{Cbw?bF#@n;q?*m`jVx)mV{>3o*N5?& ztBxHbX1>^O&u-Ur$~xrd3Oc6@(kx_eD(ya@YuLgd3 z`Ym;SGFVksjCODQgIM^G;VgF#BTZ^Xz9uqz+0ICg4rqiz@@c(!bsIC$fQH@Z)u=H5 zQ|fKNw|3E;+y@?Xwer&DozArTQMzf)*{~P0MVB*xL63(9RkdV!IU>qIHe`U6sr$i8 z`q+pNQG@=CuK?oRcI!$lTVMW95oV~m;V8eOm&4-dr=!I`KXOtn-`BhOgg4Z#!zXdx zcf7>`a}Bj!{G%}8a>NVNAGF~lpLI46J|K3*+b%I=^@Nsq4uwlwjAT!a7Vb+8k66N& z#r6X`mlQ--f*!Aw%1ayWxB!@3F=MWDi@^(j7V*xHN7_7x$t(+)@fMh12=;RLB|-M9 zx)C(bdqzqiKH`+AEJeixBa1(Bn^bwWt>l{tl-)B@>W6b^sMR()Nn35dWPgw519&Fd z+S#qT>yt9nWUgs8hb{c{)$``Qd|N`KQ*%jmCrA!Bc=eJEetq|%6vGNGY4#};aCF>; zIi%0jG)MzPmdP9ogR7pfhCpY48QMYi@Tjd(|K-c;+e){fryhCe^C7IzVC{RBaFJq? zp^nc)fvN~7l#>WJyfIdM_O-Y*O`oQ03M_TkDmHe~7K>olo_Hx?xslW%1G6 zW4a{t;obdS1I#re^1<8q<+PpioRTJ={R7{1j$m-PX7cJ#58Wb|A?RAtmsvgvnr4>- zX3`B4VLoLEaA(DJElXab07O1tMc;WAEJIHoF_f;lpvo~7oH7ir9E}MTEEKaRReKRl zMq6@hH6$LF0vueXQ7y|Q;{m9iFYIIOp%TYUhVdyA``-vCZX+>aPUM$hhA411L$7Iz zI}AsxSpp>{7C zD2mDsbduWdIK$!xcu5lwuo|xu6{~1}tP3zu<@+1Wo(o+HG4{;j> zMFV2CIy$&n$(4?V;?}M+nCTrRZiK^PNci&o9rFZIOvXvjc44_LtSFfKyyf^&_{hLe z*A0o$o1E<-`;NR#=isra8J)QF@7`kUdAkB?elk_ap!xZLHL61iu8F+PAE^Kj`^T*XFM1{C7c$C(>8-f`(WDYkA<1>p{ww#02i zNSk=h9QWh#^AW1J5IYr|N~rj3kL^Fe*QROGPg!fcQe)gd06}+zkZuG6JZE5D%+oud zh_LHknYc}I6?+t4)L){K?{EoEIrv;G91ut|r(Z&$6SSe23;=xC{nKg0rFz5uXs^pC zv`um0i4kx+MN!UWm@MJe4AoPz2e(n{=3KeTD#}y<8}*Rt&f?FhC$pD`DjZ9Z*(-q& zr8P2`p5V3*?NtK98+}*eQ?7%XflG~2H8X?2uT z^80%8dX!i%022!w$r0ot(@)vgp{%#lgu*-K7Zu;V3|JS3YIoz<|EovZ{wa)ZcNFM%8|P;P%|wx-Fn*Rmt}YqsC~!N(6x}F= z8YUP+gmk#rxi?auheLFO@<3$53Sb8M)AGNPcZ@C_Bwuk8GCs3feoDidS!E_~`a5dn zib-~@Q&`%WgLtNXbC~MYEn2Sdz22eZXk=u5dDRX1v)uK?i~L0FZoN1~%H>hb4=xk? z!|%zTgC>YrGPGY8<&HugoyloGv^0r7ZY||m)5!1uR%Lx3O3QGuc<2yjAJZ|2c%ggD z3!yW4{?cJ*L*9-r{DodY%OsdiO2+1XSpIEt9#OnaS2JA-iHLuXo$gD6cL2m4s#v z-@6=_l0v7Dk)PSrt7hHj=MLrNWWju@Tq1c9R2DF4ebdR!Nekp&>!*P>7IF`=hK@eX`}!I?p4~-(Z47xioF6uyi@UaO@dW(eKXDlEZ?|nwDfR8 ze2`aWguyryN$^X$5p$uTlvhv$6Lq(4r4(xmYN26d=B?{%Fhv)#z~Z~zD052We>}y^ z-?@8#2VE*O;2+CW4aiKJB=|C7UI**SpiamEm$ZUzVcKf$w-yuRhQyZaoH;Lb<51ys zy{krG&mgi1oQ`K;tD6vy86BS5bJ9Q}fg))_sK<#U&8mciFkeN3-YnJ1cFmO`FUi^? zn^BO;8Xg;#uEd~UY8Cz!?y_#9s~O?<9e&IO>a|Lnm7&{ksiI3AaFJl<-*)3Am+3eG+zm@|eGI=y3*QxbS73P+Wj0I7;CQ6H@cx`?@pRvxh_ zP|N}VmoJ1+%2_X6^yDjha|aj131IwSLa-3vdGKT3fSoBd(t z!7fw}D@B*62eG7`rh6$GxS_b_N({Um)ars-=G2}yU0Wv^$WV16q)x?b6xxEU{H@QS z5K)aJjpfrvuu4HNbLvebnX2h56V@Y+la&&gX#9C;K<1jZ^W;6S-8U{%r~%Fig{awT z+GUr>%TLaISq19>qp(jqLi%l|#}< z_#|j(we?ZVRh}B==puv|j_Y&@1gWO)kyyCjO-S4Swf@6yH@D%5JxwZG{J=}`>0*2I zjwK%iZt{$Pv-zqwvqr(yKPBdZo!E*C*LFt^J}+}4s=+zE{TPyCj06D;qhUb^x}ato{Q+tC zl3sOmBw}rQEZs_q0qiDgM!uute8sBqxxHb>xt>%_+rsoI^kL39*S?S-^Vz$?6TWbD};M9&)$~eCdExNje5kwH?}2s;f2iF zysY<)BWVE4N5h=_Ut+fsN|5ekRWR%_nbmEB_D;l>#a zWjeB^WJS1lJ(8y~d=s9z6Vhd+cdJ%0Y3LJsJeE-mk#6-CAtqHvZNVS46##j5n$s)TIi8; z=RX9RGOK+07c3iCAp?(5a*V-f@q#*S8YwK)a0j-L=b*7TkKbL(0K+TC)Z4(g?ULRR zv*QKty|`|KJ7}xG-;<_5-Ncu=O9x{x}1J8H8-G_?yP2GtARq=c@JxWq;bfLIYNQSslkp zVG{Rqs?vOwEQenOrsIlO>9k#mclUZSB$4+z`215Rn8Qt05_5xi%?+*96AtglVb%PT zdL+|tqD*YajbOGmW zeM$adZD;2*DnWc1%$AwR^3dtWq%Uom^Gl*C0U4rt((w(Q<#Mk)#3Pptm z(nH`Ic{(^{-``G<%|P7dwh(D+oFu8j2v&T=+tcU5!)2!*ch1^guiRa)XV{2DV>jfV zXGfH(b{%)2m5H#|4O0_B+d>nYg{UsxDbK66zILby9bAbx&=MwDpRkn zZ;6jT{XE3khSZe_y*TV%^zB4i1(rzFMY%xI$vlJ0XvF;DEG}5_5O{oJI+}K%($_M7 zM5??TxlRh)oNNu_ThZi%6zxR?9pJ`aqcURPFV<&e4VwpyR3{~!+z|}kM858ySqDW` z7jo)bC8Ph{la_Za;@Oc|BuRu0C25%HL;MjP#7&0?V+%GA{@|@wHs3o6V=_~p4QE#z z^VQc6?^No`bI`;ZI@ul_Yjlj-SXuqqa|@C@eOx-8S40iE?rE@1(o*5+HRQ&r9K>q- zc&IU`Q1>t}Uyz)v4YdhfRmsNtri8I;LJ`f^yB_872r+sYd& z(ldvTCVkyH6IAgq(EjdGQ{pRqp(>7${Dk}O@&`Z+`!a&q!u1(@oK9lz=edR2+Mt7} z-DCZ*4ToyfG2T?qr7M0{^5jbv`vDggnXsL=l0LkKZ`)6}mKXuGe93G2Sd}uBB&lPT zd8+k{$8F!(b)V?W?H5Y4I;>j-%355w;vxJ?{+j0H9scFbAXUz`5~2e-Ej{7yGH~^o z90mDzL;JqLK3$*w+#{rWX~g1(8Q;VTg>ZrR>mk#rnYEWjcWTP|MuKhfFJ$5KR01|1 zU}JkxiSg-F+1L?W11c_gS=wrIxiW+MEUZ(rn1i)3FFp-wbkWHPI}gG9@uzcF>JP#p z90}zKNw_mjz^ZtJJq>=7d-zAG1h}X4W5-Ij@W*(%l|OKk*K4~zkENOUW35SiTKwgj zIIRP~Xe9TpMchFX#8xP!`Hgg(FGpRoO=%eiXP zey^QF{QK$Q(a%ZW0wy(SFcUbIqjpRmIVEIU20UWsJE8rXwE!}85_x7>p7ic%H zbrOB*UO-V#XKbUN&e}&ZB`ixQh3$;@Xrgyr+uj6Ww`n@!!ak8*nlU~%*eIUFss(i! zmv|Dr*?u`NZ{ixf9kX(x-LyYD?x8QrmW-1!n)C2A5gjOAEOTbhqX}D2{kf_!I@UIi zmUhXG!8vW2p_Iq$9&c1+hPfL~&Qu!_KNqBqyOeJCY^0!MXR*@sQ2B&)eKt(bwjr`E zM~1iwM1#R(Zz+pd@=}S!a;{60_|ByV5@sbl6t@tTR&>~PQ9V?stnnGW?@3AUuS2jA zghawWb*7=QK$(^89O*~H&mZVQRp9MPH=pi4$Y4rg7JSz=T;i|H_32vJug1K{>%@{v z-}`K_w7)1nRVuH^lD_5224o_6fDj8X4qfKM9!5r*$O4Qfkmc_&wX zK?k?8)#F)8v#AxhCX=|O7kbI%zt)m1WIf}H-ci;5@aWwHj2|~5q-$i+w7;`=`tQO0 z;<7;8A9AXc`~WB1;kFBy>p@gk@JKJS#~xhp8+Rk{*2%I84DJUtht%l{!3Qd*uK0}t z#U`f8k7y5FmpOQT|C?WsTOODHG#GQ`DP5D0DtMA?b+^KJqBF0w=8yP1$<85K4!8fg zO7r7b9D4k*2iVdqlCYg3v6W@M+J0f#??Wt2K9D&Q(=vd#7VTKrbfNq=TzBWP&70KE z9JLpaG=ce{v`*`%;gxWgT_3O0!^Pm%UFqMiyug0g{mv)us+8E>R)%b!UUr;s$_v|? zS0$e4n9oJKTt1eN*Tv@B_S8h59XJ_Toi2P_Td^&5+6xskxt&=HlkZUA?n)-U<$B<{ zLW>Z@NuKtv9PN0MB!HE9Uki_ACDuTMS>qm+86^v^AX}DE=ft9Z@^0XnM8Io`GkYQJ zZL25JF2`r?FzrcXvLr>>)bUQ+{D+&D=1F>njt?-L$Bo_nPB8?4#wLy`mwcesdcKuo z^Av1OOpy<+vUckN_nW+4t^E4Ecs%EO*0R@w9Y4=1=%$oq@sM-bP;{xZ$v2wXvo@uf zesX=$9{t(!kywWJ)nSK4FzPW{2hGxiF-F02Hpx4twu!2*`Me+NfTgn(iWt)7df&uq z7Uj|xIsExe9R+Xj2JV{dwg~q-u5BOkgrs{2>m4=Z4mJxDNwU>&@Ciw9b-O<9zHcE` zX=gZ|75#x2OIqI6E~xJ8r-HQf<`UJKAzwQSwjKmzdSUWj;+c1nZZAQnfx^d~=!jDU z&O34w$%}Ftcx7aT`7S)tAdlKrJac3dSE>#~z1TCpE`edH?x9u{v&T z)p&uUx;A~LzUQT*^=!M|Y%p2+FNf0QT%Nr@L+~nZ%G6thNUq2EA00FOEoJ1r#0W5v z&sCso=JV8nkmYN-fXx!x%G;r9KI;Bwcb+6{$v`Ep!W9 zDsF`h_pc0rXp9mq;&fu4hpmRdMOQ7n!T)JYw$GL_1*&^d-EP!uN!z{TT}N+mtGdc0 zsF|E)k`Kntj2>ij8d$pYMKfNM;z7h>LDWa|i_dDDnJj$B>oV=Nq06nEcU-|dt%_7* zFTkrWCDHvWRf@RFPUf^w!hm!kO9|hQSaN`2raIXuQ$tx8^=8;5Y`l#?D>dh?ljG;mBS6+x8;*%tHk{f zxUrBbPe>i{5KGLveFqT%1;D#}0g#Ot&CHcLfuU%Zm2(cBrBYfs#8 ztmFlN!Q;=a!OCO&T**I9bd0sq777>y#{ z!s{U_4Og|Z$GzO|ai(A0GAKjOGfm z(-M+3%fTL4my4?HEFD4$L>oBMSJLv2@zK%pfQ9?Rqb()N%qdc(yf5pSN=ojBPO^DM zZ`FUja>Hj+sQD4{BUHO=gJ2Gbl7M+T7~&w#@YpjhMcQ`mnFC#9#wlb z{p?B3ugh7`i2Mx8dt^7N{BwrDDk0g*8+H@~2tLMN^PvtaF_Nnz^sZg|&gUU~4kRz|4TaqZ~c zJ7?85xJ$e3pbxZs6+!%f;uvp#k(T~k7bsz%KVzlgI#ZyJBzK+YQGwmOg}esl9)(# zALj_?J#i;@t-|Lq`?;Oj$gAYBr|D|XSREz_p62dAA57;|uB%M}{ponfu#}JDFnp8< zATd;Jn-!!{l_H+@JalqZ*!t)?aQo>{y1h%&lV@zZ9HVVldi>t>u7F)Ss!_jbb!0|q zpajUegj&946f~iMH>CpKp@u%P2OJqm?C!cs&Ba2vRbNH2LPu86|Cr=Ss;i@u%5^$k zWW`xz1S6H#W^zN9?NHk{UE|r+nh2Ec3&#I&f9RD2;o5B z8Z|&n&cz_U>rK9IJ7e5?<>|SC`#_p+@kHD0#`jtE(z$l##RCJk|B-=ce%NmS zk07QwwjQaGUu5$VGou06KnJ5YK6|u1nY{hyw;xi$%S%4IXjFKBdcYHY9%@f?`+31Tx5=5u$Gdek#M~uJ+$H!fXFWbm3zQiI zc>O$V=yo_4zNx%!LIqb=VvG~33c_*4lt;sN`gGa zR%2(8cW?m^&Cdx%AAGTaHP{__hSukkS)pc_1UvIi0$~lc0@L4gnPocHI}&y53M4Y7 z5K78AuLp1-PBbeF(JRog8+ANDe}cG8Z){@1d@BT;|-Dct8YqG@;C-!yVX2H(C=0i9J`_rI8YGTy6 zEPOwh2LG5zj0OT2HEi<6cGs?5o8v>$?UBzH7WGDdv^3`4MW}%L5^Wj!tY~PZ@f+C! zB?`P)BgVu}riWcZ50py7D^FX63@4a>DF6v~Ch?TiXKS%Wz4zsT?3U}rw!fnfZ%^(k z17r7(rc%2awg`tB&!-X6#j8Q6kuVhc0L5`tZ>xs9<1OfM(yKn5#w(dwOTw`@1J53C z9~grW(Oi~7!HLySDpm9{?WW=7Cssd(|A}e|W=AK>j?Z*Dl#*PAap0!BZQ+>_>(J4= z*T*TcqdO~PP{~ILGt+oK3eEJ_U}0N&|6#=itL3}N`maSbD<8E}9vVQsvJIcl42VYr zF6j?;DhbQZT^%O0Jnc!Bqz$g7;v%cGn>xKGn4ZlJy@am@O;F`R04vnS7sG2{wjLW< zXCyZFyChLVi@{=KYB|N~?fEn`ua}l5 zH(Or(Gx(;8+=LXR@j;FiHK)S$kbY7k;PsbD@!LhK3KRkf6NK9#ai;4K$)>UDLm`B= zu(szbA*8n3n=4r)K>;!6R=Q@{RlGDb-`HS3dRFV`i+7u z0iJDsdG&W)6p1dilGBwWnCKQl0h;kmG^Q9OH&iCgSnQY{pW^Gf{F<46sB8mo_6Na; z>9-1+mD>U-Tw9Y)9=^q7a~|6^^&7{h2mu_|o@ic8nD6W`zJKH8ZYUTRk+dNk%`Gur zY;h?zO+mTSzR9L@K)fH=vbk+ypAq|^DzB+fRJ(aoF>d=)?fS10sszKw%ePdguPHAW zD=i$R{|tB0JSl8xgk53`PaJozi^H$J=kv<0?#HHB*$+GZ`hHTBG!#sy1+V#H%5_Un zP+A&n^Jn%{!()M9kQit2KA9J^P8UdfgnGvD-Rb(U7I795u&*Kr%&ItG1bI!Pq@^`A)LQFE*% z!3fyC!u42fOZwTqg2kuLns=hn*twkO9XvGLoml^9*mhOb(W(PH9S@>RBc7TMS^L(6y~P7t$J`Jw&$&0er<}N zyQUqUx%fFfBt9cP){p;@2=lpGugHus1tebleX1kRxldQsst5Mp;;UtvCDZfve0o7 zM8-qaenJnzteLiq<|W>w&ANbxD#e+j?lbS(#8lpEo+x%9uqe8;Pa5!t0ytp7rnHH% zMhI!!=S4g6G%)jrcFqf0D(yIPe4z>NQa_ z@lZ!mJQGzja@9ZX#uiT0k%kYY*;@oe{?1xd!!=jb+zQb;qMl8CG}w$D*wL8IJ~yfs z4{irr<$Qn4odQn8X>^G)pWt+D!B*|#+1;XAfiD)$jinO zlmL4%NF?s6o$G{L(8`ukbp_>TcT%`7eBapc-rn4Sw`Ri@S_A zeIm8jOPft4*i>a84QiP@eJsmWGBO(qOSK~|steWz<+!>{Y&G%H-Ez^uh^?G4%wOhE z4e2b#(;M}MxI|R%v3Q>L8ilrD4HHnsTXBltn?%tyc9C9=^G%@=e0S4!TXSB5KT|+E z>+`9sus{@H>VMs&S$1(3O!^Y9ULNpi&Rxi1tgD%Qi@>e8Co11Gb^3!$9mJUmm&S zKXO^vdFZHb-^h@8E!a_3=N7~qWq4J01K&!ZWrmVy{c@pw?e3OGCTcPfV8QvU^06`z zqG&O`Y1gWS(-S`KH^aotZ|%CSpLWzj$C^n5@*TBHm-VCXm%Untyz~WgEv1CE4`c|A1-ZjTP^QB-C_owwt-7hBPwV)Ohz_-H^Yqq}KV027Aeci}!;=7zsw8 z@mVJ6$+UG4ryfd7hAyJL&nsjlL@#Y7L!MU;6irM_jQRLu0$88)QjF?!7?eQz3B(@f zq_4!V7&D*oZ*GrDxX2T9XUMgu1NLpNw9)9N;TgdyCGI1cPnu}=_6CiF`SNZuaOPM?Je+4hxjNCF7s?WN zJ@cb1!fmw1u5z`x%=EFq2}>0zk~{G3p=Q*F73MhzVr+s#()C(M9@*PF3M0m& zfo=U{e6yY(kX|7&`ps|T^a`e}EO;b%ODcPYcGsZcm6zua1Un2=4D>AusWD2U(=(lO z9!lnyicazkgMKL@>8!1cerLPUnWI43QCV(|@=&DsRn;a}u-Fzo|81ZfVN7UDWi(#! zFbjyW3Tz4TTriz5Ig_7$i1*4&z3<~STwK}8>$Pup<^<~PKr)1L@!Z`teCkx>)L`K1 z30yTkIF=r8@!kq!RWvQP3TLk(4H^Gx^T*MH1XD&cZ6Ziiz$+hRwz#a{VY|5a4?7XU zEINC$Hl(uUpl~}{43_$`b2}a(fK=TynwJ80`r%!zvA6=CKUw{jG(+DFh0he3#_cG~ z5>}h+(M=p6*K*m*6XfK~p@Iut`ag8dJ|npq8Uo%+>4j*AEq~0&tX!BeI(nQNpZd>L zdKtBfk|8FlS1_dHwh-_Qjm*iWA|ED6FPxrGNQ|?#vih}6jVUX(>)gZUHE~Qj1gk>egsnne%=yckwY%QU*nTk@*xT4T<&y~*{d95& z(j08vKGvcc1#Ooq24s#fm)A6lM~l%ICVhzy4KrHBqoFQftQpMyG{(^8*h1h3TNY7S z=bVH5ZE{rdrBucZk#GFd7mbJy-s0{<`@Tr(2UOs@PKg@hZsZ^<4TtmeozlsP*m+tr zHZ;2f@CDd=I|w^(3A5m^B!9zD(}Y?!5J7=4H*jd#jq>{JVpB$4S~A2uLv8PeE3ghN zBH<3=dLn)tE|1*`Rx7C9YEZVF+_unV<+wGc^!tG^3Fe_8d$jJs0z07`LL@@p1h6Z(ig{>jfN|u>rM63$*?$=8!vaAaZtkhUIL1zx*FnAjKlt#88iLE;V@?xv_9gRc`)917fL zaoY;qi{tw+;FhRzvJ1`iU*VnaPZ?h5sXslx0I-$Us>*Q2dR!KB4Iuue_G-+Q#yW@p z1xydudaVDv;7t(Y%ehKI)6CODuEhbQlsAfOVcJJ`BMsALu*uSp!>fUDn<3krrUa&1 z7PZ&Oy^fDyw=7faQv>#&Rz>Yv9$WRv^VOp>EzyRnr$$FVTG@7%K7&l~+^05EY3M~v zF?$tRoxi0Lon^&NnZ?A_GbaF}yT_2qV@v%(pOr*>A>;%qc*MQZZe+XCVbNSQO=n3G zYZg}QBI%G`3XLj}zthGl0U;wJT+(?8mP(HZ#FRJv@(Q@z5XDS_sOdA4bP-_2*@)sb z4!WbkCo}9D_NBfd`hyz_$l`(`B|@2_viu)C0ts(?FX;NPF;kSy4$JO-Ax~600~ayA zPvBCM4q?_$yvmy{Tz}Va&QzrtGMTSa4Bq-jy#41?tTX29v%WIvE4)c)8XWed&aR^m z`s|WkE>@n5<5mj}H%i0CG-EDk@xHZc`z2caWxN?Jy=pM-D5DlTguPYK(g@Z=5+UDK zMm{be1e%aHRBA{8t1nubV%jJJC0&Ua!R*kg^5_pHkbv`g<-9K1!3Qq_CvxTp^?a2H zC8}(OP(#MXZ+%x(LTmiox!G26I3Gm#KPe>aaZaJ&4HDbiIU$HBe`3p|^OmxvnwWTY zK)Za}`OP(0A5)5%$$SUcX&LxGEM0dz)%_R$T4j{Vj+DJKva*$tYwwUPYs@aCi zGC%XNC@SJoPqXk@lRUw7)y^;ZYjAGyZ?tuA7OVssQLYYrt5~8qrPfV6Q$qu-cT$4{ z-JnHCgKAiQ$a;6sVkXdxMC_~6g!ha^;?8|Q1IVRc;b?o2YO7Cx_1nrw2ao~zI*MQQ(KaL6{eG3~i_0G+LLb@u+ksRAuW0vpj7N{| zq~oXCp9Gz{o3BvXs#GU-`Hf-*x+$@uwu+ZyF@)m7N>II&oY*VoO;jZg6dvBo z4{JRO-G2GJ`SvI6c=!7r&6{0`Ix%dAP9g zX1D7T)GcvJCeQ1lH0Q+-Dt#q%HY&QC}}h!7iU zt#{W^;*VzLL_QJjoPa*`Ndx@FD&I@_i?n-qwW0u`iVQVZDxeFqJp{51gmFSi))<~g z5B_iLEO=uXD4W$%T0G2x(QM|j@xZMLF|rbRIjcZn26WteY<%H0TVVnSXDCl8Z#w?1 zQaTM=j!GkoNL2S!-r9hZMw7dBA>Eq|&|QK4Dd{W^O89#5*;sYwYrZJ~>gn0ADV`@$ z&y)pJ-tut1wCPC(Z{g`Fn;u9?v{4uZ8VBR>&81{+;cZ}#is=-(axBW3l`tlt-Itjn zeVD;(&tWJn-h+2?K&w+hATkT608DHG?3rU+_^)fsz%{^`Bl@^ez|>S_Nghg-EnECb zik96J=jQ15nJrvxi~`J?Z)V( z3P$ZER|7R3(*UOw{mv%r7pB;v*j!~fJ?2~X$;xEYRFou@OX?X>Yp!1X18fcuG|Yi_ z4nr1ji_EkHTEvHmGqh-9PEnP#wetcE$ zU%qK@Fp&>2{88hcK%p8i=*}c$)O_ps;YNeS2Av?N0B$!w2M?Qv-Zj&h0kXDI3JFlz3Yk`VtzTYbEF7(|;KS4|W3#T#{o2Jj@(|KU@)F(2}2p z|7%;S%BXn?eC%Ldbk2V*klyzg`IQGm!d)<}8NF1bd?^J@lp)L`E5pN}5_b!zc0hVX zlIA4jwv7#mA_F$;y{h?2)m7&R=1)cL>8J6s?6$lPc~b&w8r2aI5gSCj(}qpIUKMul zeRU-r00run2Y9AGm76CIaJc!IQEdmJ$=`_iZbYtk>$_jWaCF`zXhq8ky7DP5qR%vv z`HT&T=W8{3#*%2Ck`voFjnrs_2s(ND?+V9oTR) z`IQfgF8E_6`y`se#vQ?d_ZxL1^V0$pd(c}DWuAAa@t;{_EO^{%eV#5NWQ=Xp7D37j zMXGN$8MSQI-iUiB{ub;E%9Hr4+Ua`t*$pVf?yP;#u}Ed=;i0Rbkz|^Dx^MQDS(gZx zG9a|DS&EW)pk|M_I1#^n0exw+58@4|cLecYV=Mf;p( zT@hlm0{3OT0Xq|DsD%p~CO`LvXB^D|FCIeEoV^f5?ck8!x0fL7?}OJ|QMxbDJ7)NA zv6qR%T9l-II{a~DGc3UT*9~;uv(LgtR|Q(VAOjjQBO!;?Eq$WE7%5UztqXMm>0*Iy z_qTV;*PO}zJT6b=c~h?$ElZx#&QlHj9{545Ez?*Ad%yS{BmN{f&)W7}RZ-kVnP?n3 zc7#V>Rw9^{c4|3VUgw<;Isv;%>uGGGJjxg(CmXil<0!`r8`3JeE!`LXIogZC5h^8* z{8gXNt>->)c_Vq82I9rEZ(d-d0L%l>#~~}izmHSByE%^Sm31CO{VMc42!wd&&g+@~|jlVtT zwx!leUP0aEq>Zz@%IpH>CO*2^B&4$=_|o^p>OiO1^qrdEhz&1ih{ErQvuu|YMbW^~#) zM#DFY0@3yRhiv`4I`~Hm9njmFJjiiisTyS+Trix@(SfhB7C{=Osa?EdC-t0D!Tdnd zQ|~AsLuFBlYTFXa6y99XLYweHr*I)zEBnUhfo@ti8&AwDYtSD46D3080^%HRdj*x~55%M|g#b z+Y;u0BRFw*9>Jz;fUnl)4j1vi$%(ICD08ue`qUj`Gb_YOlKD&@R6RHy8Se5v@;5m& z`}wpHuFLdbglJBQE9LK3=zSMZDbsXtT>@}gnZe?7k@-CMEJwtGUfB$3{^>gt91cN& zvmznKsbu?+cfW~;PVNgNhrD8dO6w>Sl)Mu9vHY}7%i^a#`PI9+EpALH3r+1_2<3ZS zyXrd1=X3c87Bc3lvaY;Jg*3A@$U>qI?N@<(AD|nu^&Q`wzcrXzmtZV&9a{1|c=1Aq zk}1D_o_bL6hHuyddc zl*#{erW*D}SaCehL!D1^8kp*`$1s239~Ng2)n7xUiZky2KIgG@cFOAaI{v@-E8X&8 zjQVHo4~FyD5o38C^F?qB0)7q@OT++tI2#X#c{{~lh&KX@jTR*-5Z^Z#SuhuU@=-C{ z*6R_)_l0Fox5BvnGG}r6uv=lDkk~s2!C6j(v~G)hK-J1 z5`f~+uvp8s_pS=qZ8S_p$1Cl+Y5ktR^GJDEN;#hpzD4|Ymx=s5Sn6NOtc>jPDXaVPWN&dEo9{VkPAaj zBsqu)$7pm8>*Uqj@^DN#I!t67V4bX9ic9q5Y68#jw;EJ2&+tq>|9@U{)dr`30nRb-6M}WYyb2z=-yXxn$JOO z(&v<7{uR>qUpF{>$Z5sBF#O?31?`u%)3F;NE`YL|ee1oquG}-^t)IrUi5+lJ?>2{>SA95D#Phv! z69iu{Z%L<)w#4VNWl+7T!UBleD=Pml<>|v$YC1t0>d`%Y3Qs0y{TFwhnYmi$Q(-Ye zmmD9SW#}?ca;wqXo?Sd1bc^PZ#5R?jU=>8k_9TB#_Jlk6M6a7fr0^E9}Lov=k)9|R|cCrH6=Kz?<;m9IPb$~vg7#Nbsu5x5?wg~pT84dK=vc9 zx>LUoI{!?TtV!5jaM#?BgP9H9gF093nz!jT4mPWHOiJCoeMvlKDw?UHIN>u^B;=(M z(>RhpPTv9zy_z|*rb&V_w@uJIq`Z7W+&Ob_XJXYFGW!XZK$)0hqYK_8j0Yn=@V^n3I3uq8n{+#WcfxN_i+i1}RQ0gd?Z4Q}3&h zEM9GYqmouVxgcilwuYT62P92Crti(lp(1W`*8Z-iBkefaoE^MI2mHxwt;HPUS@sH}L+ZiDAjCht&^gWb{pj>XAW}c#vT(~9D0sihkmG|{KSh7m$SFda=4Y;T(m(V_nh8nW88qOKMfQp z%~@?5>OH6-c2!FCHMluR(19bDU{Sc7edLrB?zGaPGl6To@8UkV# z>E9H^I@buaVmDrYJaF*6-#R65i`*R>vQH#?@rlO6hZ4`z=}*h{RFQg=%T3zDpQX0j zb7^r&Mu@JC6LDz~(04Ll*X@d&BZSywP5$nSolmRKhsqTl*J`_gCBra{x)1)e(E7!h z=M86peZ#c+^0<6frOEWwsDeLHSyX!bsYH@kDMK%&C9KX;bzSt!{0#OIO~ZS52bU3- z7s%KUks6!7Wx|u%xT)X4U z8NYG>SK}Nz`uAq$8;@`@#ze$iMKNP*r^cd<5xq{Jo%p>j=mKU2tcIDmSWdyhTg{Tn zB`3xz`)qiGEE+{Xci{dS%n0<`qwp0GPGYw%9!fMS1*}34Z_At^tAD90wpF+mnMfEL zD=P(+jDEr!gi}$In;X(YR2WD7UL>ov+m(yYx>qmN%}mpcZ7-K5-_{@~Tj-AB<`sS; zMrh^opI46r%c9|9qNg=!xz4-S)-xcWg%TWsU3>QVDd2?UtlV@A+v#oJVFf$nbhnv& zL+=&LXI1=xs1+8(YwoakN#@~`bacX#tL{HVuFqAEJ-aO6r?QovYGJ38Lk7uxf8{EK z8o4k+Hh6(9eD#!wcj-mipgp&MqRafo*<6a08nu9WS(@6MdwRintw;dC7d5K*$e8#< zW$q}re4cFI$&9D|anHCm*E9ww^!n=92EFp($tb|_cNviMe;qfB*jquobe>iYDE@iS2!Mb(?<;ej$Y?}(W<(ueLTZhyYKir+ z(394?YUF{cEI#BRij}|Lu>Gn3suc_Rx+;8Wc6H$8$Bf4jsMC*Prq~d#BB1{jX@{$k zNxWwBvMxWyMpwYg{04e2Cw!YKH3%VQ*jasAtKo`-y9)sE29Ww#MK46{E^a4mw2Yif zaj7q&FG&+@XBuYe<6VFICc>jU7k6+?5>lx!&irEcnkMiBH|Agd-w(X*JIETkmD1pF zxep{R&-X!t%q9Q%X}@6_S%}h3e;Y#}bisDTQ& zl9*jkkPqIpF~^-LFUKq|t%%94zMz+*YGwoyI&R!Y2m*2ua1#Fo3E0q}uEVbnKwYX3 zi|gy8R0;PUKYduvO6$+|9l?OJP?p}QvGi^e*PdxE!dR+EOlLC*P6a%M%T;u&PP*X3 za)hMo#AcHG?(es1r)Q1w8u4T<@!Z$KXZ3DWnuYCre=GBpqH}R2XZ&CoYlW!!9Vuw3 zhkj|eX+@F69W%?DzROIL4;ho2?qRZP&ss(IdP9N2Q7tJUGy}BcG&Wb6EElLN;Yy=4 z|K!h@=cQ3nbUw|0b>uyss#VLFp~4t%#)?O{kgpw5U-*5;?xA=t&kjx#A_BY;e0Q&tOsUp;$Y zdA$%?Ri{K1D^!F;Qt?SZi~nV(KGr(YpeF)>)f5HvDia05z+v`;m?6{(vY0FYROYHFOUSY=Fo zTu1)*5yuwCM{{=_is8SOGD+V#c#Z@WUh^oS1$c6wqDK8ms-Eub^vc8x3;WsleJo7B z29a$un6qyDPP;jz;iek7db3PtdB|LDi+7@(~FYwtd4{HM!&r;TRQg5m)Z%-BhZEahXkLb0z^MH1)_>6 z*!0ZNWZ!nb-?@C~U*8)5t{DqcwUA8IQ7Lcn<}C08_vPof<{@)^VHyycGQ(EE{FoKdfojECL`RG5!rySfOK#Y1$rM!;lFx|b$l?=-KaoRr# z=S`I)4g@@G-!Rfs2(kr=e2uwJkw2!-R@rjLBFo;{#xz%?;{JP4pH$2u)IBAr!XMh_ zLZmgDnRKZ5cYf~onai#YK-Q^>(~HI@CnvojqA4c_omp+x^a)TRG<_*Dv*`&C2O7Sp za5jY;8PMR4`Fayw85&5U{wt(4nEtXS98g+Aa~c zcQ0N@gHW%-YOumV#VYB%;T%!EZPjV_tXqNSfJ znK?_&IJ|u9>)Lad6%R&M-qUF}^DQl2NS>Rr{eAW?M{Xv?TPfYg#3D~5&V9G@D)-qI zzb~BAc;(UoRR!+d`q}N%icc^b={o^LtJO$ALdN)0G9Mu6U^0uA>&4YAzKhDsrr*}y zl-u?ceWr{Zd$rN!Bg4BYGEYAjP%^{POf9c^4XmS$3$N$TBXK&pPTMPwwsq-qf9o5?mAaXF#ct>BoY8+qiK?cUtSqS{--LE|5#Xt$)(FhOH%S_YO+w7H%!n%nh zl`pRQvB;6{793XUS4DbQBJvT(s-aj8=6$Nzoa#sC5~MQ_nJ;>UCS9qALVa-74RB&L z#|{5XiAhzPdz;FR^b)pn%vYud>~s?LDLo;P8-&qd924K~PkF<0>g#)t36pr7Q-CJR zmnm+q4%^wIC-F{VYNcOxP;9cQgoC#(*_2IIt?%nq&Y{#9zDHYEKJ z@E0{ou`P(*N>1*%PSxqukcs{KP&DNY@ejy}p)yu8QAxRJ?~jY?j?(j5${qmjo7TTJ znpDR5ND0MD4P}e&ppOREI)CWgb%Oc+*@qGf7e5oMu+v4FUd`~?;f~>rv{{n5<_E69e3o|dOcIU1#I`W3<>VR0>&QM; z@;R1L=qOywrE79?_la~Yocbb5X3A0d5$Jh{^Y0yPjM+72nsfkakW`E^7E~pwGDRz! zuldz%95!%rFLqg_qrhY#^3)#a7XigF)+bvfZR3?qEnJ2`uVz}U`aJi*KXzGNs`t}m z^nRTaruA7>v8f~bK42tAEnZvPlLg4Fh0yTM24Kd_Z2s$j5?R)2k33XOL3W_H^7;j3?O-n+!+y^#)JZoy;17_kIT%xnO@fW22A@mNG42{LB#bf&S6 z9mgX!(13xxLZF56@Qn@7*ACJ?{UfEgv!j(ty7UHZHpgPO4Oh=2BWkK_Xq)qjg{5PM18Rth zy(%pqS+vOFTT_j5*8tdJ0P8Pl0E`{!Q!c`!mbBlF)*9~)nrY>{lm5t2Fkx>Z0VxYN zq9!gQX%tzzK6=&k1Lj!d^!%6{6c~ovhu*U;so^zCHr)y;_w+aU?Ms*baTEti}YlQSjL(T7gu5sPkPte9WyKJ}VX;^97WKR&ZVGd#TPEkEwY~hb3`jy*uCLF$Am1@%1kTqbEbZJ5pUD00)Fzv>g+Kz$l1t6$~kHtTxErzQq7B9^K02ouT?()Yc z=_mZ1`F*!T|DRby#H%#!dq+7C2BF|~ZXWlXn9qCNfoiFkk?mwYQ!7%F8t%t%8*mrFQC=~4dTcapL-K@CSK`bc;73MucfO-Fx)ssvQD9G;9_J#9+J z#x3x`U1gB&(;ifQLl{bX_^73IJ=or0__7_6tT7sWM@+p1+=gXq?ulH)<9bmu$<~in2QvpL; zR%4H}8gn&iL^alvinK*3m4ghnaG@%7RNAf~-&iA%dNP&y7=>SDs)GE3%k|AYOTI^A z_hCPfIg%ti4hH+m`%s+CNp#GcfN1TDs1@z4d5bCA<0kS<-q;1SFKSbRjLuoE; zc}WRXnF6OX97$xAV4Ull`lT27A(p?G?GQ ze%mdrIOE^Tc~^3eaSySeS*nzmUK(y(s&iO~5bAB{Ly(im zf4WeReF|{$R-oJ<*k7|kI%&q5$h9Wz;eQDNgS3cq0R5aJcmQ5FqeA2e6jVo(tnqLq z`p5V<+l&kIb=ot43|vxxiPzYacl-{}mx-)reOT|1TH2~4R%GvGwI z;lSgNd><^NQd}Gh7NlMbdQP-C`a?C9vya&)o+pgZ^WK0J3h9=sk*XLh`?Lfe@M?Bpu&EczB%3a*-WCkL0Tta$HCKA2vhUH~ z01dX~pi1P!)8E0#4Gs}p$)~azxCiF*A;K76GD2FJ!!^sSs)?iP^mtBtHoevK0Sjx} zU@r_Ke3g-5O+tx%tW^GgLo9tF+3RY3(RnpT|A?yt$vb~zM+^@zI{21FSiQsfn%78!p-U`QF;rP zvP-|sk6Zjq!!PE(kXPSs6|AFwZlIxOKq_+vmh^yUGQG2Hd$e=yM$*$bs6<#9mScFL z-;fz$54c+wz^6=Liuh9!c()zz7fP}OedwD4h zZpZB$gGX1CD^RC7LR`tj!UiN=ix211MXhnT7F5+E-pH*X+eeLV-F(!Z6ZT3t@t^Y` zz+&pDNCd-Qd`oSYGG4 zl{fWU1DY5bMPaix{l#m_5}4$Kv=M#p?sag z3z_c|)(`DmAxN5@nBs0}+QMJV24_F9QME1cAY_N?zaImI+WsSk)UZE5;VinH7SCh? zh+^^mep?#=7BzxiBew|WIdMP+hO%fUkVAKfagp-E*~?(!!dnpy7Q&>3>y3pvKkVW` zjEj1qL5bC*$Eh}rpXI}>>T1E_?K8G@ucg7xP(cpYd4Ug^6Xx7iY8#RQ?3Wa z;tfiy-zq>9`fc+bb~jTa()3S(Zx;_4+ZG-e;G)bU&9RMG0QD4idJVb16Ki=AXTuQm z!%#%^WWzUwIdvW z-lMfUnI28jD0H?oN{;)Rk!&F;gs@aNOKQ<577uAIc-ck9THHWm=$P|TjTo}8sau?; zVPPKj;7N)|*1W;L=>GuE1W25L1>32TSRols7fZPiyAIfI^2a7#;;UxlWl2Fyx)}H0 z(Nj^ag1pzCk~g*D`JPTe0yHl?VB_wHUZZiXYH^qqgFaub4xJyIv$4vgBwvThUMzf2Msp_EHdmCKzseY= zwy<%oWrC|2Z~4+fMSZHOj)Pir9XjvRcZyv}v@h1&{(A1}pah$fj*!-vu~>_1Y{HM* zF>)Q1zoS>hTIV2d%sdJdkIMgF9EpoNRSe#*cL0BIfTe$Pqc8r!J9hPRarrC|m!*!^ zbFjwzIrlq%0#uK3?8RiBH9VNoxRJ>EHa{W>ImWFuw!<3e=!mCY52{&X)E~vhIF*aL zk9NfeE#eD(2M^v4CjGERRR+$^%!s-b--OD>G=6?vSM(riMsNhGr^Jad;A z+HKn-L;tG>>^H|bugPbD6#oA@Y?h@`Z1ygs`dPh#^l2|Q0U{256QcJAZ0&FjWuLJh zs){tpEOh&KxzZ7Hae0~0{+s=aFN$Z6Uh25{-r4c50jD#pMF0hG#!(!~3R^HPlgnTBC*0W?lua@cN*oNm}MMP^ap8qV(g41%0W z{@0b1oyn|w9yew3!MSSumGT^oSPd`U=UZ+hDrDSFX#iU)vrIuo+w3tB@+YFqJ6-D? z&K{TlTEp6DQzKdb#Vf&ToL6}{oSTA)mKZ*mj}XLzGzt=#)G^qP(g9#)Qqea7dCk8k zjBRT>z?QG8VdfwWf!L6|-+m>^k8%Zplg(hHOF1PXz*AstM}14`A(!{!_YNBsWNR6{ zdfG*rmwe3Eg?=;Df+#_oS&;W|I{d2suTNQL=xt72=pxZ^O&XV2Ri>UZa{3Ky>Gcu_26_w8XWp3^y=v^JTE+VJ3%gB0gceP8;^uTZUCdQ z9$#rGQGifaDM{>m;n2o8pfV`$^iTsY`+x2i^zbjo;$xk~4b4F#_7Ei%~hw zf>Fyux28N*a~dvIFKQ?*(Q$a6xf%|GIHYE zU0;vwpf*qHGb?vea{y0S=+DiP`3C|<&$oiJ>}PIgFV{W`zl*61@7@($;KR(YO3rgT zYx7tct;H}AFtZfTz^putows#%U88*~jjJsRbyH4t zc;lBdtyV6g<_2=RB>7Lj?SX$sQlVb~dcsXB9(2!--OrvuDQE$W3XPWb$_6%0%Dq`4 z9B8)~sCRTS~b{o?B0Em7G zgM3&7p-6&qR*mX9N=a}6s08lpfKmu}cfbE9h**USUelC{9b(1QsCY5{a*HQhu0IuC z$K^==I^H@9N-N1ne=!S_6V4ojeHS4R)h+8GV3P4#n8nhrbePSpQ%%2K)>$WA{Gl7n z$;+#}N-2C#eFC=DHqQv-Aya-8)`KXosXMBB4LN*W^iIUNFRy{|Ym} znrbq}YA{YxFC30-^`O^d&>o-zy7mUVV`yKEPX%yepQV2_q1+J$>aB2AW1=BI1p2;3@X* zT*pE&jJCAF(z7QUaORhOSfC_X- zt^T>1xR8x$RZG-MJ}ed&Dd{N|-V`VaJ_1F74W?-E*Gz-H4xY^woEQ7oxJ4UK_2@)JYmakD;VQ_grr${BC`){i}qEF_hJv3{%E5JCE{H~L%q7+Ujh{3=qtJTFLF)p_@}s;XGO4x2FScd& zr;=yDCD$;I{IJB=00go9Kw=gc@G|8 z^<3g@9KUEW34o#^?kJ2_!A_5ODend*XSibyXaOr$=jrcPqq}7lnloCQ@~7`XhX1U0 z%zBv)@V*rtb&oOG8n6Qjw@}C11o3gmK-gV&Y;UR%6k~w?L|00jT#MmtO+AfCNI%|4ChOEE@_8 zoT6O@pUuPu_3xw0>6VUKp~Qzfze-=MT}*_Tu0Wl)Avy+LS<{mXsxW*m251_K?iqp> ztBV*RMT+={->?=iepoRPZ*On$LeooW!f0`He|O2>DwAJcVSYkqvvD$)cdQMwuMpyP z`ag8bs0G#}LYhSJXIe{iClhS%r$=cA{U`$>MLHLsc^RsF)Sd+ohB!sh6?!MkhN8sD8T%1cd4`c|^7P$)CbO`o> zJlAyhE;znmizJoeGwc6VNhzRjXK~UcAH-I_6&(A`d9e#NdgePtQ|aY1cO4628wqn~ z)|{M^pZZ4n`9Txc8QYy)df@n_-GHhb=j?OJtB>E#xnssuM)R79Eb%c~9y^X{;1&62 z8qVFXX)WVY*B0@{ohTNG6kYbWYgKSXV|G1s1wShXQVRdS->w?V z^k91-AP0VLG79|sY4I6ayjABu@`nHNF!%* zI`UGp#MAvd{UZM2-+U|L26X>#cAi6<<2=zfXHT9w=h%w*hiUF3;~@T8Eku7$BH=TV zX{G#^pb|w~4I3IIdmuO;rueY&#bfFqXEho8*zxrTrW%o1gT@i)6;$!sRs9v0T4 zq`Qw&ig*mK@!bvUTHLZM*Y@OjxpAdV| zMvz6vx4ST#*p*0+MhfUS%&5`v+4EVF<$9u`guN_8pO|wx@*29t0I|4El-1wo zBvqyPm4_d(uwZYl^wbs$DN5ra|;{q z9NK{H)hn|f8yDC> z(ZIKUbl~2^_`10HMI3ofFjbF_Vr)U>4=!T~|KWbk4^KTgOPY&Z4Wf4l?-@%E4Rl^7 z!dilPIZ53_3e4Z))g~x=Pj0elz2vuVgC_dS1WA__Fc^jD&&35KCN>yYge6xnHYTgy z1chWZZ!PTb6pRTFzHTfNBNnzS9RX3bx+;lP;gDjB;Pexp)j7*|Ps&)+v?bUHJFG+g zoNl$@ju6ToY-)47-05cO#4acabc7DA)0YlUEwl4zu$Cw3!91&XI$38XoVj+0CA84f z6`DF_CgUx8(Jjo+we1fB)TFC#Qq=#%CkH2uX{aVCJH)l@g;q*|wI==AoVQAs0^@^s zpQ%x2F6wU?K-mE;^ud$!VO5Eb>+~c0jqb^;S#szYhe&iWs-hwPe%ubC=M;6{3N-k>Mpt@QV?pV1Mx)uI$m9oSyO(rUw` zP4XEiemIZNDlBn9Zs;SVbd_g2&31OcywY*#i)Sr7`3&s{_D_5<`ibeYEBsi9j;buD zl!8%PSC#7}^1KAlZM^yG8%kdve`CvIHlIgv%$D9rcf#$?0JF8Fhd;fWs=E5kkly{* zOkY)Q#c`(R)x2-D*u>neFRR}x$g^%RJ0FSaLGrh6q?Y{*Wc}>4V9j6+Muvsw!A0YD zjo+#!TH+o|nWhxxd|H`NZP2Ra)T30{qNziFFjeVR*;!i!eY^P1PC6-Aae$^bRP2VQEq6B?NeN8)QNHaZh!OA(bwfnQv)r9_G>9schrg>>vk%MRrv@BO zvq+*Eu9i8!HW^kwNO}w9|8RpYg!F`st&Dl^i7l^S%hPo6IgsKZX1?*StpP_5{(|zX zlWD1U8MSd^cc0U|apdV4se;97YvS8e{`uN8l;Ykk(Ik@UcN=%O>+$KswNTQCZnJjFV1|&kNB(mfmL9$cimYC7;c4Vu93*T%^F-8TjA0l z)MejQvi(L@0?sj1g9DD7w8MJs?M+a@1AkqXW_b^;mQ4! z;23$y^&n~mq`Ugtr(~S1GvYJlEBsDcatDIR3Z{n1Q;#7*Q@cBDl;{KMOO8%<=gwyL z8m_aN=|zcrej+H#v4BB?a*3G%-P{)`DyvmclEOZrFplg4pS3MgA^mR-e3x7~=TkX4 zg1~r_R&1g*Jr`M>C-;cV$FtAk@Ln7UQsLWnd-X|_!9JflZ`w4*=EO)BGI{*x%@Xsb zj{k10Z`}RNv~-8{4cAZwyE(X5unog;EJD^j4RsbGFaLyB8sEV6R_FJz^5nSs$#e?3 zn}gFtPl0dgutdh#8-#~z5Dvj_g<;t)*%81OR~1oM%Yo|4MAt&Hs|^bi$`S$EOhGgG55J>WKi#`N5%!^BtQ z3MIk5`+Pr~om}0dO)szPddhj_9jlJ}@E-wl^TSfMw zHv`N)6YQyl&s2URC8DckzY-b{6o7+2t{j^-=5TfJ5xl>LWlvjzV{emT!(NT`Wz8ZZ zbe%)M;H%cySbejXt8*da(!jm55f@9PX3=A*68HppylrP|aqbbtEhJYF1?omi${HQkRT-@CJqcrf{~p6;vRMw%k|^RC75bEz2h z*42UMXw-iBV{rcPVt zEX!&iAIRx2D=<}yggd>;mXJ^1&ZTW3yUq|Dt`k1gxb(Y7lzlI zyWdacaH+W5rd(9%V7zH&&28OWq<=Q)M;S<&w4G31zF1(*I*F5QcCH&&N)M%0Sp2|;1w|M9{9u8 zF!n{}ncXUSfDJqD_<>0PwioK?1nKrxgn&5b@ZWfzOaTl1QyHQ(@D;o0hA~PKzEs1x z__o4ml&I8^U_k%Q0J!*Bp$C3w#5pEQ1pa@b`pKQ(*eY(|1|lP0B!R5vQM)(Vn~S+R z!a+BV7;x0@3+2=GG%w5!t6u6^b#^;uUF9!h&}mCUSG;Do7x~ABhKgrK1#lmVCGFcp zCVb2UorBZorb2=PT_ryLt#|0Isa^wV2PJM07F7BYoqjuYzf3dMvRZ9hpCC1gLohkY zh`)VLGi&vf)@ofg2l*Nx6g^jX>@LgJa!%Am2>3CQ2f@7UeX; zQqLpZK$A+IsOUjjJac+q#A$F9jmgyrTnxTd+W*uvs8U<*~3y!N?l-8gVN47YFMWezpMsy4c2{sD;3g#_Uym$~TRxNq7GB3)E)sIw$* zq%9tV$*;5u+@@Yv{FYLf<9bhKD8mJ8;9`TRHb!~o1|(j2aXRghrtSTU`yQ2g?AWkq zaqZiB>a2H~resdXo{FTN&ubAlV~>x;?`qsTW_^mYo{WT= z`qqFmjyrBtS#L(*S3&NFXWAkjxZPr*?T_^O4M>P17BJe;&OU?_)WRNxbqroa-jZq=tO) z?S%>vIQX%*Sch3yC6v;|h+SsxRWUsU51pbV62-wH`nDXiWe+!QC#!UiAI^&&-2m~0 zdWDd$*MFPlpuk2@`jn2{mEJSI&U9oLM3?NJ`?BnMnPmtJ@5?J9&r@q*4lQ{Ge4jCq zW7*}Q;|qm?ZDCC!=6tvhn_$*;VoFQe$Lpi^W;Nd=uctS^or452=Y`$R-95#cXQu2> z9p@4&6394pw_0@)xl#zRmk&2#uzGS-lY(Uy(5!k%=VE7#gsTB_zTpPPURi}d3qlEW zK19wKO~#jMYi=>k#0yJFe8l`kNGdKxS?GJ_N!5rP-I4KNTFWF_>vcs*ok5951ZOJr9;53ak*_j#Xt=|9(<)V zrzble6>V!GxZ=wD$Ddf#oH-@xC2Y| zz{Z-}!jBnYP~*{O?=uFjWSFrv`cO)eJ}bDnN-#ZOBIi5sMfVTdNeXAMeIX$sF7tTx+4)ix@`i~`k|T$`U?JuCvDPHa63v{%byW_zZt z`3yk_tcf;KCs})%cHp8Z&nIhKRFs-Q@`(v1smE6}9^Ljf10=i;v*m5I`Z~Jx^tXApjYxH-|yx*ij{MTt_lSI7WNasAq`{8;W<9|ap3a?G$M(h%G$F0=8hdn*r zzprHa$A2`Q2~x4fixu9H%$$#^Aa+^|IBl%>-f+07eyPUTCbWoOWh}GZ3sBB6-P5P$ zQ?@F&cKK&S&q*4E>_manzLt}X@#7Fgzqx)lK}FG)NSy&27W4PXdkj`HQou+~XkdS% z`8vtc=uzzz!XX#9;PLI|NZ}9z00XtV} zR)pvg#8g z)BY&8inG>^=MgEs<2bML-mPQpJb7ni8uxa2B43}1-ECX*lIjAh!L2`FL&?F<*FRl= z55ZTLr86J?0GNcGcJClq^*d8N`Fb9RBVL`74MUw?o^1`$n42f_K-1?)Rv`?g$QJ2g zKVYca;w%E0wjG?r27S`CW55p@s`)@7{6Ct$G9aq1Yx^+LL#KjBmkgye(jfy3ASi-# zH;9DNAxH^Q(p>@~3@Os6lt?#7Nr!;6S&&@e|?{%#!)|q))GK=j77WR{q zQhcOZXxs<<8&mT3u{~~Y1`2@3rLy{~1e*gH?>b~Bmx^;IV@u^!W9LpVTWxOo_t`Lk zc!wS9%7h@4TGhh)(Qfb`zm{+xu4SvuF0U1Wo8pMsedD-~{St)}82f-+`%DDcQ*=qL zEpOHkQ(XjyL2b9UzszSnVs~cT5~*$M!4Q&*s8D0>OZ69nU^>U3p_A0>=CYYvm>LK{ zD1`!P+I<9p|Py)jKv=MZ&CPK9QVf8ju*mSGZ|vXv&7Q=3O;?bD?RrRdrt~(Jj>CEZ@_TZFo-L2 zZV524-!z%o&#MG>B2YPRoAV#sc`kzsU7+TeSYrqV1NJNV&UNA{j~qu^X=f$WNl1L?L!+Wu*=X+NGB8q4)} z8jx4@6BJmR5}LSetfIng4;WDwKz(uoLh5_@7)vY_m(geC z>&79!_gP>WIe*Kz3o_OIe|He3MUf=ORMIYjJDcUai9=hY62JT&(Jqm-X2u4PFZg~< zSfI+_wy@vaeD(8gtvxDun&GIG#l_B_4?#FrY|qF4c~-YJKGqwK>>q(qj^7=%A;*Ol zPjys~*8`khD%?$hJdrQ%p-X^h)5c1FJtbb;W2|lE{^$O2!YeW+gU^HxBQ2O$M56#u z7UqY|!zf{Tv;zAXA*=nGy&vau+!~x|$BMwK>%f~T_AQVH=s-JFQ?f;-;DANIr`wQDC`M^Pu4HH~}XyYhxn!D*K@a z{?YGU!@vPqDDX(tU;`eJQSK&9DE~C9Wa5cEB?2&4;Xj^Fxyk>xA}!vvUo=N(J#6r{ zel!x|(99tfcM{n2?W=me4Y0PmsBR9tOeosWf_)ZTzhkNI$>0;Vpri*DmXH5w^4WaY zc`ZIWd(TEy2S=~5TB&71Hdhu_pm$|XlfNID`!#6qF0+fcP$_wjWjtl(UNw(^{&G3@ zR@5B5H-#wNDu4_dHIXxP9pgS9T?8Hx6PM?>b)SgNT8u~Y@sAdGbo7sd=NjE~|J6b? z)yIt?8+S<)@t*9NBegr5e$raIIvHc?7>xuc#2D)(YGlK)5u`7zrnJyR=Z;1$Epz|tGhkL7W8Ms6qXWNcu{BII(;w6}uP+Ow zKmXg*dcs^oU~+a+i8Bd$8vCA#C5|he@TX=2rA# zL1XEW?vxSfGU0Ly?z1oQ&u>%0pQ<$(Z_+t;(*a0dmL3{=xYeAgbh$JCmzbzgTA=|J0e)SkasQwzdHWAMWG$&29hvWgMH(zB;rs0WfJWJyRfBfVm`S z8+JE{TJ@QpO)3;NM^;yG$E=_NnYJ^JAg_N$?Z-QVk*Bm@Puzd*v^Du2uMuWVf6}63 zus+?Z0p{)$Ui!*WmrNG_=$nvi*L1r9{Tv`9b#`ZPpiFe0_fJD<uID=r&?b)I+tZM#x5^PoLckuOp z%*>u>KZC)ghH>vfGOo?>FThvaml2P!w(X_Yri^N3jvW=K(_hQjLYh*n(P%teO~?X5 zB%OP%=HVOaS2yj{OnSGv8_SZMY_Te0MY~<@$On3;aLjmYnYUlZoDTd_kv|Z4AGC1` z8q}&QaRrQen~0eEhK6KhN|khZ85T?Qiu?!Vq3`W)l=Ud%tZ*d!K_GowdTv1}gz zPd_9Y{C$^P#XjZd7d3*n(Y39h#c%2k=!(QU$S%B-zpo+Bw=+4K9_aLYe7Uxan#YmQ z@3kSR+?%soL@lRW*@)43srNV=@_!L+p?SuwRXLotSkMo_tq^-9yq1qY0KRWFot;)k z!UF3-M;4@5PI8WQ+As01LppTicpCpb$VNZ$p#nEL`;=FPi($yhavxkDlzi~ZiWhs? zWJ?*>wVgU&bX#Yjx=DTc=Nk+BJ!}|~xC)3q@ZXdZ%^+0ot>n$3=+`(m9fN+cqiL)+qjT7Luvm0+3lt=!z#x~Yo zqK;RQr0fivjID%8L)Kpf{ljFn74&kAejA2UiYrGsFFa$C6QzH>YwmzW$|Lq3n!>sE z3fLW%Cl4M|i661zxF`!I{d0S~fXTafcRp15+Ov()9B=V~9sv}dG?(q{PsrKKnNjoN z5BG>5#H4y=I2G-uUE|@!Kg>O6Yne$4>2GD)00=sc2X>tr~UoPTW1LVj@04gUiNyA+`|VO$s%R)&nb+Vv+xC{4VU zaxnd0iCs@c-qML?tnOqhaq9}U^6=#<;SGGqRD$JuYKD=VB=wh-7f91+2?ahWu_7Jv18L9is*I zU(dwd#B|Hx_t<031E}d^rFawAt;!Pp;_}GTH?rC{*25&~`|5pfoXoEMZMts3A-%W8 z_n%BdZF#e#)#om05Fg*(mJPwe7MbXeqh`q@Z$yD3_y$OKFgt1_C`1&iG=KLsxPYdEy1`gYk*G1C5ySyZL!N{X1{(8`1TN5=fq@ zv-1|)ZFLBG{qswM`cY+xCLNG`24*(9fCs8l>oGa04pWJhPiEW^}8lXRv{-?Ot9naPXlpy6ykn-)(nr_8txF4G7n3v zpYfgz+yhRbBm+6E?&EWJr-xi6kP)r%di5QfMa)|dR6Ql=YM&U!-O5!?JDcn#S1GNG zmitU=9{Z60C+ERcqbGT(LT>o1BZ7bjYiOdpf*$RWia2_jbgz!s%P*7i^2+7Xo7xm@ zsn2k36gt}8TSV|LbhJ6drQV}P@@bgEwOe!i;CfyZi8B=FMXp|oJ#r6Xg6V**$p_fF z+V6^54p%R4`V4KV{kJ(e(8^g211}MJx-((%5->fXBKt(DpMo??@AMiiRQEnw>f3DkYCQ45b!th;YcX!IN?7m&h5NMeLbOkI# zUtQ8|ZMDqjxQ2UiaTl!kLc<)S((7yeT-y{?QH5%|(t`7$FDY^akp zV*Mw5BKr5&_&nss5^&KIPv^V3TGOSiDjIR#7wFR;D!>y%hZk#=jizWe{fnZ$E% z{_;H9WxP-ngJfSJ96vhb*_4#0<8$z8PEh;`!^&yj_8Q7+1Ziyrf^^o`^n*m>1^&xC1M^^%tb8qB2y-EaE)$-`axPTr$JFin* z`wd$f7*aTqc3G0Zxkmr@FGKob>Bnx&7|!5h;8&Gdp#ODzQ8p|)=+W^$XaR(9MZ3@p zpv*6M?7E}pU{=SCM1@gLqZ_LF{02?scnHu5=&LzRaS&-hY$;EmT3F$soxQn~a7ZXc z+U1DtMIBI?rK8WPo1<}2BqotAJ71-LY%2LomS%ss<^*?DcUb)7j6r-k>1RXKCJ=01 zh2&X7W2aAf=9+cZ>g)0-gzfhjOGPMSmK2#v-nb|Hi3V!TrnB>$-<|6}^VK&j)AuN+ z-9OBW4W+A*WvYn2^PHbldPX;#I~2(sFEo3c8}~)eR@vonhB%FI@UQ3`lf==)3iO>( zD5L*ick|qeYtdq0MZph0z{{GQPMo+>5A-Sl6j zH60WCi3jD#H&lqCt3BGa5j9spJL3bp1}`BTE5|ZMO8s%MGVzsy5*N2TcD%hc=RR`7udNXM6G|JZ8|a{eb;uK*qepReW$C6yI0dFtu@J*Qq==QGl+Eag%U1M~j*8t$%@PpJwd?H5OCD>!JA9M9o#VD4J z*#kwO(Dza-w_v3YR|HKH4jPj*BrGE)(V=$d`nY1UD94!u*95Xv%Jjake~wKFM_Be7 zh24}jP|m}fI;(EFdp7-yYs1ssgR8G$iWK9P8E4S`i^miuEDqgrX86-Taw+jW?44(L z!VUx-ElnuReC6n7;j+aE#2vONTmQhi(bC&oPRtRk{MSdAG9tyJ&F067J@1H0shrqt zFft80w?LbS0FV@9bs=kx&=_-U$d7+o6n#+g#L)dLH90DLwy|*M;vi;T#6T>aL)0_Zl2Z&hF!bX>?hN6w*fL4rHJlQ9W(m6L0Hg~l%YbNWYI^%v^ z`^xA(5CfX8Ui}%$UTI~Mr4cGf!G*Mve2tcWRhr&m;Gl-d9$Bmm3NYG+xC1g1=IgBC9&c-h<8f!9z3lVIhXeSa`d( z`+(mT09l5EudXRZgR_(F4{Cl0;X>-ntV{ZAURx`jc-#7N1Rdn2`psOt7ycnB4tgj$ zd6!Pc!>uROnZic_2d`UNoe@e~Y2)vK2P$DM$hnu$*r zMSv8JqbogcANwHH^PD{zWPlO-{~ z{KBVaT~{r~Cw0x<9Cs~@+?z$s&b$OQGpS(^S*-01WdD}{IP^FWmsOA5r%mEDs|0p4BJq&D_4xK5lFQmPH3kXGEtaE1 z7Chjf_SH*|j+L3Agk)JyrK)d4WVGP2%H<1U@$lb^zN&6f^)~v)7(wgf(FSwZY{}m3 z+A{tuvs5}`Q|uJr;y`61gBMD!N1N`?*C(-NSSykX+-1e%Q@4F)Q^5Vu8{y8TaRMt# zspt?-41e9peiIyK<$2qx5^&%GokOzK{2ovRi`^RKGIXmdUHGYU_2+?g?`$Hi)H`3BLv1;+TttUB6kM4~E7Qu7pLuLE&_NxOQxr(^s?Y zc$fab@`rrk*=^8p|FGT?gxkYIKWhFc8Yu^K1-}R18Q-&UMYdJGk8vg6sJ8Cbei-4_ ziP!D;;uDeBG9Uq4c;_f1>9$kVzLp$9hZga6c8Z2^23zSLjKYJrp}++(+AS}mhtOk| zeH&eSCbWo1vD^UXI~=tUzt8YAd{NS%f;RU4X7a}@*~B#S!TK{$pyU`CDrBCA+f(lI~c z-P}mkTawMVK@X$D1oWYBxO7PU-SV+WPe!d1IZ3c!&?&I2f6WM^!`8bo&$XwFrn=mA z-T+7)9DtN`i1HBc?ns=9Luk<;d9-hJ4mUn;&SB*hdg4`TL1MD_pw0{m2W_vgA z1Bco5$KzDQ*LHC5+ki+RW$#MZ!?l$#6;3SQiTko61WJL zSAdF3e7_=eI{vi9^*h8NN8Ax{YQu-2IWGjm7#L1y7d``q*Eqxhee5nFMP^F8?O(Rl z>L*98&_4$hwCA66n{3>X!{kt*TKw`C49J*;li_pRrnL~RrctFBy%SUbO}Vqtv+@;x z?U`*Hu&I(iKK}(;I|Et{JHoY?Pa`ePArq@XQzCy{fD3rIKmZk=ah5Zq`i~nv~y>q z_+g_(J~4Mt2z7$3N9oW|yxaaXcxA(Db9{(zAQq6`_8U${zsJC`qnb+ko1q{yoxKcV z(HKo7dEx~$586>WuyCt>BSO^=v=^lQ4ZS}m7hheSyO)!9#6<9|mQhZWDi~Q+2}IvX zV^Gr(yXAQ2kALMtdKp<17@&SXxfz4Vp65G=bt(lU7ueMpOZ<@-;o zsfQ7UNmfR3EAjPFg|^1fgZ9W9Y#N4?Xx)Y##w}4gV(Qq4j1B#}*;Y`S#H?DA*t;90 zwq-MH8wCy8db?BD&wyga24t)W8C4i)wJgvF#kbU`MFr1CzSvyWuDc6VfjJ z^!@Xi2rVrCvD{Xufk#nAl-0-8lxk*Z#bE6x@?& zTwM#+zvC0`DT+jiJgW`&KAKT8$bu%0xvH$w3CR*@U@&qLx zzHhYtcU{1&4311F-QE$Ce808iw0%5exm2nD*l?W`H5FhKSM9492}5!otEn z5)Mz|TCe^pM2W@Ws+38F0deznqwBVxnHL+XE#29VXF`9b`F<$FTd|q>>TYM2ZcfQRO5ky+?Nc=arlL* zh}9ohF@4I!*>yEJ>7(i%?U%dKqISb02^hZRObd0tlU4H8q>t+ph8E{LbF8d#C5JCx z^#|cYIfUxSGr5PrY$!;*LFlMDiYGVvjOF-_ZKw2jJR+mJ1zG#-+Fr?EyKJ|Dcg_S{ zPXFvu!_GYT^^$}em_X^(3-gJfN(c{nv2h?5h3fEYZiXq)>uPi4J+Q6)&#aTz>AyEUUVEEzT4bKb{KzN1D55&n%ULl$uzkrM^iZPe zNuFjHxcsRv5)0NF@z{y&xFARnO>9F_?#9UbmL=RciEW6{vd~UdrPBL^qU7x=4ip&p z?>@MseC@5tb+dtR&yIPHsY%_6zsGWOZgPeBCpA!raZ`8fh?B=MpiVya9K#KE0d&oev3!nqnM|ZfTWF3kixByyQOoip_Wy%t~}$9`ERy zzh^VTf|yLb`7p{RTto(YeN@H#=W5EIuxnuOu+=rfi&; zug1Z3xb_)OuR@v63}=@NZ#+G2{d6ru!kdcV3cK@W-NBv#%?T5FD}`9MMr!WL4ID8x zYBSdLqkX=2>3$TBa4ls@V@8eK_hWqMw@^+ma}BIZN3m@Dhu{dE9Iei-RYLpEV~+sP zpgdm|VAQ=$Si1Nt+Bm=>Q?VAX64)9OAxoboB>y0gwt)>zi>q6i*UcDn93bH4n*Eh- zaSYxU=ODkqt}^UC(BZ8s4~)jkTXvs%t_%G=C4)otXRG}i2P^#K&?&Js$|7c*t}!u^ z?87({W@Ae2GnIQTol+aTi^^cl!O|Y+8sb*O8T|0%OZAkf+H&X6CaR zMBF|h6ncvjx9wj#QNZubDp70q`grh3eg!Ywd-HPfSNX@|>cI#skLAWnbv9=PEC^e3 z;+c_I!rbxml$a1kOXR*W%ZF^ZJS+7JKK{hWCkDGRRBBjRnRbMMOtTNL?KH4^N8BPm z<J=~$tfF8SlQ1*%WQDSD^Cv%U-O)P~_nxf*Rx|a0TIj`Mmx>ml zf!2obx@;gW+C1y$7YDEUY!T+~7Bb%kgOOC$IWgw*HTgG)q0!gI72HG#4_^I!6!`EN z3ypS^x5>b6syohm+slu_ap6{&y(XJSVw1=5pK$Bee{X_?Qk?ZI5mcOkVu^Vg#*r;6 z=+7uLJ@6>4NYF&#k(=&J`L0>o1?{4xmdEV||E)nO z#Nw(bwruwO8FbND;zythE1qbkjTt%GVm024T00V^7x2h^0D>Y_;;$Qezp3bZP7TU> zc7?@@p%p)^r#)wJU790=gclSWD8y%bzf4>baIh`n`Tw>$3`3tG#;G5(xLA&5Z2X0* z4JYpOffNI}TD^aT&~B??Ke*EZAl$-Fx})b}BO8gzO|n?PsNo~^qZtw|I{&jY3Ml*z zKz@Wq$PTA1kHl#uojk#HIQ)GyO!zik`+*Ys-X4fpYTc-I0}n3rr##Z| z1OjNzkD^jog9cKoJSy>j(?C%&=|DG#@Q3i${BbNiT)f_^=Jp*oBXy? z0tr(ZJoTBu&bvwCk+(Z#0B%fK8oHI(Gc(ekn!?!ri%}Y!6ynXggTxgqb|0ya%mR|E zz>%~+3mCD59}sxvIA4f!^ueMkZHCkUfkD`>G<*8m&D~U7C$9LU*znhchEVL-3(6Xv zHw0Z~%&|4*fJnJaA$;*MejT@zlyz3E2O*p)4oZOHmuB6P7G3ElQSo|}VJ$^8dJe;< z-xq;H)Pq{ojwb_{_GqZAJ6<^s<~R@I!@u3eavu#>(-9GeAi3cib>(fPrkt>gAdI(4 zywyaW_T2vaRg>%_vB0&xYc!61h;>lM#@P$C0n`8}URB}`mq;PFTBZ*(`N&#&1VsET zWa;a4Nc>khCcDsmQNMU0yWjoO>$nR@YBM(Y+bNt2_rM)}->(00NiEBIpu}IJKaCW(WY{C*iAUw5VBY|q3{JGvH!(ez+{HP4 zT{A`ppT;y&iT!lgD;m1%+EZSUQs6jtp02lK!mmz zSP16MhV<+~OX_F~pWTpn!mU$p8~elYx3a^8D>%#Q5AS{h-b{4vV~h#;()zj)^+llh zbNUnNcHRpWlcn@GyhPg^e$D^9KO91ZsQ1GY8?ST7e7ehipWJwC2HxiIjTqr!>YO_* zfDsG&J6mRMthP!K=R?=G;Bt{WrU)2XyHO)~o)yDUo%;1TqGbt+3Lh+c)rkm;OHURq zbKv=fv^YKr#@7$5Vis$=BqLcTO6^qHEW9$1&Vfa2c{F===QBZ9Q&+N0-!K9Dr&~b< z)PF~?yo2+UMdFlIEnCyqE}PX{hckZruf-gBY*vyPFc{qY1yTvf1P+M3r-XF#{e?_= z6U&%UFq_vm7DA6$w!btyz<>2HkdCR11latOtc1)2LevE~ldT@%DHA2we_R?CIwmP_ zcTqlc*Sv0J)AkPdXM$z_Gy9Y1+QkbgOs1|37+nf0e|tyVjFt)PDEPEaFDS@B5N&O? zB7`CZEXHLc@8#X*@mmL=jo65Cygh6*@wM617O<}paw0s`f+Hzh1oeK(NN#feKHpE=V*;reWQse>KQKh$ zb&48r9pHrA;u)p@FMECN?fQoJv!CaNLe~m^rv4R58~Bo87@+DqU^+%<=RKh-kcT5q zF=q_P8Mrqjx+H%vn}OnpE|oOC(Wt8{7>IQMP8tm4%LayD1{dlNcD^g*>02FgHX$vp zIJ!F;hl_60jW0U)w4f*(1Ewn*yEJD}uID2|1Ch}}h(8j?wxev3UOLu#L=c(a8do_e z)2#ga_TBy{@;XLqqao~+*Dlcgxb6)HG$)sl^b_IzReA4Dg-R-O+Ffx@9}Zi5BhKbX zKRSg+-p1aKR=q%%ZVc{@bE(u$#4OgDQ-I; zVaTO2fuNcI$4qYM+PKAdtD5rJEw8cb`XEsV^B2G|qSYFWtwi)+dWc7fsl?pt zuQY=Z965aWB*Lsf1N2jcOoiN_g-Iu)9e}3J$Ais>%SQJHVSZK&EeV@^G4k|~EE1m$ z3ko(#e7)l?CM=7>qg zIM|gsq`zpL1bzlb>mv1&_$NN~-<|Gkid~!S9+?8<2>$=Pt#*N5fP`~*Sv2=eFpGw} z#o_D3Ho{}ZuPWu_nZGFF&V^8)ckjR$@KXwtF4=Od9d|%KRZ~zUGVND--fU`jb`C=O zP!8E__k{^40}$9hnypF!WKQSOBKmrPIH%<&lvrWv)V2|cRhlL?m`E#=Jf^vGK7k2^ zhsGbiP1VvC)V=8~M=lKI;By$QCE(e$5no^>eBa-*-kwlZ{Glnfnt={fUq&4F?0y6a znLGlYkp5qeycswxep`=66YZYp>g9xGpHmf$hM{0pQ0mprPT?)<*;dsJv5ysffjEzu z89G7a6PO!=N5vzX+2^0$CO~6H>c4O*LMFd!qfhuj&VRWrlN_%9N+Iw~<$Y_ZbV(XS zUy+0!7ch?LX;0fL&7UgR-o0@VN4Y`B0aNVW?P3%uo_obGtuAH^T4w6X$C=KuN7!nW zroqvf3BhGNLkCw`C=$D2s$7hwn|&kpta3iJ`*WAvf+%2t`MRh^qs6WgFY0a% zT&u31bhm&|FNY>Jo7Lng{HrwkC^pb)!^Z}JhCL#Db~mSD>k~$eX@TM@^L!#Enjzvs z8#l}~s+Sh}-j5gjb@1M;mg2aL!P6VBrhy0FLG|F$N&C4UUpWP)_8%g}lsn}k@ctSs_=*oL9PSD033RF7vd4A}i;kUE? z@68k0Lr1Aw=hd%bE}a%=TZ)pjw6W?|C%1PgR7;_t0*Zg3f3?tW<68CjI*iXj|J1BU zmG+Y&QgQj?6`~@tiKCcw#fZb4boF8P^VWI4JMcX7s89d`(yNACu;eZsjrr(R_wg8`y`*%@6xxZ={xKz~ zvb+M(RM0p-zo*ro!yJ%{(hA1FJA6sqz@5H%0d|7uhvbj_`D6~MgKcs+>Tv7mYT#rSx&ul*sjyPXo*GuM~oEBC{v z2haZmAT#tVAX$jZ(=X>_iNHsmD~dE)oz1h~=^p_j7x`>@tJyVi^eSir1qmg|c;LVF zZz#!;XR@H^u^?A7(lyDnc2>``TVk}6H~uL?7c`VHYVm9zk}k;-gJrdm_trFU;7G+5 z0E}DJb3+;SG401DW7zO*PEfWLBx=5eJ+CVi&!?X8JTUhBnJaTDMuRB*Q!mO)Bsf1Y zeOnA>X7L&q`f5t)i4$+BuXBt=hA+=bT7_7m)4A6m8t^TVO^AjPaUi^TjZenCrJN?)mwe_vI`F6)@~zV{-P# zsDD;$c4ziI0>iqizdUqkOlf|qj<{cA1WYK4_vWG0(1=WyiW?Dd0+c6DJs6y@K=gU@ z>V9<@cgNAIn0#ILpeBdrFpGSvT?~xpGVR;<6XsBpwgopyV)$UUsJa=?t=aT|Hn3a@ zN<@coNCZUQ=mjVkv2-5Aib4bTFL=<;*HRg~^)Fd;Z|zHr zA$diAq!JDr>Hs^ldoF}vNj}-N>gQK8;K?eC7IjgD)Ln2zk!j8b$=q03rgFmYGGX=} zNBZ64TgI!nI|@GTQO0DfpU4A?8NI7EYFK5Z+}Y&4voVFd?1^gS*3T|0x)Ufs-gY@% zgcw}!>;&iTq|>RrlK~1#7;^xhQ!SX5k|C2$Pmp9y86*nwU4V6DM28(@3Uk>o;#Hb4 zePuO-2ZgTF{pLW8JPl@%=p7eYl7t~~Rzl|xxHKMBKZTfDABiTdy7&3HupV%Uw9o6A zellC4{)2mE1r%yont~``m;ZeNFsW47KlGRM{d4`}N2cejesU$xSUhL*4)R>@4f@bg#t+@3lK1WhWct zF4v$t`Cyv8jR-nrr3>(VxMcMGO%VP(J7&#O&&i~09?(z;!r42XNj|w5Q~>iGQq&X> zohdUEdTUv6fQcbGjUsi{J4{T%viOH_uYLqOfcQfI!kyP_pMyuYJ zyags>81diR8T_j6xyS7I9r2aom*cppQ+$16geRA`!g|$)(6lVTLXT9iAcsWDTa;>d zdIZ|el$YLR55A%$ z-#dDt(0=2%GD!XcO;+@l+VngRn9A3JL&;U1{WoYpbS}u$87MXev8HnY0l;aWZ8hbg zL$NR}{G%0@5{nQmRDI{zMbv7<1RN9pUyHvw2G7JBkeRGQ%_&zb?fg0FXX4i;5tHr` zR(;`UcI3m~N`FOxu{=OJ;w2C&s1Eb(&(uBH(U%*v!P@_toySMBb|$j45uptuKDZ;2 zY^42eyV(A}>2Y`9Cs*?K;;NPUK{CzNEPD?RV4w%tIYX~-uj8a$wBYzu?kC4O?>Ia< z)@#3Xg)D)i_)Pn&x#=TDT0+*NmriGo6N>Po9bbFKO+crV@a0LLAHmMUL?Z2L-f{F_ z81MJwc3n)XAAh5pRr~I~^hz@jt#p2;R`@+|)dat;b7|SUTz?Hw=cTL!dDO8Yv?Jn7 z-U=i9o z1ib+8j|xOZqCk@ua)BC;Es^38n&@nHYH~V0DgP5BpZ4|35=a5y`q%Jj(HVqb;@5!O z+$0gGk^SA&!AwkO`6&c-Y*CH7wE!%oES-;0AwTEc^0&Kt0HHn(927X7GAwU_U&;BH zGRRVYJi~8Imh9CzL5Fs+V6$=yl?uxX%^Q>pC))?PC)eOAz+|zmLOL1WtxFwb;NG{T zGr+q?w)haExIu;JXC5-!3&vXCLH;)Ra#$QZRIL{7L^i2*`NOZrCIRO!w`2K0-Y&yI zQLcbNZ{2$Pc=j6=ZO%Q6uniI(h1`dOb4BcZ)dahWS7*Bm58au&Bx+WPI7Ic*&DlaQ zL{zz=>Tbj}E099FCzOTGdp5GB9%AXrPDnq6oewsXB}TT*RB$E9Gq_&;%gmDeMHnbE ze15H$HYVW8AR(RjJq-)mdo6@2xqtpB%bj8#rFVOnP*8#cF7p&+egl`dYO@X#7X1G4Ay&bhkE zS5<+IPB-VMZ~6NhS!|%JC{|M0b7seU_xIuRh&j3?faj3HrT(0&G=HEAgK=f{bbt|U zH%0j^?i7gbH{fm)v@E6JJq9sKZxMGcF2xfRzdOTlxE#+Dme?8yK8n7n_sqbgF)CXD zS@-1_pvdw7N9~tkpuPF=RRx%51`%Vwa-t(1B-Vi0GQEqSRo0exyHYu!1sPVHhMLcjX+;&-|AR`W zyf7pjEgNlWSC^U;20sJQMa%vd4hckR6XD9Fn+%1W8<}dPvr~hf440VKLIS=t5sv8TmA*^WC zPXPd}Mb44E{b}VFk>XEN0Y2mK0yx5%x(evZ;9EI(p0(_l4|@dGoT>W3PaBa*zyj>>+I(6WMUjDKd|~#QTxg3S~Z5x5U%+r!GK+tp~~H zzShHmvuuXJu{- zEgSH{mOij8K4mDP%ppODfv9?qK#0kdctnk@BT-q}*slJ4a^QQ(--KI{R3G#IxL!Bgt|e7!0+X07)UF-FYK{X&0@k7~8*fk2VCI); ziSn$gzsdGMKwNEi2O}iA&M3C58DggnsUq93Ogk>_OW{0;ufW@IjI8~exy1<^lA5Cp z9tJzNNJX=nq(9mHxiZ)OCHFC|f6he3J6Mi@kxPuu8a_MqeFITpZf6}!IZcq%CaoH_Nt zWL6|jrp@<8ivnW$Ffb`3dd2|KQxlRmuwaR_B{D)*U6i)=*k#G2?1H~?>i-vN4l0o8 z5xe>Iody_M5=O<(*B@E_i6;~%@{lpid$891p(PL1ApN4aPD0WeE#=Hv*9q)_?V6Ez zVC17$|1#$W*2hC?2ahIuCmRJe)^>Fvh-Xc)0VZkLm^(x~&Wjp}VEV9S5ed7SGa+%z z5b&C$Ur&_o{e$T80}vtsmJ33gI3R^#SRuX2 z3;oxAbs4?)2=~#Ry4@`516jFlMmUF3BG&TfjCy!BTk-I+pdDmpHQ)Jr<7#Qbz~L!< zHk{GWsZ=pw4-LPapm>Goz4wvcpg~+ji^IgngX5I9+)h@^kzd9mMoU|+ObMS&Dq_bp z1(Rw8lWL;)l%m|KqDmQxR!3svmq#U}RXYx^yMl}co$QA@w=(bOB&^!VH3CZ(H(8+T zuX8lr?VSO(9{L@9J$4Sam6lbUdayl)eaLXG@8~Fy6RqKQNv#j&6w*=AT#Uskv+D^< zHtKhzqGl)cnbrbjJ={^xyTvU9#~0o<8$sdpI`LPff8arqu(G%@)U?l|Jp=WJu&-_) zB^AmwHNektR*N50z-h_1HNrr&*$h-P_|*@RolBtt2e0*}))+n%gHcV^c?oijit=Zd ziy@h6FoUi8zh61_d??P~RL{BhGG;KN`f`NWF6gW<|H}GgEO7SkJE; zvrH5d3b5Oefab@eM$ML%C-)rY`H7y;U&)M9i`$6Ocjb1Km|yvh)A37Sa+m#Xd-3-v z!|c)*w!{#R$`j+&jL^6g$A5T+y@3K#rogzw+Gw6<90e{C{ma1 z$;%0kaob_(JTrhHWn8H?m3W6x7$4s59p%AA(K{%GvxnoN#CFEjCCCYXd+mpNZ19DR zysweJxv9d_od(HbWUYKfzeg-IvUyqLH{ukl%tb`>4d8tmR|JRgy~M+~+T^-t*cRk8 zYxq4k^Ck)4;<8ssxDSQXM$4?qa}n?; z-L&&J{1&(;xL7Z?_AAL>Y2tRI>@X`v^(iXF&$A380d&FItlZxyU%Gf_XKBz9hKUf{ zX)wqLZ$yQbINHZAAVvHikz&{B>nVO3SS2c5uG-FDsH~gI&fAU&++1O}e`NO5bnB9U zAsKv6nQ^bmOv_KzCsd`ssAcF)mMuvhMhJ43%TcXp^lJ*ge zRBP2vj;C1-(>o{U23)F|XM%|+6qo^U`E|75gZ@KVF!1ueX~s0w?;KV4Hv!txRZjvb zHlBUpCmlgBd&QB1*tls2m^3OEb516oC~Ey%i`&L0ODLlFO?JEx`2}?@TOz8$>4OmW zzx;}?((LFLbs~c<*h%o=Y9Gm^RXS3LJnwJUFMRe~?T|Q{Z(xcm>BszufijfX)m=S| zwN+7q0z1B`N$*{UTt7MbnCqQwERJt!Eyvcpr41razM>rI z^-5tc2jpWNU%=Yz|2;>{riGr9eJwiAw~*rl$=P64y-KQRB+b`3h_Gi~hBSJ04=;p0 z=&0)8Asm-5G6w)Wmcny!1vC5hChHTm+w+uJ)ayv zTN}*^bTP8=r@;=xKOPPB_0db2&Yc*s=Go|IT?iHM?++qnR&m@O&NHs5qt1pfg67%t zJ#~|GA2}gJqAivb7ASF4u=ZBbw~OGhoBqG9-U6zMwtF8wbW4MRlprM%O4p&gOOOVo zyQDcZh%^X-z(GooMq0Y1I|UW#u0z9jv9_Uygy9oNNuHJ-qh zqxle0oGPlyN!7P38WP9Vh7g*^$WE;iybxXf>B1$M?nK`jg@Z_ z4w|$Z7=KSpV2(h8MVwW$jiawwX6J6{GLparF9z(_sLko#ao2VV04?Y7IO5z7&KE^{ z{m4=oUsjezt`bEi9rV)m-OW!?T}t%uKHf1=Xt}AF@K+j-TjfblI|dsuW#(o5!0)0M zD9-Y1*@JZGK=PzAlBVlS(TI0gk4@D>;&0%SpXOPC?4W!_{q-d=5^^`{{GJ;}In(Em zU6h&3M_n3GPS47ltb<%qQ}8SB&;q*L7-(^CKK_ggi|AVS_V2$NvyOP{2>G6rD-HZQ zabyMC?SS&~540J#+`kCG@TtH4-{(@gI$iV^|AknB!@x(JPjNGnsjfY|H-?W^3+N2j zyLM8n+#s zc}od`?q}y;O?Tx58uoJTf#D(?3s3Yfam~|V5)~5?c8(1j7|F~-6biGmp`kOV0m6#vnR@LpR9t7(4wghKN$QA*?L;cu%@An)Z$w|ykk@H0% zVg|?5f)=WlSSsjOldA`oiRr0gPHoXTvsHTE)S#tAE|GoLkOUgQ$;2U#F zBvPbTokaNz*P)>a{8F8JXUb~raxLQ1<^Z)}UM!FBJ3X*KFo3GElT2ISQ)tk^`pW0+ z!Rp;D26n{MHYu|8t4@HzDG*&6dT2pLf5~2m-5Hf6g0tx$+0Cjf*d* zJ8L7Y$Z?Nw=ofjx@S<@&FVdj7yZ_w`19LmNIX8O}i9IdLQiy2|* zf#z&+L5df(I)F%oz_8N)_p7DbIBtD z3#;@tHT2oW!LINbvI;m4fPdHAo>hiH0<4byK1BOs<_T=u7+R<$n&|T-;@{=si z)Ab5j38$rzNFxP{D6LC$1j*-SQGQRDB@u*Uzq85A^1p{)ydE();yJswIOTs>zT1%{ ztdwVBhzxo1>Xd~dSNJad^O-Q^H4U0TKo|^LuOg{DR#!?t%S+|@##I))0Dvw}-N0=w ze)ljPmpV@Qb5esC+aj$8~~K#gUV0)-_F74DvmrR3q-vf{ZB*B zt%BOSL~+e2>QS@=UsEa75xJ=@fQYJ0bIzY0mTP=FBav#B<#Y~WG;cXid(Fh$U&mFa z;^Y+bZbsJSpp(drENM`Xemv5L&P^gFX|K0)&QT?V3d!UI1QKlnJ^6d8*n@Flx@2poSsdmwJ~2OX*atC!LP6y7nZ}R_*DYXA1|)F*&X8LPf0_O@&_vQP zctedRL2ouZj}WT;ixf`HZs>OZjg~zGC29>(z!1y;hzaGLVtIpQKVoY4Ds(lIc&xA5%BMXHTWd zSj>5afZ#-oZ+#O5PsL$r2S41HkpD>xXpkr-=bQTsZxeEp`yhAymAHlXM*)_Z>D15d z1Ri8ar}$p_)LF1~r?fzA*Vo+D%MIMj!UQxudIBgRm6+63h&kfNTZM#i(0`kvVXlsr z0LE#k(@zF-q8@DJdaiTWMJIBAxn102&Vw;k$;#a~sPUU$mK$|mau!x(JlTm8gC>fY zR*WjCpg%N#+cn!Fr{~~&B6c|%6r>%Gx8?#bA2vDeGevgk5c?I(BC06@h5&4ryFL@G z7_-8|Gm#)$?EJJ~_Q93%pY-GLFz-i6S`e0doc6 zdfPP?+aNzf%Ki3Q#ZohIJFYGg318n%*CZ}DXm#{-uO6}jN@U_pf6&cX#}2_x0kV}n zjOiynv=qhfDfu)Hlz!r>nuIbdiexLaHi}31IZ{7)$@`7w)fmF3(zUICOIm9A+o@@;k2R*ytFf}+J)9-sP}PD@t6S^SBdW@k7&E2Z5oqWe zMpMGg%mq)xB9DLlQR3G!pcqd`GrudtgpY0XekB2+N~<@h0MZtmK4Khz*Z7iO+~oxh zGXc?o?%O_>6(GH`{rUq!!?b?MaU%GGV^`mtHH;7(oXRQ@eM(FdfwI+H=6Kng- z`bs@os*Fdu@E4leyD~xx@D#u$7is^{B?)Bk`}tZjkM4NC6+8KK?+%g+W#C@w@$Z9J z;gD5k;Vva3wVU;FPeq#{YSH5=Tl_Zr|;9+2DWQK`%&*v zlcp(miqMMKXb(7k{T|H>^(dUp7d#i0o2pE5$s}@LpluB|x zNbXA_^bOyu&k&f)hUhI`B1P2rTG7*a==(G)Us@K|Qr;Hat5F}y6`eI?Nn;hX!P5SX={5>2al1VQ&6N7mWigJFVG87?hNS(|| zM8Y6rrfdtcIlJt6t(m_&*{7=cj05vgvZC=Y;dng6M8xg+hnVk~rKVW)*<5!nDz)Nz z;c1(KFzfrGA6{ZqB6Cc27FqC@vH1+dQbp80sA8ygO=8O(zwOG>K3Qw!nr|sB8Uvmu zO>AaNqvV*$;StCycWS6Dj(Bde{4uaUe6W^!uT&$IYh&jQ!H@(BO1aALmrVy;71`az z{a$JLmv8XN^IleTmAVV}k~hk`D73K@9eUa%_@8XmWm_5!%v^nj_K1)_u40jwFsXQ^ zoYp|zAe#=q#Y~4M_X7Br{SQcxwALwtZ7<(~ttX7%;atAVK!YmwDqiZ0z%N4|YL&({ z_tefXF)tA-Z#4udoL6;aL#%{bjDCO?Gu`wNOxC3E1k7fXElaev?W5wUVx$NW%X#-W zAA2u<8`SSrh|TEZv}a_H)Q&q~&TE8`+QbOr!()?ZJcifoThFNqn~&+%ySoK1%>K-c zpzXD7oAe1%W1^Iyxz>T|A~eD zA#{8e)ez6mfp!KLV*3otpIJ0-ijom0j45W#SI!84c4LpgAfRq?IL4Go5j*`LX(W54NYTry;P$I6x~%!GS9b2|#;)zlUTyQu zs7z_)NY~tKV1=JO_0HnfX(AXlD(pzrq9iWKViMx6E06gYG&gDsf9lQn@az*C!f4LB z$u(gk=%tzfl&A1Z)@z~r7w{$MrpOqZyc>O`B#;t z>z28m&tAfPK4-NknwD8Ur8dN|A6v$_zUUQL+dE$s!>HlQwH0~Ny6V>`Lnc#+gn|rF zEI0Y;-{^sx#?onXaeU+HSUp9QDn-#d_NT^^c;apIgO-<`zjw2bO!v2)?V zE4B^sW*!f7)SV|;AZUFwQaa51&Zsx}u;({7I%DTh70eAC>Xu4le`K);UL-F;b}ZQE ziJ3%9QQTH6(lftt5WWTDDJkpdaZQI<vf%`oIk56s{~G_MSa%M`t&<^G7+m~(b9fo* zLcNDUs8oP);d|Q7_xTsDD1kS+iyX{Bgga(&@BT#Kw)$^BED_(=ogBde)EV`R7d9tn7EK$GY<(FG~o?4mz0F> z*%@ykb_y(!xd|?JpkpGjmfK#%nOVZFESRHBORmjtvz<28O(!y!$W6*v2@JAQWN9Ot z@))y!naUFhG*bErlCedD(HVTvRIT*x=GS9B`8+s#I6;Z3@x*-ciEbX$OY8YKvFWg} zZF(#20wek%xInN2kqtzV*nqjvIWTwdW|7>~RO3nW=M(K-e9tQOH*PH)u{0gm>Ktz^ z1AIIW`LeTb-XJ{pnFovE!s~;i8lxwj9grfif_%=|YlD^_jckny`X%X%?nG6E_=rK7 z*2_KpAd`_oVmh!3cC}&-ETN~hXbXM0xkO7cfS{U-y81BiO_aj~GKnA!@&^Biaw!xRpU+Q8T3bzN% zyNPcD1cUc9@O5zL4ax7J+IBqPUUO8IM5%O{p%8CDNX`a0YSGH`rP!HoD5){EvFjdo zAU3|P$-+F)@;pg`KJ*eDpN<`ACN>0VoGtY+PgE3v0O5OXvG+l`2QiXDXTFt)FAj^q zn=jKrSHsy3?=F6S3h$F0x9-7y7qYLuf8{pGj2}+G%Nsn48-qpJ1^s=Fq1;n7gWtUA zk7z-&6%TY4nKaF6MZiOw0$*01riez_;k=ij1&F4^U02QQ$*w(MzE?A#3ND5}UcS!>yfrgN=pWKGbvr}j`FUt9#Yt6MK_r+(z0QQz2$MS~R%eZOJ*BL7X_u0ly z-9Ho~^r7^ZM6dvP$v?zeA1T-KBWQgabkB=g%9aWEyq-p8)oSBGA=M=N4H94zYv#=)6lu;7f&R{0uH`7#I4?Lx9s*W z^d6>v>~7YVU9QRP3wWIH2Z`ajQe`j0@ivr!aLVr_ZrwYSlL%6r!h=otCn20?KnjLl zwJJd}?l)ee#+Y?^Qod>dij%`$(5ZeJ1Xi&X>k|G*z*@lZ)lZ|V=0LyXlZDgp>%gn5 z5?X~@BYYp_ZM+GEL(cf9jXsEf_Gwa}(QpI3zx z9_(L+6)!)wOsmZAMY-)fK#aN~rf0pr5x?2%P(TdlWp&-I2+Rk`+Gkd6?Wre%lo3S1 z(*39R-X`WrYv@dx*s#)&B<3!Ug+xLV{GuI!+IAF`+yN2#smHvG7ye;|MG()L+P|_b z@b-s0YF@cpy?+yaNnJZrj%w&&oHyK+g0=j_+cIv0E)_m{Wl{LSKJG@@5`i&=GAM)b zFy!5?mM^b)ylT==WdN$c;%!9W?Lq({!rVfJ>>Xh-8r8?;s-M(tc0`ps_`a9V(;``t zS4<>`h!fJjW4}Im{ilS&n>cTDaz~d;Ey;>=npYrpM^Ku z-8jTxGq@Jd`N_Cirn4mI*vtrBD-MZg_H>^kFpxeIi@c)2#-@)0(-3{S&y%{RT1ax) zVZf&80}3TyM#4`l%&?xZa8nc-RPmp8%AcQeubkT!?{VH3#NUp!{mm!7aN2JEmrh;0 zfxfa!*#t7I1ML=I2q}`fd!BAYuz^7jDemTWS5F+~-{d_piqyZgzUAqVt(q=%YPG#P zH)BzIlTmXLg^{(dx1Jc&#+>1QQ{Rn3c&MqfhXyIvx!6{vZL2D0GiB(MIGyVb`hWoV z>bjIIP*3I~L;RgwH+D+vo zf@vSR!<1H@IslBm#v0eFIgA9hYkAA~^2hj8uJ@+>_)W&+z-0Z>UH9UA*n_?F-wEM^ z`sra`v%&|Dk$6H{KiIQxrR3#2O?8uV$|JGA9ys>p+;CYwQBj@hU9`NotVTiQm1>_} zgCW1JvV}pVPzhoa-BAJ0GxYFBUSkl3{4+vY>Kd|~xN*gdEu5+EnfP{lFYXB2GCwl7 zb)#|ewOH_zkxU+BGf*@kTX8o{NzZ32O`oU-qjc{4*`A2NfALU}-* zRBugc*<}f6NI>Vuj%K2rWM}x$E?TzD^F9LLga|X12?!%#(ZBMKxWK+BT_r~(F5vBA z&LR#0)>6oN4z1;-If;2`){f%VB5fNgEz!+@Gc=^z6SCPwq34pRhHa|>%7bF#?hIx( z!Z9MMv?@#=`%rYrp}#z{1b_YpoMj_xh~zZ}8!EXIm+Qa9|-mn%>ciYe7ck-rJ0z?5(|)3ny1(yh?SLYq9amXmj}k=_rINC=O*>ma+GTH5d@ervxSH|PJ} zQ8TeFkTgH58l^FEb|M$FAUA0*4P2SM(#K>(jq1SC>S8=O@AHFDgbnpT2;P57B#I7f z&Kbpe^yd^!FWsv_$BzyZ8Le9k^`lTtfgSyuCO zBth6yjLoawmdY|_Lri(NmBWtxb z&^zH_q{&WOm96{YLVk_#h>XWmkDf3|+eMJ>`3p!ovcna28emvJ+UDgM{y@}l{4q-#uQ(Hhqxyss(7go{Te0m!Na9b0z!ClZg0 zn&DNTA5ZEl^bP{Asj-+)ndKLYH~nCBXDrqp*ZG=!BM+G>ilvd@>;OzAu8PHfp@87Y z57%?rGnv!od#G&ynS*<*uPB`s*-F&d#E%1%zn@D0u2>0qv1LLTWCAwCMZc*eD#)Qd zQR^4e0E2k6+>b0JiGmtjmm)5y1i2oo&l>WwiE7YCZBM}t6Zvg4gqHITt9``sPYmsvlqfa<3r%;< zgU&Zfu{gLG$znO*ECT>!1~sYf63$!sf0Cle5Gz2GBW8Xlo^T6CuTx}@-AvS^y<(RQ zx!hed8HpHbcfGkjd`2^a-9~cw z6-Kf6+o}fpeiOM+8&bJSHHF^~6G@TdBW4(a{BZ2$q_;WW+`+*ipa- zdmLI(j7jawUQk3o{hAEke=52M2Xm)HC@+t^qYL<16Zv2|Hoeiuf=&nkK zn^C^tRhGP6u7MK(65j&4rt4SYxpd=kWGSl3W742`N@3EAn`h%wQA{N-l2U->JeyLx zNg#IvMvejr71D`h1tBEFmQ^DAnqK8k4kZ4_r+xdW|LGXwD_War0OT`G3$%Rr0U1oY zqWWnxo}xHt>8EheAz)J)Lf^EJx{oDBMX10!`Imc03(gzJ(BjNR5@PZPA7wXB3L#W; zY$%GyBCk?nVyGr}sCM5eA%COAz8!x&?)#CI%~;KTxgqxT0L_d{HRyr>ZD~(@I_4Ta zj#fN=c+n_Ypo^jXX&v)3emb}Kos>`9OB0m)8pix!6%fwtaKGtHm4XmHl-HLiQw9nU z)L7tv-7C7o+Px$m+_gQQ1EZBxGU*2g3%U763fhQ*1WT*@7UbwM&}=IHp;tF+zBcz& zH5P-w8Y?AnC>sA8yQsux_$YP?VOe3v($o@`c50-53h1a7o3D=@^XatvCD;+5?09Nt zGu8zE9Mc@L=5I6R^&!#C&J`~rzqY>GK3 zjB9DuuOESg3JX0F#cUp(4&JwDEv`>;Wb1OpAYcX>IyIQZq-J3FafjXjn66RZ@<3Nl zIJL^vOLB|J9t1XH8CGnR%w^CZymioxS;x5)gGMJ&DDO}R^Yn1+}qCOr~nDCrCRWmR#y2HwTW z{6t_8xQBiP?r$_~10Vs2vYDmk#P$16h91m?fcRpdUbF4xl^?hC_nwq0*FgxSDl3l#2>*&}B>7L95q5(Yrf#J`rICG)2=$Xz3uc1MZ!-~K zI}yU6d>P~j#`xGE)bzQha(5(*;#F(-I3nf7IH)Vvpz4|qv_NfL@Ye`}9W7LMT;5jg z2?Qq7pN>?p@dSy0#MaIcZ}Rr?4sk}TL+w;QEe8K_a>6pEXxvu-$>mo2iC&j-%fMgL z#5qA#(!1&T9qFJ2g@%CL=x54onr3CmF&wsVo?jIx4nAiH%|xS7^)Dp{ zTT(Rgn@znCmtV_^02fyhYRIzs+OiLrFjt)3(BJO!vpnV4HEfM_GY-Yti3Jl|5(L6D z3Ud}^WSH%}X^mJA)Xt#=KW5+=C*nXWC3n`MP>&J=db??LbAxKVvZfD2d36gS8}zA| zx-m&jVaV^G=V z%uaGP;;M?W0HNAWL4ZCN58B6*l>){SYGvgHLXyb}+vuz`w`RFtvWo|lww7o@(glV{ zM`m`yF&&-|j2V*>GW@ds0xXQ;%L3mcn_h43LW>U3+ehFd%J}&kcpL z$N?6~>qq86myi+PSVp3%SnT2+x#8cnfH6BFGRsK`&qI8uJ`*oA4mk*qVKx`%5($mY zYHM%#r|NAGTD;vt4H|L+n^5g<7je2OYq^Heb4TMk0YdJ#S!rMGbD~aYHoh7lH#+Lx zAIoRdYh3}++4>pPJ;VFWEau1bocP_{D7K~0*QAu1>g!eo!NaXHGjPlUs*-|A zW5*B1<9?f@&+5Rxw48#-(MMdJ%8zRJmK%7uOuwKq@s0JpsBrUQT=V5X(oZ00pHP`o zFve_l){`M7{KB;J!gpqRpK6iKR%?v^X)bP0+4}4MAo4MA)UMtrC?f7lz+{^ zo#*JbGgA2x9ZU;(3wup!aSQnXB{A;zO5@YmuGo_4;TNj6R~l*&p)uQvWyX9L*ExnQ zMDjJMf5>v1n@@Y(sC$ZLe3X}55n?Xc)2M<+NRO*{91`-wiye{RM#cQS(AaiG{ioQL zfl%im2_MBb%C>3SzwFE(eI{+8Ik7vkn#Sf&bwf+)r8wA_euD5rnn)(q-lAx`=}oJ> zKkr!g)%C$H7SJNH%}LuEso`H7Ctv6x3SCva5MRD8NMAn&!&4aqehAK8udVJBgi?5* z3(&DR$3%Y=+dv7T%5Sb1HE8F~Z3of)5Y?r%9LifBF7v(6*$Ia09^wX9vb7HM^OGn} zjq*7qxz3_lls;hP^w*s*lQw~6i3Ee=uUW@Pea(IN9rR>u2OQq( zU_KTL*4v+C48bZlJD)iV52A;l&AyxtkKYB*?LrNVW*U3+^6%B1(64}NE%-2GzO&)x zP5b%jeART*jD4CEQh!wy=Z|fXnz$^8nB@9}34AM;@ zs`1lp;77qZ)J#0xb1PGZz{Tx`HHYyL0+P%_2x>sdHw5}?@X`VxUAn}sOC)Y!Y+>n; zIA(U5!y#KuLoG1(8|1@=oYj4OT+nB|2)OSI*O`Z;)Il%p-sl8_pmVnRHh~#{-v1$7 zPUdfS)qE3gCTN1BWA~?#3lA)t{sUqWe-j@;r7M)x9NRX0&~oW0iElj@G}qtA58ZBC z#DRHzJ4Ms@sQ))GUm4|b1=AKOjG+*Mc-cW{ssV(pz24l`Y{&}@GSH)%kb@|z0}k){ zQm0mfpo(LC3oGOK}=W;V8Azg&8sB6 zK4Xb--O)Y~(AYXzxvQZ1212VCS9tZ_VRb^v`J4Rj-AAZl_n%(zB#|i5%HLL!BTpEJ z;`P38Y}gK5)$!Bt`b=b)m1bSA`w?aKa9uuffEXVm#0vL^)b_LbE2g_H z(*fnvvoh4B^{PLaa4w5M#Tg$o{}sw)HCi-URx1d50MV z$$VCcE}%q<4qE|)4dL5s~6xds-+mh1LDBwVpmC{cjvoDskB5> zUH7`LCvq*7lqFDRk^l#@ZRd>VPwNKs<^oD$2zS)(p!(HO1nCq}9U58AR!7B8%BE6} zqjDw+(Bjj|p<<agUA$EiyFvBOoF%`&BQ<1w(S6)EC=UDguaFn}0TSY@RNt1!0bbHVP(%Z?U~~dIkoO{hl3FA;dIp$X!e6#gx}W zH#)!`&yr!3P%uDDbRPsp>i2^Lv(q^x+GUu{)(RetMuUCB9cB{7_V9Hsz!89jZn$10 z|Ey?+SERlVxqq!iE1g3Gi3<<2X!nNW+S4d(VggE-*!;&LIp-K==GDGf$^hr=Sxs!A zEiU836m!6$Yq@_EP&Xx(uq1aMo1ktMDat7BrX-v#Jag@>GjsUXNJh~OW5MiKD^ zL-avyGR%)WeiuAaMum*47DiIZ5ScDWba zsmAz@Rb+q|qw*2WkDM*6X8Fo@LsP8@+j@}_12h{vh&({+*6|2U(~1>47h)pXfX@Gl zN)3CyppF0{HUStIM9o>bC>)Y!mE%0MsYV+ZQedZwUuFcVxN{>BzDY>UG%YaC>T z5wZ?~amyR0*N?;`^-L?W(vAn(-?Rq{OC&Qmy6udR(Q=ZFm6wM(T}yRbY1z4f!PD-v zw{B=q8!%sHVtv=N)I91Mhl1e#E#5{#5rNDCNlnaXoD+npX*f58ErND-Dr^X?O>909 zbou6OzUKsio0y~;=*NU!XIx+Yp1aug7rhLxMoV}mm`2NaZZ_ONKG*hL664GV>5&MIixMJP7SnnlXm34jh{9Jf!xGz;xnjkv z*k<} zgU3zvp;!W`Aj1&XFNkd(d3?I;cpN$#0#dQSR+Q7YUMC#K%QvLiB@aark8`F#WMnHtCH|qiS~L_7V#G5a)%wT zpBfr381xy)5#i4qH`c_41WD@Ny}sCyD(A*QfbM72eRri`z2MhK$OlKRyq174id|pz zY)-Uv5>2UKJUAIW!A>v%%xRTNR7q!w!miTI6t8X(yE}q!8SKgTQ&@IuyMq8l;UqdP zs~)Uetn0c(y&4S@=jA6nG2M}NWb8TE`}-)LD@=};9G`iFBx$^GwQ~EcL3QXg^?fMG zWDxdwAmUvFhmk{B2LDonlJeMn*PgU|FZ&m!@4KYM3j)>1!n^N4oS>n=fxO$h60=t(dL_w(pTGAzulbwY2jR6Cdv4|DxVn#w?}d0jS2B45itwD?RP$OQs89CkuOf z`-e`ln+?>fyf^Bi-s0p|wn(e5Ttr2OV-fx67pu!3La#DKRPxRSeR!YOyocHEUdYNY&TOp-VE> zaXp0{v2`PXIW*bK6r~IahB|%Q)2W#B{G5(9mgwM>q%mg)PCMrKNfvq8AeJZ6{;?i> zvTvWT&x%F0@TjLw*p9bVk1nL0KllGUzlXYuwX3v2xyE~A-Rx`0{?mKzJSoo%<-V@b z`Hcvcj>NqG!|p6fSZb^#>pyAvyI zyVFRT6T7R3d}T=rwDLFw38TiUHILS$68L>7L{aTva2#I-1rAuc41ma7b0|EjpqZNX`z{lQ6Cf4--e_ z*RkW|))kf3OfDj&ri?YOc^ImXkv_GBUbNj_*)|q^Swhwo1o|8kaDC0GD{sH_hJY9H zE3H`b^`f9KhP@%J>w(pqBfhQEQud`Ur(3BNRcHN4oiDg}yhaMyT)g#G@b`OO(1cMu z)U`XO7Wt0SrK*@i{J5y5s2fQ$;h0O8E>CsYt4}#Oa#mRW&~;vRQS%+D>k*@1KHG1N zcVEBFaU~DcJ}3-6WnV}>{CMDyINzUt@bTB&cZZ?Z_ZVVH7lzHzhQIq5w=R7ix^(9_ zT3exceNc1$y!2Dm9%^RcfV}mRYr0WF!n-A-wn{|K^lnLXdkYs^K{XG{=9A)Mzat;S zPdm}(fiDzQzcIi=1wNGO`RY270~6>SzO}TcY{gH?8lTmb9hk`h2nH;9;;m0-2k~@d zDZ>pk5-*Tylc2K0H})xybhIwN`I=3mHFv2fm3z(^ZbPNRXSiI>Pj2>F!j!eIiKw(0 zxr>gHIB-HuWWM_-JJ1-*%F3IVq|M!|8f8A2O)72;^|&c!Q+lv5?nI2A1zqefP_e(! zCd+=`MTF!!b|QMH{q(45uFLWXTjN53sIY+NE!#Rtp}cS3L*Xc9=$EN^f%TzWh7Z9O ziwqBq2fLaC$Afa&**i@e=I$+u@BZ9LmkOM86$qq0U~uQuNh%buXJYtDTwHwkZJ@y4 zzi)O>mSg>El*F~K*Y+$%)Y`A3p3C#ElvE~xC!3yM8_#85T;6Bo{^{^%ZJ@E#L1|f_ zqxern(2j7;OJ2?jtD@lVDvRg!7u{D6#?{DpSBA%Fs9u^JR5HB77|@&M!>kX=Yknbg zW*3?_4LhY>%Q_4@6^=2Cd;ab2CaREduh_y%o}^ki(j5UmHVFf zu4Q&GBRwt*^@j%ft0QZn+6NrmiwkjBQi3^8S0Ao6j}Z?K>~cb|`*RHK_8lgpgubIc zvM(Co*Y&2NRyv_ZdeiBOZeeL=H~FcM)H21wf(5GR?KN;sP0QT(hrTy_{*G1n@1@Ck?{i}e zeo{|5_Dd}6e*Q$xI}SsI6@|&5LyAJ2a$mQhQ~E#!Iluh#00eq!juU}KF+zpE+K*L?Yn zwZ6#vso_z7a(#zSj@Az4Umi>(YA5udb=S8X>JSngl@~>&ANsi`#aedH>uT-%&NprL zC8W~Hh>e9BR{K&{6JtDe-?vmPDv7iI)QYebW^D~du1SK)f-uR9`3 z%PbxCCM}H-*_Ik{$NT5Y&*Un}_o+PGP)h0f{>;}CE}cjW$4;yXcnYl70vdJJ;)~Kc+b!Z4x{Q%0)cBh%FzdSW;tM zo|9mR#&EJHN2xYiJ*Q@o-?x9PASxQL3pDNB)w5SG{+zP}XPs*?zE}z;VOUS{K(cB% zem(DK-)Z(_xZs)T5sh@>k4crH_x@64g++N=&yRA5WH3&)57vl?{G+LfKtC{8o0Sme;Jp@LT`;OWRz6 z$k!gcrCF+ZSs(Y>0=l!7FDNS(ROZbMcn0PD9ckPPIYi>kW^J=dwoHf6Ep~?2V$KCD zUYUvB$Y;jxeTZ$*-;troS*Z1N%)xK4=v#yL`@lRt<;7`N3|_j2eZvP1bkTlz0aXkVK*tm$XNe>(WafQd zkG_0|4XI?4h|kBqR5jRRJWHPLJA+CBAsf-cNub^Z6sp8T8B8F$4UaMuKEheYNnC!# z0d2l0CvS~5WD{*zsAs{Co1;575Ly=)O6egZ2iG4#>&9xf@C6r!rC$`uv6wX-?al58 zdE5wG$sQ0w*kTJw8AR?QFucmI-ACj>K|FndP?#hTYd}9zbcka+I4*6%+FoIdA696C zAv4{NoSR?7tK;tU)lf@|j6nIGXQtoIELbJT8atWVigQqOB>1B}%f0#KATX)BqK5b0 zKMys#VnOS5(`22t*ky3V`k1v^S~l@$e`V_#Ikct?1Hx7S#E_TW%&+3k&G$-YGQQ`I z87Qr(k(f4|Rr{r3F*$8QBrjS}z_W$4HRHoWT6^+GC2Kwh0K$MVBmFWcH!@Ue<$lUh zr`TH8DXs^Qr}b%dA!3DJUru{yhriAHBgf~t)U}`@4?->Vp=Dvw{TBPS(b@LTz8_fu z{1U{3@T=633x6$9hQ3k!O<7S4S5N zXA+1aZA&WtVbrdQ=!LlDpTOv+_M~k29_kv-&PK6O(sEhQvJ|uYx_l4LqxKDQSzReq zuu4${hFb0w<(uv2CvH8M?9G-)fLs#tt?SM8d)EVZg@T<`D)R0`%cFhBhDAbQOj-SCUSrH9tUY74wt{+gX+(J52daW>;&%)|1#?|Xizde9@ct7 zjR@sxe})NZ#wudF838yK|8wLo>!8Wwzu|*QD^Rw5w*teQhbz79_HbM8jvE=#6HNt< z1cZIHR>keX&yL~$xyQm>G*DsO%itnwBMikdWl3+%>66ie8!Hk>((hdVac-b!U!S=M z)#hc~eFXm;u=z%HaSi!6D-CZZD(m`;JUX7id0l;>q%hQddrNzxaooe_uCg{`E>CtA z``Ru`{&ODq+hIlKPEMLzB9^eWF&1k%b?o?}O)&-3)|2C~YjK4_62_C@Jgw=I@r4^I zw0}=@GM{Fbvq^j;il&%H@+dDNxS{kkj}8eKBeLDU3vRvf&Q<>`vF0!B`9J45>>3$p zDr`uXgE)&jH)SjilFtOy>^bLVo(9Mj4x-*|&zQSjJzjCXqq_aS14db|BQX|i8lc8A zxMh+pFNs{|(`4E*P1dlD8nydXS)KwwWdG-IvY%%C$D?Z8xt8=Ww>S=2?0Cp(4&S!p zAI%m9Y)1Rl_U}6@zrZS3{?Do#%qN3Y2b+_0)yp;sYmn}paQAcj2BkoQ;DB>a)AJvM zOyq_p%?~}b2b>oFISiJPu@e;LBooJkHsqp~+3*Uj7lEPA=4OC(c}&Id4st>pv*eUt zKFoV6a`#F8=REM)RT~ukSNT#Ki;7|nooPprOaxBAqC055IUL1bWxo{?zC0~!p+W!m zJg3)CHaQzjDp54o#M^ik%}#N{=}WMXI+F9v|F`a?Dz2B_#+Lvo{XfTxH+!-zba}^u zsZm8mGUuKwcvvPUe=INgv(Q%>KF!j}x1M+*FCpPrQ=j2~WV>ozB4ZF)_;E$TVvqX#&HJzkZbviw9XUE8v089q zwC`p?k`cqseg^Z-{{N$e{_BlE3$vj9@v1&;17EG>95)--rR8!UFpi{&+$5kIHNhXg z?;N~G{Ds^7=fV89)6wJ~f5--39>Q{r0{26S=d@J4fJV z01WHD$C>^To18ZYnFaH>G4qRxiqu7o6pSH1dBLYiTRSdvzK$`TJT^=daP0^E?@18Y z-bE2@p|Z&tiIVgUQda(dG@esgBhlhFckS$H_3XdD-uF9gzLH5CYy3HtONYd1TOa*k zZ5+N48U~v%^)|w zi~pAPW*qdxR2!VQ-@WJuAIB@ELvQCwH#cj4CU7%A8uIzJQ_OOAlE$KZ z?NC!T8*akeU;X>@v+<^)(u=lN6l1``uJc3Sl5;`n9WKD|{nhd&;?6bTR+q0V|24G` zDHge+B3YKa^`gHnn9|lZoM{XnLOU1Is_r5?APaCT4dQB=G)o{2XiYEtZwl(suu-;jKS;(hMG?u3`Cl zR9%$n?mNo2M!2Lm%qCCdUcI@7{;w5lAg5B(o@A!s0G7CQ=WC&Su*;ACTjKv*9E3MR z%;2+Ov0BkcD7rdTAjEJt+@n#HM1~di?`)X@Lauy9X<>OP1*1DZ{LcX^Z6Mcnr(h`= z_-$$}2XU%p*GN<_)gT>JWohc|ktvS2dLw}KpC5zF=vWhAgA#S@B(nX-l6roF3f_bK z9I6hL4|mQ?n{Z6Ch%0+zHk-$N?|;tHV38AyDSG+6tKHIC?w?JkDnhmZWL`sf3!Nq? z5vOK-nu(j$)$0GA1W74cEN@xl9t<#-SIVWMs*cJ)f<2DAd>EWG2I7qh2A39d-^2gE zHnMB4KtYyh>FLGqpOv(APo?Id@54|bieG?};8w1Bp*^|!sep0Czh}69sp5YFhCH7Y zsJ3>K!9-B*l_g~|Gz+{KUA%f_8t`cwaJblLu!Id2fVcbKUu4%0d&YJZ8g@np4t_BmQ)Y-Vsi;0Vi?Ica(|onKg9Go@>nc-#bE< zwEVWx?bXLP&RsYgmB@HcQU|IR@F@7-D_wU#bJ6J+8vy!oOT zf;1wM9^Kn)SWE$Hzn}w9UG5fM>w?xwT4&#md*`-W&siV#Fy+`rx zCdFUh@9}>+Yt>{L4^$H?|w=0|1KS{ m@+r>y;hJCa|1Jd|;uervu;YfGeh|OAlDxEvRH=kX(EkU%;)*2z literal 0 HcmV?d00001 From ec8bc1d3d403dc0fa4ae1687219e1724c22bc9af Mon Sep 17 00:00:00 2001 From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:39:02 +0100 Subject: [PATCH 17/59] fix: use camelCase JSON tags for GCP Vertex AI request fields (#2225) **Description** The request body we send to GCP Vertex AI (the Gemini generateContent API) had a few field names in the wrong style. Vertex expects camelCase names like `generationConfig` and `systemInstruction`, but three of them were written in snake_case (`generation_config`, `system_instruction`, `tool_config`). The `safetySettings` field was already camelCase, so the struct was inconsistent with itself. This updates those three names to camelCase so they all match the format Vertex documents and the format the `google.golang.org/genai` library uses. Vertex does accept the snake_case versions as well, so this isn't fixing broken behaviour, but it removes the inconsistency and means we're no longer relying on that leniency. I also updated the tests that were asserting the old snake_case output. While I was in there, I replaced some hardcoded content-length values in the unit test with a check that the content-length header matches the actual body length. Those fixed numbers would have needed recalculating every time the body changed by even a byte, and the new check stays correct on its own. Testing: the existing unit tests and the data plane tests pass, including the GCP Vertex AI cases that exercise the request body end to end. Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> --- internal/apischema/gcp/gcp.go | 6 +-- .../translator/openai_gcpvertexai_test.go | 49 +++++++++++-------- tests/data-plane/testupstream_test.go | 8 +-- 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/internal/apischema/gcp/gcp.go b/internal/apischema/gcp/gcp.go index 285225bc04..fc007b0073 100644 --- a/internal/apischema/gcp/gcp.go +++ b/internal/apischema/gcp/gcp.go @@ -24,17 +24,17 @@ type GenerateContentRequest struct { // This config is shared for all tools provided in the request. // // https://github.com/googleapis/go-genai/blob/6a8184fcaf8bf15f0c566616a7b356560309be9b/types.go#L1466 - ToolConfig *genai.ToolConfig `json:"tool_config,omitempty"` + ToolConfig *genai.ToolConfig `json:"toolConfig,omitempty"` // Optional. Generation config. // You can find API default values and more details at https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig // and https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/content-generation-parameters. - GenerationConfig *genai.GenerationConfig `json:"generation_config,omitempty"` + GenerationConfig *genai.GenerationConfig `json:"generationConfig,omitempty"` // Optional. Instructions for the model to steer it toward better performance. // For example, "Answer as concisely as possible" or "Don't use technical // terms in your response". // // https://github.com/googleapis/go-genai/blob/6a8184fcaf8bf15f0c566616a7b356560309be9b/types.go#L858 - SystemInstruction *genai.Content `json:"system_instruction,omitempty"` + SystemInstruction *genai.Content `json:"systemInstruction,omitempty"` // Optional: Safety settings in the request to block unsafe content in the response. // // https://github.com/googleapis/go-genai/blob/6a8184fcaf8bf15f0c566616a7b356560309be9b/types.go#L1057 diff --git a/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index 07ffe78315..d81fdca9f3 100644 --- a/internal/translator/openai_gcpvertexai_test.go +++ b/internal/translator/openai_gcpvertexai_test.go @@ -8,6 +8,7 @@ package translator import ( "bytes" "slices" + "strconv" "strings" "testing" @@ -79,12 +80,12 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) } ], "tools": null, - "generation_config": { + "generationConfig": { "maxOutputTokens": 100, "stopSequences": ["stop1", "stop2"], "temperature": 0.1 }, - "system_instruction": { + "systemInstruction": { "parts": [ { "text": "You are a helpful assistant" @@ -129,8 +130,8 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) ] } ], - "generation_config": {}, - "system_instruction": { + "generationConfig": {}, + "systemInstruction": { "parts": [ { "text": "You are a helpful assistant" @@ -169,7 +170,7 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) ] } ], - "generation_config": { + "generationConfig": { "maxOutputTokens": 1024, "stopSequences": ["stop"], "temperature": 0.7, @@ -209,7 +210,7 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) ] } ], - "generation_config": { + "generationConfig": { "maxOutputTokens": 1024, "temperature": 0.7 }, @@ -245,7 +246,7 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) ] } ], - "generation_config": { + "generationConfig": { "maxOutputTokens": 1024, "mediaResolution": "high", "temperature": 0.7 @@ -281,7 +282,7 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) ] } ], - "generation_config": { + "generationConfig": { "maxOutputTokens": 1024, "temperature": 0.7, "responseMimeType": "text/x.enum", @@ -324,7 +325,7 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) ] } ], - "generation_config": { + "generationConfig": { "maxOutputTokens": 1024, "temperature": 0.7, "responseMimeType": "application/json", @@ -351,7 +352,7 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) "enterpriseWebSearch": {} } ], - "generation_config": { + "generationConfig": { "maxOutputTokens": 1024, "temperature": 0.7 } @@ -400,7 +401,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) // Since these are stub implementations, we expect nil mutations. wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-pro:generateContent"}, - {"content-length", "258"}, }, wantBody: wantBdy, }, @@ -438,7 +438,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) // Since these are stub implementations, we expect nil mutations. wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-pro:streamGenerateContent?alt=sse"}, - {"content-length", "258"}, }, wantBody: wantBdy, }, @@ -477,7 +476,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) // Since these are stub implementations, we expect nil mutations. wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-flash:generateContent"}, - {"content-length", "258"}, }, wantBody: wantBdy, }, @@ -532,7 +530,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) wantError: false, wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-pro:generateContent"}, - {"content-length", "518"}, }, wantBody: wantBdyWithTools, }, @@ -582,7 +579,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) wantError: false, wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-1.5-pro:generateContent"}, - {"content-length", "396"}, }, wantBody: wantBdyWithVendorFields, }, @@ -630,7 +626,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) wantError: false, wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-1.5-pro:generateContent"}, - {"content-length", "395"}, }, wantBody: wantBdyWithSafetySettingFields, }, @@ -675,7 +670,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) wantError: false, wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-3-pro:generateContent"}, - {"content-length", "343"}, }, wantBody: wantBdyWithMediaResolutionFields, }, @@ -716,7 +710,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) wantError: false, wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-1.5-pro:generateContent"}, - {"content-length", "404"}, }, wantBody: wantBdyWithGuidedChoice, }, @@ -757,7 +750,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) wantError: false, wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-1.5-pro:generateContent"}, - {"content-length", "408"}, }, wantBody: wantBdyWithGuidedRegex, }, @@ -785,7 +777,6 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) wantError: false, wantHeaderMut: []internalapi.Header{ {":path", "publishers/google/models/gemini-1.5-pro:generateContent"}, - {"content-length", "190"}, }, wantBody: wantBdyWithEnterpriseWebSearch, }, @@ -801,7 +792,23 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) } require.NoError(t, err) - if diff := cmp.Diff(tc.wantHeaderMut, headerMut); diff != "" { + // Separate the content-length header from the others and assert it + // matches the serialized body length, rather than hardcoding a + // byte count that breaks whenever the body changes by a byte. + var gotHeaders []internalapi.Header + foundContentLength := false + for _, h := range headerMut { + if h.Key() == contentLengthHeaderName { + assert.Equal(t, strconv.Itoa(len(bodyMut)), h.Value(), + "content-length header should equal the serialized body length") + foundContentLength = true + continue + } + gotHeaders = append(gotHeaders, h) + } + assert.True(t, foundContentLength, "content-length header should be set") + + if diff := cmp.Diff(tc.wantHeaderMut, gotHeaders); diff != "" { t.Errorf("HeaderMutation mismatch (-want +got):\n%s", diff) } diff --git a/tests/data-plane/testupstream_test.go b/tests/data-plane/testupstream_test.go index d73bd0aafc..19bf7d503f 100644 --- a/tests/data-plane/testupstream_test.go +++ b/tests/data-plane/testupstream_test.go @@ -310,7 +310,7 @@ func TestWithTestUpstream(t *testing.T) { path: "/v1/chat/completions", method: http.MethodPost, requestBody: `{"model":"gemini-1.5-pro","messages":[{"role":"system","content":"You are a helpful assistant."}]}`, - expRequestBody: `{"contents":null,"tools":null,"generation_config":{},"system_instruction":{"parts":[{"text":"You are a helpful assistant."}]}}`, + expRequestBody: `{"contents":null,"tools":null,"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a helpful assistant."}]}}`, expPath: "/v1/projects/gcp-project-name/locations/gcp-region/publishers/google/models/gemini-1.5-pro:generateContent", expRequestHeaders: map[string]string{"Authorization": "Bearer " + fakeGCPAuthToken}, responseStatus: strconv.Itoa(http.StatusOK), @@ -324,7 +324,7 @@ func TestWithTestUpstream(t *testing.T) { path: "/v1/chat/completions", method: http.MethodPost, requestBody: `{"model":"gemini-1.5-pro","messages":[{"role":"system","content":"You are a helpful assistant."}]}`, - expRequestBody: `{"contents":null,"tools":null,"generation_config":{},"system_instruction":{"parts":[{"text":"You are a helpful assistant."}]}}`, + expRequestBody: `{"contents":null,"tools":null,"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a helpful assistant."}]}}`, expPath: "/v1/projects/gcp-project-name/locations/gcp-region/publishers/google/models/gemini-1.5-pro:generateContent", expRequestHeaders: map[string]string{"Authorization": "Bearer " + fakeGCPAuthToken}, responseStatus: strconv.Itoa(http.StatusOK), @@ -338,7 +338,7 @@ func TestWithTestUpstream(t *testing.T) { path: "/v1/chat/completions", method: http.MethodPost, requestBody: `{"model":"gemini-1.5-pro","messages":[{"role":"user","content":"tell me the delivery date for order 123"}],"tools":[{"type":"function","function":{"name":"get_delivery_date","description":"Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'","parameters":{"type":"object","properties":{"order_id":{"type":"string","description":"The customer's order ID."}},"required":["order_id"]}}}]}`, - expRequestBody: `{"contents":[{"parts":[{"text":"tell me the delivery date for order 123"}],"role":"user"}],"tools":[{"functionDeclarations":[{"description":"Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'","name":"get_delivery_date","parameters":{"properties":{"order_id":{"description":"The customer's order ID.","type":"string"}},"required":["order_id"],"type":"object"}}]}],"generation_config":{}}`, + expRequestBody: `{"contents":[{"parts":[{"text":"tell me the delivery date for order 123"}],"role":"user"}],"tools":[{"functionDeclarations":[{"description":"Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'","name":"get_delivery_date","parameters":{"properties":{"order_id":{"description":"The customer's order ID.","type":"string"}},"required":["order_id"],"type":"object"}}]}],"generationConfig":{}}`, expPath: "/v1/projects/gcp-project-name/locations/gcp-region/publishers/google/models/gemini-1.5-pro:generateContent", expRequestHeaders: map[string]string{"Authorization": "Bearer " + fakeGCPAuthToken}, responseStatus: strconv.Itoa(http.StatusOK), @@ -507,7 +507,7 @@ data: [DONE] responseType: "sse", method: http.MethodPost, requestBody: `{"model":"gemini-1.5-pro","messages":[{"role":"system","content":"You are a helpful assistant."}], "stream": true}`, - expRequestBody: `{"contents":null,"tools":null,"generation_config":{},"system_instruction":{"parts":[{"text":"You are a helpful assistant."}]}}`, + expRequestBody: `{"contents":null,"tools":null,"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a helpful assistant."}]}}`, expPath: "/v1/projects/gcp-project-name/locations/gcp-region/publishers/google/models/gemini-1.5-pro:streamGenerateContent", expRawQuery: "alt=sse", expRequestHeaders: map[string]string{"Authorization": "Bearer " + fakeGCPAuthToken}, From 4a98a029863ec8701ff4ebdc9318ba8ef447913f Mon Sep 17 00:00:00 2001 From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Date: Fri, 12 Jun 2026 09:08:29 +0100 Subject: [PATCH 18/59] fix: prevent empty namespace from breaking RBAC name rendering in helm chart (#2226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** The controller and inference-pool `ClusterRole`/`ClusterRoleBinding` names are built with `printf "%s:%s" .Release.Namespace` to keep them unique per-release (introduced in #1511). When `.Release.Namespace` is empty, this renders `name: :` — an unquoted trailing colon that strict YAML parsers reject with `mapping values are not allowed in this context`. The whole render fails and zero resources are produced. This doesn't trigger via `helm install` or `helm template` (the CLI always defaults the namespace), only where a renderer leaves it unset — e.g. the Helm SDK's `engine.Render` without `ReleaseOptions`, or Replicated's image-extraction render. The fix adds two independent layers of defense across the three affected helpers (`_helpers.tpl`) and their six call sites (`serviceaccount.yaml`, `envoy_gateway_cluster_role_for_inference_pool.yaml`): - Wrap the namespace in `default "default"` so the `printf` argument is never empty. No behavior change for real installs, where the namespace is always populated. - Quote the rendered `name` fields so a trailing colon is always unambiguous. Either layer fixes the failure on its own; both are included for robustness. **Special notes for reviewers (if applicable)** Verified with `helm lint` and `helm template` (default and explicit namespaces) — names render correctly as `:` with no change to existing behavior. Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> --- manifests/charts/ai-gateway-helm/templates/_helpers.tpl | 6 +++--- .../envoy_gateway_cluster_role_for_inference_pool.yaml | 6 +++--- .../charts/ai-gateway-helm/templates/serviceaccount.yaml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/manifests/charts/ai-gateway-helm/templates/_helpers.tpl b/manifests/charts/ai-gateway-helm/templates/_helpers.tpl index 357098eadb..0a44188449 100644 --- a/manifests/charts/ai-gateway-helm/templates/_helpers.tpl +++ b/manifests/charts/ai-gateway-helm/templates/_helpers.tpl @@ -76,7 +76,7 @@ Create the name of the cluster role to use {{- if $existing }} {{- (include "ai-gateway-helm.controller.serviceAccountName" .) }} {{- else }} -{{- printf "%s:%s" (include "ai-gateway-helm.controller.serviceAccountName" .) .Release.Namespace }} +{{- printf "%s:%s" (include "ai-gateway-helm.controller.serviceAccountName" .) (default "default" .Release.Namespace) }} {{- end }} {{- end }} @@ -85,7 +85,7 @@ Create the name of the cluster role to use {{- if $existing }} {{- "envoy-ai-gateway-inference-pool-reader" }} {{- else }} -{{- printf "%s:%s" "envoy-ai-gateway-inference-pool-reader" .Release.Namespace}} +{{- printf "%s:%s" "envoy-ai-gateway-inference-pool-reader" (default "default" .Release.Namespace)}} {{- end}} {{- end -}} @@ -94,7 +94,7 @@ Create the name of the cluster role to use {{- if $existing }} {{- "envoy-ai-gateway-inference-pool-reader-binding" }} {{- else }} -{{- printf "%s:%s" "envoy-ai-gateway-inference-pool-reader-binding" .Release.Namespace}} +{{- printf "%s:%s" "envoy-ai-gateway-inference-pool-reader-binding" (default "default" .Release.Namespace)}} {{- end}} {{- end -}} diff --git a/manifests/charts/ai-gateway-helm/templates/envoy_gateway_cluster_role_for_inference_pool.yaml b/manifests/charts/ai-gateway-helm/templates/envoy_gateway_cluster_role_for_inference_pool.yaml index 2378e5eaf3..023267bd64 100644 --- a/manifests/charts/ai-gateway-helm/templates/envoy_gateway_cluster_role_for_inference_pool.yaml +++ b/manifests/charts/ai-gateway-helm/templates/envoy_gateway_cluster_role_for_inference_pool.yaml @@ -10,7 +10,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "ai-gateway-helm.inference-pool.clusterRoleName" . }} + name: {{ include "ai-gateway-helm.inference-pool.clusterRoleName" . | quote }} rules: - apiGroups: - "inference.networking.k8s.io" @@ -24,11 +24,11 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ include "ai-gateway-helm.inference-pool.clusterRoleBindingName" . }} + name: {{ include "ai-gateway-helm.inference-pool.clusterRoleBindingName" . | quote }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: {{ include "ai-gateway-helm.inference-pool.clusterRoleName" . }} + name: {{ include "ai-gateway-helm.inference-pool.clusterRoleName" . | quote }} subjects: - kind: ServiceAccount # The service account name is hardcoded to "envoy-gateway": diff --git a/manifests/charts/ai-gateway-helm/templates/serviceaccount.yaml b/manifests/charts/ai-gateway-helm/templates/serviceaccount.yaml index a984ca1ccf..2c8fc7d6e8 100644 --- a/manifests/charts/ai-gateway-helm/templates/serviceaccount.yaml +++ b/manifests/charts/ai-gateway-helm/templates/serviceaccount.yaml @@ -19,7 +19,7 @@ metadata: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "ai-gateway-helm.controller.clusterRoleName" . }} + name: {{ include "ai-gateway-helm.controller.clusterRoleName" . | quote }} rules: - apiGroups: [""] resources: @@ -103,11 +103,11 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ include "ai-gateway-helm.controller.clusterRoleName" . }} + name: {{ include "ai-gateway-helm.controller.clusterRoleName" . | quote }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: {{ include "ai-gateway-helm.controller.clusterRoleName" . }} + name: {{ include "ai-gateway-helm.controller.clusterRoleName" . | quote }} subjects: - kind: ServiceAccount name: {{ include "ai-gateway-helm.controller.serviceAccountName" . }} From 9a4b02c2195bc5d7d9e495ec096c4a584069653f Mon Sep 17 00:00:00 2001 From: Linus Schlumberger Date: Fri, 12 Jun 2026 10:46:26 +0200 Subject: [PATCH 19/59] fix: skip empty-content assistant messages in Bedrock translator (#2191) **Description** In the OpenAI-to-AWS-Bedrock message converter, Bedrock Converse rejects assistant messages with empty content arrays. Some clients send empty assistant messages. This skips them when they have no content blocks. Tool-call-only messages are preserved. Fixes #2208 Signed-off-by: Linus Schlumberger Co-authored-by: Ignasi Barrera --- internal/translator/openai_awsbedrock.go | 6 +- internal/translator/openai_awsbedrock_test.go | 111 +++++++++++++++++- 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/internal/translator/openai_awsbedrock.go b/internal/translator/openai_awsbedrock.go index d5ddc9d168..39afb833af 100644 --- a/internal/translator/openai_awsbedrock.go +++ b/internal/translator/openai_awsbedrock.go @@ -521,7 +521,11 @@ func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) openAIMessageToBedrockMes if err != nil { return err } - bedrockReq.Messages = append(bedrockReq.Messages, bedrockMessage) + // Some clients, like OpenCode, can send assistant messages with nil or empty string content and no tool calls, + // which would translate to an empty content array that Bedrock Converse rejects. + if len(bedrockMessage.Content) > 0 { + bedrockReq.Messages = append(bedrockReq.Messages, bedrockMessage) + } case msg.OfSystem != nil: if bedrockReq.System == nil { bedrockReq.System = make([]*awsbedrock.SystemContentBlock, 0) diff --git a/internal/translator/openai_awsbedrock_test.go b/internal/translator/openai_awsbedrock_test.go index c07479ea62..4cc83339f6 100644 --- a/internal/translator/openai_awsbedrock_test.go +++ b/internal/translator/openai_awsbedrock_test.go @@ -225,10 +225,6 @@ func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_RequestBody(t *testing.T) }, }, }, - { - Role: openai.ChatMessageRoleAssistant, - Content: []*awsbedrock.ContentBlock{}, - }, }, }, }, @@ -2582,3 +2578,110 @@ func TestCacheControlHelpers(t *testing.T) { require.Nil(t, cachePoint3) }) } + +func TestOpenAIToAWSBedrockTranslator_EmptyContentMessages(t *testing.T) { + t.Run("empty assistant message without tool calls is dropped", func(t *testing.T) { + for _, name := range []string{"empty string", "nil content"} { + t.Run(name, func(t *testing.T) { + assistantMsg := &openai.ChatCompletionAssistantMessageParam{ + Role: openai.ChatMessageRoleAssistant, + } + if name == "empty string" { + assistantMsg.Content = openai.StringOrAssistantRoleContentUnion{Value: ""} + } + + o := &openAIToAWSBedrockTranslatorV1ChatCompletion{} + req := openai.ChatCompletionRequest{ + Model: "anthropic.claude-3-5-sonnet-20241022-v2:0", + Messages: []openai.ChatCompletionMessageParamUnion{ + {OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "Hello"}}}, + {OfAssistant: assistantMsg}, + {OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "Can you help?"}}}, + }, + } + + _, newBody, err := o.RequestBody(nil, &req, false) + require.NoError(t, err) + + var awsReq awsbedrock.ConverseInput + require.NoError(t, json.Unmarshal(newBody, &awsReq)) + requireNoEmptyAssistantContent(t, awsReq.Messages) + }) + } + }) + + t.Run("assistant message with only tool calls and no content should work", func(t *testing.T) { + o := &openAIToAWSBedrockTranslatorV1ChatCompletion{} + req := openai.ChatCompletionRequest{ + Model: "anthropic.claude-3-5-sonnet-20241022-v2:0", + Messages: []openai.ChatCompletionMessageParamUnion{ + {OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "What is the weather?"}}}, + {OfAssistant: &openai.ChatCompletionAssistantMessageParam{ + Role: openai.ChatMessageRoleAssistant, + ToolCalls: []openai.ChatCompletionMessageToolCallParam{ + {ID: ptr.To("call_abc123"), Type: openai.ChatCompletionMessageToolCallTypeFunction, Function: openai.ChatCompletionMessageToolCallFunctionParam{Name: "get_weather", Arguments: `{"location":"NYC"}`}}, + }, + }}, + {OfTool: &openai.ChatCompletionToolMessageParam{Role: openai.ChatMessageRoleTool, ToolCallID: "call_abc123", Content: openai.ContentUnion{Value: `{"temp":72}`}}}, + }, + } + + _, newBody, err := o.RequestBody(nil, &req, false) + require.NoError(t, err) + + var awsReq awsbedrock.ConverseInput + require.NoError(t, json.Unmarshal(newBody, &awsReq)) + + var foundAssistant bool + for _, msg := range awsReq.Messages { + if msg.Role == openai.ChatMessageRoleAssistant { + foundAssistant = true + require.NotEmpty(t, msg.Content, "assistant message with tool calls should have non-empty content") + require.NotNil(t, msg.Content[0].ToolUse, "expected a ToolUse block") + require.Equal(t, "get_weather", msg.Content[0].ToolUse.Name) + } + } + require.True(t, foundAssistant, "should have found an assistant message") + }) + + t.Run("mixed messages with some empty assistant messages should handle correctly", func(t *testing.T) { + o := &openAIToAWSBedrockTranslatorV1ChatCompletion{} + req := openai.ChatCompletionRequest{ + Model: "anthropic.claude-3-5-sonnet-20241022-v2:0", + Messages: []openai.ChatCompletionMessageParamUnion{ + {OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "Hello"}}}, + {OfAssistant: &openai.ChatCompletionAssistantMessageParam{Role: openai.ChatMessageRoleAssistant, Content: openai.StringOrAssistantRoleContentUnion{Value: ""}}}, + {OfUser: &openai.ChatCompletionUserMessageParam{Role: openai.ChatMessageRoleUser, Content: openai.StringOrUserRoleContentUnion{Value: "Tell me more"}}}, + {OfAssistant: &openai.ChatCompletionAssistantMessageParam{Role: openai.ChatMessageRoleAssistant, Content: openai.StringOrAssistantRoleContentUnion{Value: "Here is the info"}}}, + }, + } + + _, newBody, err := o.RequestBody(nil, &req, false) + require.NoError(t, err) + + var awsReq awsbedrock.ConverseInput + require.NoError(t, json.Unmarshal(newBody, &awsReq)) + requireNoEmptyAssistantContent(t, awsReq.Messages) + + var assistantWithContent bool + for _, msg := range awsReq.Messages { + if msg.Role == openai.ChatMessageRoleAssistant { + for _, block := range msg.Content { + if block.Text != nil && *block.Text == "Here is the info" { + assistantWithContent = true + } + } + } + } + require.True(t, assistantWithContent, "should preserve assistant message with actual content") + }) +} + +func requireNoEmptyAssistantContent(t *testing.T, messages []*awsbedrock.Message) { + t.Helper() + for i, msg := range messages { + if msg.Role == openai.ChatMessageRoleAssistant && len(msg.Content) == 0 { + t.Errorf("message at index %d is an assistant message with empty content array, which Bedrock will reject", i) + } + } +} From 3f817c08e8e4f820ca487afea2785cf57a226c4b Mon Sep 17 00:00:00 2001 From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:33:26 +0100 Subject: [PATCH 20/59] extproc: skip CONTINUE_AND_REPLACE when no body change is needed (#2170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** **Scope:** this PR fixes the narrow case where the upstream cluster ext_proc filter issued `CONTINUE_AND_REPLACE` even though *nothing in the request pipeline needed to mutate the body* — no translation, no model override, no retry, no backend `HTTPBodyMutation`. In that path, the upstream filter replayed `u.parent.originalRequestBodyRaw` (snapshotted at the router phase), which clobbered any body mutation applied by an earlier ext_proc filter in the listener chain. After this PR, that path emits `CONTINUE` and leaves the body alone. The broader question of "preserve an intermediate filter's body mutation across translation / retries / `HTTPBodyMutation`" is **not** addressed by this PR. When `wantBodyReplace == true`, the replacement body is still derived from the router-phase snapshot, so any intermediate filter's mutation is lost on those branches. That's an architectural property of the snapshot-at-router- phase design — the upstream filter has no view of Envoy's current body, by design, to avoid re-buffering through ext_proc at the upstream phase. Fixing it requires a separate design (either re-buffering at upstream, or propagating a diff via dynamic metadata) and should be its own PR. **Root cause of the narrow bug.** The existing safety net in `applyBodyMutation` looks like it covers the no-config case: if bodyMutator == nil { return bodyMutation } but it does not fire in practice. `bodymutator.NewBodyMutator(nil, ...)` returns a non-nil struct even when its `*filterapi.HTTPBodyMutation` argument is nil. So the early-return is dead code; the function falls through, calls `Mutate(originalRequestBodyRaw)` on a no-config mutator (which returns its input unchanged), wraps that in a `BodyMutation`, and the upstream filter sends `CONTINUE_AND_REPLACE` with the original body — even though nothing asked for a body replacement. **Fix:** * Add `BodyMutator.HasMutations()` predicate. `NewBodyMutator`'s contract is unchanged (it still returns non-nil); callers can ask whether the mutator actually has anything to do. * In `upstreamProcessor.ProcessRequestHeaders`, compute mutatorHasMutations := u.bodyMutator != nil && u.bodyMutator.HasMutations() wantBodyReplace := bodyMutation != nil || forceBodyMutation || mutatorHasMutations before calling `applyBodyMutation`. When false, skip the body-mutation work and emit `CONTINUE` instead of `CONTINUE_AND_REPLACE`, preserving translator header mutations (e.g. path rewrite), header-mutator output, and auth-handler header additions. Skip the content-length dynamic-metadata restamp on the `CONTINUE` branch since the body length did not change. All existing `CONTINUE_AND_REPLACE` callers stay on the existing path (these are the branches where this PR explicitly does not change behavior, including the loss-of-earlier-mutation property documented above): * Translator emits a body (AWS Bedrock/SigV4, OpenAI to Anthropic, OpenAI to Vertex, etc.) — `bodyMutation != nil`. * Backend `HTTPBodyMutation` configured — `HasMutations() == true`. * Retry — `forceBodyMutation == true` via `u.onRetry()`. * Streaming with `IncludeUsage=false` — `forceBodyMutation == true`. * Model override — translator emits `newBody` via `sjson.SetBytesOptions`, so `bodyMutation != nil`. **Tests:** * Added `Test_chatCompletionProcessorUpstreamFilter_ProcessRequestHeaders_BodyReplaceContract` with two subtests: * `no translator body, no mutator, no force -> CONTINUE` exercises the previously untested path. Asserts `Status == CONTINUE`, no `BodyMutation`, preserved translator and auth-handler header mutations, and `DynamicMetadata == nil` (no content-length restamp). * `translator body present -> CONTINUE_AND_REPLACE` pins the existing behavior so a future refactor cannot quietly flip the contract back. The coverage gap that let this ship: every existing `t.Run("ok", ...)` subtest under `Test_chatCompletionProcessorUpstreamFilter_ProcessRequestHeaders` set `retBodyMutation` on the mock translator, so the "translator returns nil body" path was never exercised. **Related Issues/PRs (if applicable)** None. **Special notes for reviewers (if applicable)** * The fix is request-side only. Response-side code is untouched. * `NewBodyMutator`'s contract is intentionally not changed (it still returns non-nil even when given a nil `*filterapi.HTTPBodyMutation`). A predicate is added instead, to keep the blast radius local to `ProcessRequestHeaders`. If reviewers prefer changing the constructor to return nil in the no-config case, that is a larger refactor and a separate PR. * The bug is in the generic `upstreamProcessor[...]`, so the fix applies to every endpoint spec (chat completions, messages, embeddings, completions, GCP, AWS Bedrock, etc.). The new tests exercise one endpoint spec for the same reason the existing `ProcessRequestHeaders` table-test does — locking the generic method's contract. Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Co-authored-by: Ignasi Barrera --- internal/bodymutator/body_mutator.go | 8 ++ internal/extproc/processor_impl.go | 31 +++++- internal/extproc/processor_impl_test.go | 122 ++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 2 deletions(-) diff --git a/internal/bodymutator/body_mutator.go b/internal/bodymutator/body_mutator.go index ec9d3c6c97..f5613d7967 100644 --- a/internal/bodymutator/body_mutator.go +++ b/internal/bodymutator/body_mutator.go @@ -29,6 +29,14 @@ func NewBodyMutator(bodyMutations *filterapi.HTTPBodyMutation, originalBody []by } } +// HasMutations reports whether this BodyMutator was constructed with a +// non-nil HTTPBodyMutation config. When false, Mutate is a no-op that +// returns its input unchanged, so callers can short-circuit the +// upstream filter's body-replacement path entirely. +func (b *BodyMutator) HasMutations() bool { + return b != nil && b.bodyMutations != nil +} + // isJSONValue checks if a string represents a JSON value (not a plain string) func isJSONValue(value string) bool { value = strings.TrimSpace(value) diff --git a/internal/extproc/processor_impl.go b/internal/extproc/processor_impl.go index 546deafc54..abb6ac956b 100644 --- a/internal/extproc/processor_impl.go +++ b/internal/extproc/processor_impl.go @@ -366,8 +366,19 @@ func (u *upstreamProcessor[ReqT, RespT, RespChunkT, EndpointSpecT]) ProcessReque } } - // Apply body mutations from the route and also restore original body on retry. - bodyMutation = applyBodyMutation(u.bodyMutator, bodyMutation, u.parent.originalRequestBodyRaw, u.logger) + // Decide whether the upstream filter should replace the request body at + // all. If the translator emitted no body, no backend HTTPBodyMutation is + // configured, and we're not forcing body replay (retry or + // streaming-without-usage), then issuing CONTINUE_AND_REPLACE with the + // captured original body would clobber any body mutation applied by an + // earlier ext_proc filter in the chain. + mutatorHasMutations := u.bodyMutator != nil && u.bodyMutator.HasMutations() + wantBodyReplace := bodyMutation != nil || forceBodyMutation || mutatorHasMutations + + if wantBodyReplace { + // Apply body mutations from the route and also restore original body on retry. + bodyMutation = applyBodyMutation(u.bodyMutator, bodyMutation, u.parent.originalRequestBodyRaw, u.logger) + } // Ensure bodyMutation is not nil for subsequent processing if bodyMutation == nil { @@ -392,6 +403,22 @@ func (u *upstreamProcessor[ReqT, RespT, RespChunkT, EndpointSpecT]) ProcessReque } } + if !wantBodyReplace { + // No body change -> no content-length restamp; emit CONTINUE so Envoy + // keeps whatever body the previous filter in the chain produced. + return &extprocv3.ProcessingResponse{ + Response: &extprocv3.ProcessingResponse_RequestHeaders{ + RequestHeaders: &extprocv3.HeadersResponse{ + Response: &extprocv3.CommonResponse{ + HeaderMutation: headerMutation, + Status: extprocv3.CommonResponse_CONTINUE, + }, + }, + }, + DynamicMetadata: buildRequestHeaderDynamicMetadata(u.requestHeaders), + }, nil + } + var dm *structpb.Struct if bm := bodyMutation.GetBody(); bm != nil { dm = buildContentLengthDynamicMetadataOnRequest(len(bm)) diff --git a/internal/extproc/processor_impl_test.go b/internal/extproc/processor_impl_test.go index 719d5aae53..8eaccd65dc 100644 --- a/internal/extproc/processor_impl_test.go +++ b/internal/extproc/processor_impl_test.go @@ -26,6 +26,7 @@ import ( anthropicschema "github.com/envoyproxy/ai-gateway/internal/apischema/anthropic" "github.com/envoyproxy/ai-gateway/internal/apischema/openai" + "github.com/envoyproxy/ai-gateway/internal/bodymutator" "github.com/envoyproxy/ai-gateway/internal/endpointspec" "github.com/envoyproxy/ai-gateway/internal/filterapi" "github.com/envoyproxy/ai-gateway/internal/headermutator" @@ -744,6 +745,127 @@ func Test_messagesProcessorUpstreamFilter_ProcessRequestHeaders_AWSAnthropicBeta require.Equal(t, []any{"interleaved-thinking-2025-05-14", "context-1m-2025-08-07"}, betaValues) } +// Test_chatCompletionProcessorUpstreamFilter_ProcessRequestHeaders_BodyReplaceContract +// locks the contract for when the upstream filter must NOT replace the request +// body: when the translator returns no body, no backend HTTPBodyMutation is +// configured, and forceBodyMutation is false, the upstream filter must emit +// CONTINUE rather than CONTINUE_AND_REPLACE. Issuing CONTINUE_AND_REPLACE with +// the captured original body would clobber any body mutation applied by an +// earlier ext_proc filter in the chain. Header mutations and auth headers +// must still apply on the CONTINUE branch. +// +// The sibling subtest pins the opposite half of the contract: when the +// translator DID emit a body, the upstream filter must continue to issue +// CONTINUE_AND_REPLACE, so a future refactor cannot quietly flip the contract +// back. +func Test_chatCompletionProcessorUpstreamFilter_ProcessRequestHeaders_BodyReplaceContract(t *testing.T) { + t.Run("no translator body, no mutator, no force -> CONTINUE", func(t *testing.T) { + someBody := bodyFromModel(t, "some-model", false, nil) + headers := map[string]string{ + ":path": "/foo", + internalapi.ModelNameHeaderKeyDefault: "some-model", + } + var expBody openai.ChatCompletionRequest + require.NoError(t, json.Unmarshal(someBody, &expBody)) + + pathRewrite := []internalapi.Header{{":path", "/v1/chat/completions"}} + mt := &mockTranslator{ + t: t, + expRequestBody: &expBody, + retHeaderMutation: pathRewrite, + retBodyMutation: nil, + expForceRequestBodyMutation: false, + } + mm := &mockMetrics{} + p := &chatCompletionProcessorUpstreamFilter{ + parent: &chatCompletionProcessorRouterFilter{ + config: &filterapi.RuntimeConfig{}, + logger: slog.Default(), + originalRequestBodyRaw: someBody, + originalRequestBody: &expBody, + originalModel: "some-model", + stream: false, + forceBodyMutation: false, + }, + requestHeaders: headers, + metrics: mm, + translator: mt, + handler: &mockBackendAuthHandler{}, + // No-config body mutator: HasMutations() returns false. This mirrors + // SetBackend's call to bodymutator.NewBodyMutator(nil, ...) when the + // route has no HTTPBodyMutation. + bodyMutator: bodymutator.NewBodyMutator(nil, someBody), + } + + resp, err := p.ProcessRequestHeaders(t.Context(), nil) + require.NoError(t, err) + require.NotNil(t, resp) + + commonRes := resp.Response.(*extprocv3.ProcessingResponse_RequestHeaders).RequestHeaders.Response + require.Equal(t, extprocv3.CommonResponse_CONTINUE, commonRes.Status, + "must NOT issue CONTINUE_AND_REPLACE when nothing actually needs to mutate the body — that path silently replays the original body and clobbers earlier filters' mutations") + require.Nil(t, commonRes.BodyMutation, "no body mutation should ride on a CONTINUE response") + + require.NotNil(t, commonRes.HeaderMutation) + require.Len(t, commonRes.HeaderMutation.SetHeaders, 2, + "header mutations from the translator (path rewrite) and the auth handler must still apply on the CONTINUE branch") + require.Equal(t, ":path", commonRes.HeaderMutation.SetHeaders[0].Header.Key) + require.Equal(t, []byte("/v1/chat/completions"), commonRes.HeaderMutation.SetHeaders[0].Header.RawValue) + require.Equal(t, "foo", commonRes.HeaderMutation.SetHeaders[1].Header.Key) + require.Equal(t, "mock-auth-handler", string(commonRes.HeaderMutation.SetHeaders[1].Header.RawValue)) + + // No body change -> no content-length restamp. + // buildRequestHeaderDynamicMetadata returns nil when LogRequestHeaderAttributes is empty. + require.Nil(t, resp.DynamicMetadata, + "buildContentLengthDynamicMetadataOnRequest must not be called when the body is not replaced") + }) + + t.Run("translator body present -> CONTINUE_AND_REPLACE", func(t *testing.T) { + someBody := bodyFromModel(t, "some-model", false, nil) + headers := map[string]string{ + ":path": "/foo", + internalapi.ModelNameHeaderKeyDefault: "some-model", + } + var expBody openai.ChatCompletionRequest + require.NoError(t, json.Unmarshal(someBody, &expBody)) + + bodyMut := []byte("translator-emitted body") + mt := &mockTranslator{ + t: t, + expRequestBody: &expBody, + retHeaderMutation: []internalapi.Header{{":path", "/v1/chat/completions"}}, + retBodyMutation: bodyMut, + expForceRequestBodyMutation: false, + } + mm := &mockMetrics{} + p := &chatCompletionProcessorUpstreamFilter{ + parent: &chatCompletionProcessorRouterFilter{ + config: &filterapi.RuntimeConfig{}, + logger: slog.Default(), + originalRequestBodyRaw: someBody, + originalRequestBody: &expBody, + originalModel: "some-model", + stream: false, + forceBodyMutation: false, + }, + requestHeaders: headers, + metrics: mm, + translator: mt, + handler: &mockBackendAuthHandler{}, + bodyMutator: bodymutator.NewBodyMutator(nil, someBody), + } + + resp, err := p.ProcessRequestHeaders(t.Context(), nil) + require.NoError(t, err) + require.NotNil(t, resp) + + commonRes := resp.Response.(*extprocv3.ProcessingResponse_RequestHeaders).RequestHeaders.Response + require.Equal(t, extprocv3.CommonResponse_CONTINUE_AND_REPLACE, commonRes.Status, + "existing CONTINUE_AND_REPLACE behavior must be preserved when the translator produced a body") + require.Equal(t, bodyMut, commonRes.BodyMutation.GetBody()) + }) +} + func Test_chatCompletionProcessorUpstreamFilter_MergeWithTokenLatencyMetadata(t *testing.T) { t.Run("empty metadata", func(t *testing.T) { mm := &mockMetrics{} From 61b4cc81564959b1750331ec0b1cfdcb4f003ce4 Mon Sep 17 00:00:00 2001 From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:07:14 +0100 Subject: [PATCH 21/59] fix: insert AI Gateway extproc after the buffer filter (#2227) **Description** **Problem** If a buffer filter (`envoy.filters.http.buffer`) is added to the chain, for example via an EnvoyPatchPolicy to raise the request buffer limit, the AI Gateway extproc could be placed ahead of it. The AI Gateway extproc runs with `RequestBodyMode: BUFFERED`, so when it runs before the buffer filter it buffers the body against Envoy Gateway's default 32 KiB downstream per-connection limit and returns `413 Payload Too Large` for larger bodies before the buffer filter's higher limit applies. The buffer filter raises the stream's buffer limit to `max_request_bytes`, but only for filters that decode after it. This was hit in practice with large request bodies such as Gemini CLI tool definitions. **Solution** When a buffer filter sits at or after the computed ext_proc insertion point, insert the AI Gateway extproc immediately after the last buffer filter. Envoy Gateway already orders the buffer filter before ext_proc, so this honors that order. Behavior is unchanged when no buffer filter is present. This works because EnvoyPatchPolicy patches are applied before the extension hook runs and Envoy Gateway does not re-sort filters afterward, so the buffer filter is visible to this code. The extension server only moves its own filter, not the user's buffer filter, so this fixes the 413 for AI Gateway's extproc specifically and not for other co-resident BUFFERED ext_procs. **Testing** Added table tests for three cases: ext_proc before buffer (inserted after buffer), buffer before ext_proc (inserted after buffer), and no buffer filter (unchanged). Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Co-authored-by: Ignasi Barrera --- .../extensionserver/post_translate_modify.go | 30 ++++++++++++- .../post_translate_modify_test.go | 42 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/internal/extensionserver/post_translate_modify.go b/internal/extensionserver/post_translate_modify.go index 31edc9b1e0..694ff060f0 100644 --- a/internal/extensionserver/post_translate_modify.go +++ b/internal/extensionserver/post_translate_modify.go @@ -854,9 +854,20 @@ func shouldAIGatewayExtProcBeInserted(filters []*httpconnectionmanagerv3.HttpFil // insertAIGatewayExtProcFilter inserts the AI Gateway extproc filter into the HTTP connection manager. // -// The order is simple: make sure that the AI Gateway extproc filter is the very first extproc filter in the standard -// Envoy Gateway order. See: +// The order is mostly simple: make sure that the AI Gateway extproc filter is the first extproc filter in the +// standard Envoy Gateway order. See: // https://github.com/envoyproxy/gateway/blob/f1e6dab770fabc70d175237380eedfc1f9b1a9e5/internal/xds/translator/httpfilters.go#L93 +// +// There is one exception: the HTTP buffer filter (envoy.filters.http.buffer). In Envoy Gateway's canonical +// filter order the buffer filter is placed *before* the ext_proc filters, so a user who raises the request +// buffer limit via the buffer filter (e.g. through an EnvoyPatchPolicy) expects that limit to apply to the AI +// Gateway extproc as well. The AI Gateway extproc runs with RequestBodyMode: BUFFERED, so inserting it ahead of +// the buffer filter would make it buffer the request body against the (smaller) default per-connection buffer +// limit and reject large request bodies with a 413 before the buffer filter's larger limit can take effect. To +// avoid this, when a buffer filter sits at or after the chosen insertion point we insert the AI Gateway extproc +// immediately after the last buffer filter instead. The AI Gateway extproc is then the first ext_proc filter to +// run after the buffer filter (not necessarily the first ext_proc filter overall), so a user-supplied ext_proc +// placed ahead of the buffer filter, e.g. api-key auth, still runs before it. func insertAIGatewayExtProcFilter(mgr *httpconnectionmanagerv3.HttpConnectionManager, filter *httpconnectionmanagerv3.HttpFilter) error { insertIndex := -1 outer: @@ -871,6 +882,21 @@ outer: if insertIndex == -1 { return errors.New("failed to find insertion point for AI Gateway extproc filter") } + + // Find the last buffer filter so we can insert the AI Gateway extproc after it (see the buffer-filter + // exception in the function doc above). + lastBufferIndex := -1 + for i, existingFilter := range mgr.HttpFilters { + if strings.HasPrefix(existingFilter.Name, egv1a1.EnvoyFilterBuffer.String()) { + lastBufferIndex = i + } + } + // When the buffer filter sits at or after the ext_proc insertion point, insert the AI Gateway extproc + // immediately after it instead. Behavior is unchanged when no buffer filter exists (lastBufferIndex == -1). + if lastBufferIndex >= insertIndex { + insertIndex = lastBufferIndex + 1 + } + mgr.HttpFilters = append(mgr.HttpFilters, filter) copy(mgr.HttpFilters[insertIndex+1:], mgr.HttpFilters[insertIndex:]) mgr.HttpFilters[insertIndex] = filter diff --git a/internal/extensionserver/post_translate_modify_test.go b/internal/extensionserver/post_translate_modify_test.go index f7babc8de4..78e71f2a8d 100644 --- a/internal/extensionserver/post_translate_modify_test.go +++ b/internal/extensionserver/post_translate_modify_test.go @@ -168,6 +168,48 @@ func TestInsertAIGatewayExtProcFilter(t *testing.T) { expectedPosition: 2, expectedFilterCount: 6, }, + { + // Mirrors the EKS setup where an api-key ext_proc and a buffer filter are added ahead of AI + // Gateway. The ext_proc at index 0 matches afterExtProcFilterPrefixes, but the buffer filter + // must still run first so its larger request buffer limit applies to AI Gateway's BUFFERED + // extproc. AI Gateway is inserted after the buffer filter (position 2). + name: "insert after buffer when ext_proc precedes buffer", + existingFilters: []*httpconnectionmanagerv3.HttpFilter{ + {Name: "envoy.filters.http.ext_proc.apikey"}, + {Name: "envoy.filters.http.buffer"}, + {Name: "envoy.filters.http.jwt_authn"}, + {Name: "envoy.filters.http.rbac"}, + {Name: "envoy.filters.http.router"}, + }, + expectedPosition: 2, + expectedFilterCount: 6, + }, + { + // When the buffer filter already precedes the first ext_proc filter, AI Gateway is inserted + // right after the buffer filter (position 1), preserving Envoy Gateway's buffer-before-extproc + // ordering. + name: "insert after buffer when buffer precedes ext_proc", + existingFilters: []*httpconnectionmanagerv3.HttpFilter{ + {Name: "envoy.filters.http.buffer"}, + {Name: "envoy.filters.http.ext_proc.apikey"}, + {Name: "envoy.filters.http.rbac"}, + {Name: "envoy.filters.http.router"}, + }, + expectedPosition: 1, + expectedFilterCount: 5, + }, + { + // Regression guard: with no buffer filter present, insertion behavior is unchanged and AI + // Gateway lands ahead of the first ext_proc filter (position 0). + name: "no buffer filter leaves ext_proc insertion unchanged", + existingFilters: []*httpconnectionmanagerv3.HttpFilter{ + {Name: "envoy.filters.http.ext_proc.apikey"}, + {Name: "envoy.filters.http.rbac"}, + {Name: "envoy.filters.http.router"}, + }, + expectedPosition: 0, + expectedFilterCount: 4, + }, } for _, tt := range tests { From c23fc4bc0ae5146c90a926ef4965e0f11e2a6de6 Mon Sep 17 00:00:00 2001 From: hustxiayang Date: Fri, 12 Jun 2026 15:05:43 -0400 Subject: [PATCH 22/59] fix: allow strict function definition in anthropic (#2232) --- internal/translator/anthropic_helper.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/translator/anthropic_helper.go b/internal/translator/anthropic_helper.go index 649e5a9692..2e919789ee 100644 --- a/internal/translator/anthropic_helper.go +++ b/internal/translator/anthropic_helper.go @@ -140,6 +140,10 @@ func translateOpenAItoAnthropicTools(openAITools []openai.Tool, openAIToolChoice Description: anthropic.String(openAITool.Function.Description), } + if openAITool.Function.Strict { + toolParam.Strict = anthropic.Bool(true) + } + if isCacheEnabled(openAITool.Function.AnthropicContentFields) { toolParam.CacheControl = anthropic.NewCacheControlEphemeralParam() } From 2e23b28b7e21a2c2605d3380cd3977d64875878a Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Sun, 14 Jun 2026 07:41:51 -0700 Subject: [PATCH 23/59] docs: update release information to v0.7 (#2233) **Description** This updates the homepage info Signed-off-by: Takeshi Yoneda --- site/src/components/HomepageFeatures/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/components/HomepageFeatures/index.tsx b/site/src/components/HomepageFeatures/index.tsx index 57b57f8407..4cb996d3ac 100644 --- a/site/src/components/HomepageFeatures/index.tsx +++ b/site/src/components/HomepageFeatures/index.tsx @@ -19,11 +19,11 @@ const FeatureList: FeatureItem[] = [ ), }, { - title: 'v0.5 Release now available', + title: 'v0.7 Release now available', image: require('@site/static/img/3.png').default, description: ( <> - The v0.5 Release of Envoy AI Gateway is now available. See the release notes for more information. + The v0.7 Release of Envoy AI Gateway is now available. See the release notes for more information. ), }, From 625167e59b7a49c23dc1eeaed998a4bd8634d4b0 Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Tue, 16 Jun 2026 16:16:49 +0200 Subject: [PATCH 24/59] Merge commit from fork Signed-off-by: Ignasi Barrera --- internal/extproc/processor_impl.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/extproc/processor_impl.go b/internal/extproc/processor_impl.go index abb6ac956b..733b2ef974 100644 --- a/internal/extproc/processor_impl.go +++ b/internal/extproc/processor_impl.go @@ -267,12 +267,10 @@ func (r *routerProcessor[ReqT, RespT, RespChunkT, EndpointSpecT]) ProcessRequest Header: &corev3.HeaderValue{Key: internalapi.ModelNameHeaderKeyDefault, RawValue: []byte(originalModel)}, }) originalPath := r.requestHeaders[":path"] - if r.requestHeaders[originalPathHeader] == "" { - r.requestHeaders[originalPathHeader] = originalPath - additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{ - Header: &corev3.HeaderValue{Key: originalPathHeader, RawValue: []byte(originalPath)}, - }) - } + r.requestHeaders[originalPathHeader] = originalPath + additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{ + Header: &corev3.HeaderValue{Key: originalPathHeader, RawValue: []byte(originalPath)}, + }) if r.requestHeaders[internalapi.EnvoyOriginalPathHeader] == "" { r.requestHeaders[internalapi.EnvoyOriginalPathHeader] = originalPath additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{ From 78391c6d7531236085664bd278f827171ce3f98b Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Tue, 16 Jun 2026 16:44:34 +0200 Subject: [PATCH 25/59] mcp: limit request body size by default (#2238) **Description** Limit by default the size of MCP request payloads. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** N/A Signed-off-by: Ignasi Barrera --- internal/mcpproxy/config.go | 1 + internal/mcpproxy/handlers.go | 12 +++++++++++- internal/mcpproxy/handlers_test.go | 14 ++++++++++++++ internal/mcpproxy/mcpproxy.go | 17 +++++++++++++++++ internal/mcpproxy/mcpproxy_test.go | 19 +++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/internal/mcpproxy/config.go b/internal/mcpproxy/config.go index 777e6a2442..6a231c7ab2 100644 --- a/internal/mcpproxy/config.go +++ b/internal/mcpproxy/config.go @@ -31,6 +31,7 @@ type ( tracer tracingapi.MCPTracer client http.Client logRequestHeaderAttributes map[string]string + maxRequestBodySize int64 // maximum allowed POST body size in bytes } mcpProxyConfig struct { diff --git a/internal/mcpproxy/handlers.go b/internal/mcpproxy/handlers.go index 7d5dce7862..74dd528d4e 100644 --- a/internal/mcpproxy/handlers.go +++ b/internal/mcpproxy/handlers.go @@ -276,8 +276,18 @@ func (m *mcpRequestContext) servePOST(w http.ResponseWriter, r *http.Request) { } } - body, err := io.ReadAll(r.Body) + limit := m.maxRequestBodySize + if limit <= 0 { + limit = defaultMaxRequestBodySize + } + body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, limit)) if err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + errType = metrics.MCPErrorInternal + onErrorResponse(w, http.StatusRequestEntityTooLarge, "request body too large") + return + } errType = metrics.MCPErrorInternal onErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/internal/mcpproxy/handlers_test.go b/internal/mcpproxy/handlers_test.go index a20afa5b61..8b6a214d8c 100644 --- a/internal/mcpproxy/handlers_test.go +++ b/internal/mcpproxy/handlers_test.go @@ -168,6 +168,20 @@ func TestServePOST_InvalidJSONRPC(t *testing.T) { require.Contains(t, rr.Body.String(), "invalid JSON-RPC message") } +func TestServePOST_OversizedBody(t *testing.T) { + proxy := newTestMCPProxy() + proxy.maxRequestBodySize = 16 // tiny limit to exercise the guard without allocating a large buffer. + + body := strings.NewReader(`{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}`) // > 16 bytes + req := httptest.NewRequest(http.MethodPost, "/mcp", body) + rr := httptest.NewRecorder() + + proxy.servePOST(rr, req) + + require.Equal(t, http.StatusRequestEntityTooLarge, rr.Code) + require.Contains(t, rr.Body.String(), "request body too large") +} + func TestServePOST_InvalidSessionID(t *testing.T) { proxy := newTestMCPProxy() req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(`{"jsonrpc":"2.0","method":"tools/call","params":{"name":"test-tool"},"id":"1"}`)) diff --git a/internal/mcpproxy/mcpproxy.go b/internal/mcpproxy/mcpproxy.go index 354f233638..ea8c1ef733 100644 --- a/internal/mcpproxy/mcpproxy.go +++ b/internal/mcpproxy/mcpproxy.go @@ -14,6 +14,8 @@ import ( "log/slog" "maps" "net/http" + "os" + "strconv" "strings" "sync" "time" @@ -39,6 +41,20 @@ type mcpRequestContext struct { perBackendMetricsRecorded bool } +// defaultMaxRequestBodySize is the default maximum allowed POST body size in bytes (4 MiB). +const defaultMaxRequestBodySize = 4 * 1024 * 1024 + +// getMaxRequestBodySize returns the configured POST body limit from the environment variable, +// falling back to 4 MiB if the variable is unset or invalid. +func getMaxRequestBodySize() int64 { + if v, ok := os.LookupEnv("MCP_PROXY_MAX_REQUEST_BODY_SIZE"); ok { + if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 { + return n + } + } + return defaultMaxRequestBodySize +} + // NewMCPProxy creates a new MCPProxy instance. func NewMCPProxy(l *slog.Logger, mcpMetrics metrics.MCPMetrics, tracer tracingapi.MCPTracer, sessionCrypto SessionCrypto, logRequestHeaderAttributes map[string]string) (*ProxyConfig, *http.ServeMux, error) { toolChangeSignaler := newMultiWatcherSignaler() // used to signal changes to all active sessions. @@ -49,6 +65,7 @@ func NewMCPProxy(l *slog.Logger, mcpMetrics metrics.MCPMetrics, tracer tracingap l: l, client: http.Client{}, // No timeout as it's enforced at Envoy level. logRequestHeaderAttributes: maps.Clone(logRequestHeaderAttributes), + maxRequestBodySize: getMaxRequestBodySize(), } mux := http.NewServeMux() mux.HandleFunc( diff --git a/internal/mcpproxy/mcpproxy_test.go b/internal/mcpproxy/mcpproxy_test.go index 966b768eff..572ec779c9 100644 --- a/internal/mcpproxy/mcpproxy_test.go +++ b/internal/mcpproxy/mcpproxy_test.go @@ -60,6 +60,25 @@ func (f *fakeTracer) StartSpanAndInjectMeta(context.Context, *jsonrpc.Request, m var noopTracer = tracingapi.NoopMCPTracer{} +func TestGetMaxRequestBodySize(t *testing.T) { + for _, tc := range []struct { + name string + envValue string + want int64 + }{ + {name: "default when env var not set", envValue: "", want: defaultMaxRequestBodySize}, + {name: "custom value from env var", envValue: "1048576", want: 1048576}, + {name: "default when env var is not a number", envValue: "not-a-number", want: defaultMaxRequestBodySize}, + {name: "default when env var is zero", envValue: "0", want: defaultMaxRequestBodySize}, + {name: "default when env var is negative", envValue: "-1", want: defaultMaxRequestBodySize}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("MCP_PROXY_MAX_REQUEST_BODY_SIZE", tc.envValue) + require.Equal(t, tc.want, getMaxRequestBodySize()) + }) + } +} + func TestNewMCPProxy(t *testing.T) { l := slog.Default() proxy, mux, err := NewMCPProxy(l, stubMetrics{}, noopTracer, NewPBKDF2AesGcmSessionCrypto("test", 100), nil) From 2cc24178b70b66e4cb97393a20950a230af31149 Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Wed, 17 Jun 2026 17:06:44 +0200 Subject: [PATCH 26/59] chore: bind pprof to localhost (#2243) **Description** Bind pprof to localhost so it is only accessible by default from the local pod or via kubectl port-forward. Since it is for debugging purposes, it does not need to listen on `0.0.0.0`. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** N/A Signed-off-by: Ignasi Barrera --- internal/pprof/pprof.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go index 1c1054534a..26d61fbad2 100644 --- a/internal/pprof/pprof.go +++ b/internal/pprof/pprof.go @@ -40,7 +40,7 @@ func run(ctx context.Context, l *log.Logger) { mux.HandleFunc("/debug/pprof/profile", pprof.Profile) mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) mux.HandleFunc("/debug/pprof/trace", pprof.Trace) - server := &http.Server{Addr: ":" + pprofPort, Handler: mux, ReadHeaderTimeout: 5 * time.Second} + server := &http.Server{Addr: "127.0.0.1:" + pprofPort, Handler: mux, ReadHeaderTimeout: 5 * time.Second} go func() { l.Printf("starting pprof server on port %s", pprofPort) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { From 4ae1ec4d578d648e6b5cc290c575c8f51594830c Mon Sep 17 00:00:00 2001 From: hustxiayang Date: Thu, 18 Jun 2026 00:36:38 -0400 Subject: [PATCH 27/59] fix: double counting for the usage in stream mode for anthropic models (#2239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** `ExtractTokenUsageFromExplicitCaching`  is called in both start message and the delta message, as a result. The usage report is not correct. Fix: remove the accumulation in the message delta; also the previous tests can pass because the test cases did not mock the real cases, update all related tests cases as well. --------- Signed-off-by: yxia216 Signed-off-by: Aaron Choo Signed-off-by: Anurag Aggarwal Signed-off-by: Aishwarya Signed-off-by: aishwaryaraimule21 Signed-off-by: dependabot[bot] Signed-off-by: Hritik003 Signed-off-by: Linus Schlumberger Signed-off-by: Ignasi Barrera Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Signed-off-by: Takeshi Yoneda Signed-off-by: hustxiayang Signed-off-by: hustxiayang Co-authored-by: Aaron Choo Co-authored-by: Anurag Aggarwal Co-authored-by: aishwaryaraimule21 Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ignasi Barrera Co-authored-by: Hritik Raj Co-authored-by: Linus Schlumberger Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Co-authored-by: Takeshi Yoneda Co-authored-by: Ignasi Barrera --- internal/translator/anthropic_helper.go | 15 +- internal/translator/anthropic_helper_test.go | 148 +++++++++++++++++++ tests/data-plane/testupstream_test.go | 22 +-- 3 files changed, 161 insertions(+), 24 deletions(-) diff --git a/internal/translator/anthropic_helper.go b/internal/translator/anthropic_helper.go index 2e919789ee..a852d1a678 100644 --- a/internal/translator/anthropic_helper.go +++ b/internal/translator/anthropic_helper.go @@ -966,8 +966,8 @@ func (p *anthropicStreamParser) handleAnthropicStreamEvent(eventType []byte, dat &u.CacheReadInputTokens, &u.CacheCreationInputTokens, ) - // For message_start, we store the initial usage but don't add to the accumulated - // The message_delta event will contain the final totals + // Set all input token counts (input, cache read, cache creation) from message_start. + // message_delta may also contain these fields but only output_tokens is used from it. if input, ok := usage.InputTokens(); ok { p.tokenUsage.SetInputTokens(input) } @@ -1061,17 +1061,6 @@ func (p *anthropicStreamParser) handleAnthropicStreamEvent(eventType []byte, dat if output, ok := usage.OutputTokens(); ok { p.tokenUsage.AddOutputTokens(output) } - // Update input tokens to include any cache tokens from delta - if cached, ok := usage.CachedInputTokens(); ok { - p.tokenUsage.AddInputTokens(cached) - // Accumulate any additional cache tokens from delta - p.tokenUsage.AddCachedInputTokens(cached) - } - if cacheCreation, ok := usage.CacheCreationInputTokens(); ok { - p.tokenUsage.AddInputTokens(cacheCreation) - // Accumulate cache creation tokens - p.tokenUsage.AddCacheCreationInputTokens(cacheCreation) - } p.tokenUsage.SetReasoningTokens(uint32(u.OutputTokensDetails.ThinkingTokens)) //nolint:gosec if event.Delta.StopReason != "" { p.stopReason = event.Delta.StopReason diff --git a/internal/translator/anthropic_helper_test.go b/internal/translator/anthropic_helper_test.go index eb39929731..c395094854 100644 --- a/internal/translator/anthropic_helper_test.go +++ b/internal/translator/anthropic_helper_test.go @@ -7,15 +7,18 @@ package translator import ( "fmt" + "strings" "testing" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/shared/constant" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/utils/ptr" "github.com/envoyproxy/ai-gateway/internal/apischema/openai" "github.com/envoyproxy/ai-gateway/internal/internalapi" + "github.com/envoyproxy/ai-gateway/internal/metrics" ) // mockErrorReader is a helper for testing io.Reader failures. @@ -1157,3 +1160,148 @@ func TestBuildAnthropicParamsWithReasoningEffort(t *testing.T) { require.Equal(t, anthropic.OutputConfigEffort(""), params.OutputConfig.Effort) }) } + +func TestAnthropicStreamParser_StreamingTokenUsage(t *testing.T) { + tests := []struct { + name string + events string + expectedInputTokens uint32 + expectedOutputTokens uint32 + expectedTotalTokens uint32 + expectedCachedTokens uint32 + expectedCacheCreationTokens uint32 + }{ + { + name: "with cache tokens", + events: `event: message_start +data: {"type": "message_start", "message": {"id": "msg_abc123", "type": "message", "role": "assistant", "content": [], "model": "claude-sonnet-4-6", "usage": {"input_tokens": 678, "cache_read_input_tokens": 13363, "cache_creation_input_tokens": 0, "output_tokens": 1}}} + +event: content_block_start +data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} + +event: content_block_delta +data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hi"}} + +event: content_block_stop +data: {"type": "content_block_stop", "index": 0} + +event: message_delta +data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 678, "cache_read_input_tokens": 13363, "cache_creation_input_tokens": 0, "output_tokens": 5}} + +event: message_stop +data: {"type": "message_stop"} + +`, + expectedInputTokens: 14041, // 678 + 13363 + 0 + expectedOutputTokens: 5, + expectedTotalTokens: 14046, // 14041 + 5 + expectedCachedTokens: 13363, + expectedCacheCreationTokens: 0, + }, + { + name: "without cache tokens", + events: `event: message_start +data: {"type": "message_start", "message": {"id": "msg_abc456", "type": "message", "role": "assistant", "content": [], "model": "claude-sonnet-4-6", "usage": {"input_tokens": 100, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "output_tokens": 1}}} + +event: content_block_start +data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} + +event: content_block_delta +data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello"}} + +event: content_block_stop +data: {"type": "content_block_stop", "index": 0} + +event: message_delta +data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 100, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "output_tokens": 10}} + +event: message_stop +data: {"type": "message_stop"} + +`, + expectedInputTokens: 100, + expectedOutputTokens: 10, + expectedTotalTokens: 110, + expectedCachedTokens: 0, + expectedCacheCreationTokens: 0, + }, + { + name: "with cache creation tokens", + events: `event: message_start +data: {"type": "message_start", "message": {"id": "msg_abc789", "type": "message", "role": "assistant", "content": [], "model": "claude-sonnet-4-6", "usage": {"input_tokens": 200, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 5000, "output_tokens": 1}}} + +event: content_block_start +data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} + +event: content_block_delta +data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Response"}} + +event: content_block_stop +data: {"type": "content_block_stop", "index": 0} + +event: message_delta +data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 200, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 5000, "output_tokens": 8}} + +event: message_stop +data: {"type": "message_stop"} + +`, + expectedInputTokens: 5200, // 200 + 5000 + 0 + expectedOutputTokens: 8, + expectedTotalTokens: 5208, // 5200 + 8 + expectedCachedTokens: 0, + expectedCacheCreationTokens: 5000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := newAnthropicStreamParser("claude-sonnet-4-6") + + // Feed each event block separately (simulating chunked SSE delivery), + // with the last chunk marked as endOfStream. + chunks := splitSSEEvents(tt.events) + var tokenUsage metrics.TokenUsage + for i, chunk := range chunks { + endOfStream := i == len(chunks)-1 + _, _, usage, _, err := parser.Process(strings.NewReader(chunk), endOfStream, nil) + require.NoError(t, err) + if endOfStream { + tokenUsage = usage + } + } + + inputTokens, inputSet := tokenUsage.InputTokens() + assert.True(t, inputSet, "InputTokens should be set") + assert.Equal(t, tt.expectedInputTokens, inputTokens, "InputTokens mismatch") + + outputTokens, outputSet := tokenUsage.OutputTokens() + assert.True(t, outputSet, "OutputTokens should be set") + assert.Equal(t, tt.expectedOutputTokens, outputTokens, "OutputTokens mismatch") + + totalTokens, totalSet := tokenUsage.TotalTokens() + assert.True(t, totalSet, "TotalTokens should be set") + assert.Equal(t, tt.expectedTotalTokens, totalTokens, "TotalTokens mismatch") + + cachedTokens, cachedSet := tokenUsage.CachedInputTokens() + assert.True(t, cachedSet, "CachedInputTokens should be set") + assert.Equal(t, tt.expectedCachedTokens, cachedTokens, "CachedInputTokens mismatch") + + cacheCreation, cacheCreationSet := tokenUsage.CacheCreationInputTokens() + assert.True(t, cacheCreationSet, "CacheCreationInputTokens should be set") + assert.Equal(t, tt.expectedCacheCreationTokens, cacheCreation, "CacheCreationInputTokens mismatch") + }) + } +} + +func splitSSEEvents(data string) []string { + parts := strings.Split(data, "\n\n") + var events []string + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + events = append(events, p+"\n\n") + } + } + return events +} diff --git a/tests/data-plane/testupstream_test.go b/tests/data-plane/testupstream_test.go index 19bf7d503f..c3801e27a4 100644 --- a/tests/data-plane/testupstream_test.go +++ b/tests/data-plane/testupstream_test.go @@ -551,7 +551,7 @@ data: [DONE] expRequestHeaders: map[string]string{"Authorization": "Bearer " + fakeGCPAuthToken}, responseStatus: strconv.Itoa(http.StatusOK), responseBody: `event: message_start -data: {"type": "message_start", "message": {"id": "msg_123", "usage": {"input_tokens": 15}}} +data: {"type": "message_start", "message": {"id": "msg_123", "usage": {"input_tokens": 15, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 10, "output_tokens": 1}}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} @@ -566,7 +566,7 @@ event: content_block_stop data: {"type": "content_block_stop", "index": 0} event: message_delta -data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 12, "cache_read_input_tokens":10}} +data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 15, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 10, "output_tokens": 12}} event: message_stop data: {"type": "message_stop"} @@ -614,7 +614,7 @@ data: [DONE] expRequestHeaders: map[string]string{"Authorization": "Bearer " + fakeGCPAuthToken}, responseStatus: strconv.Itoa(http.StatusOK), responseBody: `event: message_start -data: {"type": "message_start", "message": {"id": "msg_123", "usage": {"input_tokens": 50}}} +data: {"type": "message_start", "message": {"id": "msg_123", "usage": {"input_tokens": 50, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1}}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_abc123", "name": "get_weather", "input": {}}} @@ -629,7 +629,7 @@ event: content_block_stop data: {"type": "content_block_stop", "index": 0} event: message_delta -data: {"type": "message_delta", "delta": {"stop_reason": "tool_use"}, "usage": {"output_tokens": 20}} +data: {"type": "message_delta", "delta": {"stop_reason": "tool_use"}, "usage": {"input_tokens": 50, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 20}} event: message_stop data: {"type": "message_stop"}`, @@ -910,7 +910,7 @@ data: [DONE] expRequestHeaders: map[string]string{"Authorization": "Bearer " + fakeGCPAuthToken}, responseStatus: strconv.Itoa(http.StatusOK), responseBody: `event: message_start -data: {"type": "message_start", "message": {"id": "msg_789", "usage": {"input_tokens": 8}}} +data: {"type": "message_start", "message": {"id": "msg_789", "usage": {"input_tokens": 8, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1}}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} @@ -922,7 +922,7 @@ event: content_block_stop data: {"type": "content_block_stop", "index": 0} event: message_delta -data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 15}} +data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 8, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 15}} event: message_stop data: {"type": "message_stop"} @@ -930,7 +930,7 @@ data: {"type": "message_stop"} `, expStatus: http.StatusOK, expResponseBody: `event: message_start -data: {"type": "message_start", "message": {"id": "msg_789", "usage": {"input_tokens": 8}}} +data: {"type": "message_start", "message": {"id": "msg_789", "usage": {"input_tokens": 8, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1}}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} @@ -942,7 +942,7 @@ event: content_block_stop data: {"type": "content_block_stop", "index": 0} event: message_delta -data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 15}} +data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 8, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 15}} event: message_stop data: {"type": "message_stop"} @@ -1052,7 +1052,7 @@ event: content_block_stop data: {"type":"content_block_stop","index":0} event: message_delta -data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":9,"output_tokens":10}} +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":9,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}} event: message_stop data: {"type":"message_stop"} @@ -1090,7 +1090,7 @@ data: {"type":"message_stop"} {"bytes":"eyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiLwn5GLIEhvdyJ9fQ==","p":"abcdefghijklmnopqrstuvwxyzABCDEFG"} {"bytes":"eyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgYXJlIHlvdSBkb2luZyB0b2RheT8ifX0=","p":"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234"} {"bytes":"eyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjB9","p":"abcdefghijklmnopqrstuvwxyz"} -{"bytes":"eyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsfSwidXNhZ2UiOnsib3V0cHV0X3Rva2VucyI6MTV9fQ==","p":"abcdefghijklmnopqrstu"} +{"bytes":"eyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxMCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2VucyI6MTV9fQ==","p":"abcdefghijklmnopqrstu"} {"bytes":"eyJ0eXBlIjoibWVzc2FnZV9zdG9wIiwiYW1hem9uLWJlZHJvY2staW52b2NhdGlvbk1ldHJpY3MiOnsiaW5wdXRUb2tlbkNvdW50IjoxMCwib3V0cHV0VG9rZW5Db3VudCI6MTUsImludm9jYXRpb25MYXRlbmN5IjoxNzk4LCJmaXJzdEJ5dGVMYXRlbmN5IjoxNTA3fX0=","p":"ab"} `, expStatus: http.StatusOK, @@ -1119,7 +1119,7 @@ event: content_block_stop data: {"type":"content_block_stop","index":0} event: message_delta -data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":15}} +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":15}} event: message_stop data: {"type":"message_stop","amazon-bedrock-invocationMetrics":{"inputTokenCount":10,"outputTokenCount":15,"invocationLatency":1798,"firstByteLatency":1507}} From 1d2f496b999ea474ab619b92afe4bcefe7c9ab90 Mon Sep 17 00:00:00 2001 From: Aaron Choo Date: Thu, 18 Jun 2026 11:16:46 -0400 Subject: [PATCH 28/59] api: mark ServiceQuota as todo (#2249) **Description** ServiceQuota currently needs to be implemented. The extension server does not inject the descriptor set but the rate limit config is properly configured. Adding it as a TODO/follow up. Signed-off-by: achoo30 --- api/v1alpha1/quota_policy.go | 3 +++ .../templates/aigateway.envoyproxy.io_quotapolicies.yaml | 2 ++ site/docs/api/api.mdx | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/quota_policy.go b/api/v1alpha1/quota_policy.go index 76867f9725..9f5d57983b 100644 --- a/api/v1alpha1/quota_policy.go +++ b/api/v1alpha1/quota_policy.go @@ -40,6 +40,9 @@ type QuotaPolicySpec struct { // Quota for all models served by AIServiceBackend(s). This value can be overridden for specific models using the "PerModelQuotas" // configuration. // + // Currently, the rate limit configuration is properly set up, but the descriptor set is not being set in the Envoy Configuration + // TODO: Add changes in the extension server to support ServiceQuota enforcement. + // // +optional ServiceQuota ServiceQuotaDefinition `json:"serviceQuota,omitempty"` // PerModelQuotas specifies quota for different models served by the AIServiceBackend(s) where this diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml index 81621a0773..e4963877e0 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml @@ -370,6 +370,8 @@ spec: description: |- Quota for all models served by AIServiceBackend(s). This value can be overridden for specific models using the "PerModelQuotas" configuration. + + Currently, the rate limit configuration is properly set up, but the descriptor set is not being set in the Envoy Configuration properties: costExpression: description: |- diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index b3dc277b90..9787df2861 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -2337,7 +2337,7 @@ QuotaPolicySpec specifies rules for computing token based costs of requests. name="serviceQuota" type="[ServiceQuotaDefinition](#github.1485827954.workers.dev-envoyproxy-ai-gateway-api-v1alpha1-servicequotadefinition)" required="false" - description="Quota for all models served by AIServiceBackend(s). This value can be overridden for specific models using the `PerModelQuotas`
    configuration." + description="Quota for all models served by AIServiceBackend(s). This value can be overridden for specific models using the `PerModelQuotas`
    configuration.
    Currently, the rate limit configuration is properly set up, but the descriptor set is not being set in the Envoy Configuration" /> Date: Thu, 18 Jun 2026 11:40:45 -0400 Subject: [PATCH 29/59] api: update quota policy docs (#2261) **Description** The failover logic depends on an Envoyproxy commit that has not been officially released. Marking that feature as a todo and will enable it once the changes are stable. Signed-off-by: achoo30 --- api/v1alpha1/quota_policy.go | 6 ++++-- .../templates/aigateway.envoyproxy.io_quotapolicies.yaml | 6 +++--- site/docs/api/api.mdx | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/quota_policy.go b/api/v1alpha1/quota_policy.go index 9f5d57983b..7db87fe064 100644 --- a/api/v1alpha1/quota_policy.go +++ b/api/v1alpha1/quota_policy.go @@ -12,8 +12,10 @@ import ( ) // QuotaPolicy specifies token quota configuration for inference services. -// Providing a list of backends in the AIGatewayRouteRule allows failover to a different service -// if token quota for a service had been exceeded. +// Generates rate limit configuration and tracks quota usage. +// Reject requests with 429 once all related quota to that request has been exceeded. +// +// TODO: Waiting on next release of Envoyproxy that will support routing based on non-exceeded quotas. // // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml index e4963877e0..8c033f0b62 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml @@ -28,10 +28,10 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: |- + description: | QuotaPolicy specifies token quota configuration for inference services. - Providing a list of backends in the AIGatewayRouteRule allows failover to a different service - if token quota for a service had been exceeded. + Generates rate limit configuration and tracks quota usage. + Reject requests with 429 once all related quota to that request has been exceeded. properties: apiVersion: description: |- diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 9787df2861..4397a1c5b0 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -481,8 +481,9 @@ MCPRouteList contains a list of MCPRoute. - [QuotaPolicyList](#github.1485827954.workers.dev-envoyproxy-ai-gateway-api-v1alpha1-quotapolicylist) QuotaPolicy specifies token quota configuration for inference services. -Providing a list of backends in the AIGatewayRouteRule allows failover to a different service -if token quota for a service had been exceeded. +Generates rate limit configuration and tracks quota usage. +Reject requests with 429 once all related quota to that request has been exceeded. + ##### Fields From 8aac9c0b3ecf8f6077bcf9135007ca9494717de1 Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Fri, 19 Jun 2026 12:00:34 -0700 Subject: [PATCH 30/59] docs: fixes @mathetake's affiliation (#2262) **Description** This commit fixes my affiliation Signed-off-by: Takeshi Yoneda --- site/blog/authors.yml | 2 +- site/src/data/talks.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/blog/authors.yml b/site/blog/authors.yml index bc7ed73827..4e202350b4 100644 --- a/site/blog/authors.yml +++ b/site/blog/authors.yml @@ -45,7 +45,7 @@ yaoweng: linkedin: https://www.linkedin.com/in/yao-weng-ab091442/ mathetake: name: Takeshi Yoneda - title: Envoy AI Gateway Maintainer - Tetrate + title: Envoy AI Gateway Maintainer - Netflix url: https://github.com/mathetake image_url: https://github.com/mathetake.png page: true diff --git a/site/src/data/talks.json b/site/src/data/talks.json index 7990aeb4a3..415e933351 100644 --- a/site/src/data/talks.json +++ b/site/src/data/talks.json @@ -28,7 +28,7 @@ "title": "Evolution of Envoy AI Gateway", "url": "https://www.youtube.com/watch?v=oXlnhRKPXhA", "event": "EnvoyCon North America 2025", - "speaker": "Yan Avlasov (Google), Takeshi Yoneda (Google)", + "speaker": "Yan Avlasov (Google), Takeshi Yoneda (Tetrate)", "description": "In this talk we will explore use, configuration and inner workings of the new features in Envoy AI Gateway." }, { From 7e3ec6d7fdf8e2cfd87c5f5ceed714c547b8e350 Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Sat, 20 Jun 2026 09:21:30 -0700 Subject: [PATCH 31/59] deps: resolves vulns in site/* (#2263) **Description** This resolves some vulnerabilities in site/* Signed-off-by: Takeshi Yoneda --- site/package-lock.json | 3738 +++++++++++++++++++++------------------- site/package.json | 6 +- 2 files changed, 2015 insertions(+), 1729 deletions(-) diff --git a/site/package-lock.json b/site/package-lock.json index 2e6119e9f8..4fb694035e 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -32,15 +32,15 @@ } }, "node_modules/@algolia/abtesting": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.1.tgz", - "integrity": "sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.19.0.tgz", + "integrity": "sha512-Lhnez3hhXHk25lfxLAMxvkP4fmN3+1RgADhD2ssMDBYuAsDVReeyP+3SGRx+ntq8ijMrLqUyfvO72TB6jsTteQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" @@ -79,99 +79,99 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.1.tgz", - "integrity": "sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.53.0.tgz", + "integrity": "sha512-0ZjA5Hcmaoz5Lj6OG0zhfIyeqzJZnLW2CRJA1W17UwMFGRtZAJ9yJKRvPEDA6gkpsIoQxORTSW6sWFiuYncPNQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.1.tgz", - "integrity": "sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.53.0.tgz", + "integrity": "sha512-kWNodP75iiEaOtemC9F/hlxNBG5E2QUjN1BusnE6m2b4l7Qh/BUO3fGCVsmKJI65VO4VKGGmT43ICvHtTcJ2JQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.1.tgz", - "integrity": "sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.53.0.tgz", + "integrity": "sha512-YPN45TXD9Wrse185t/Ta7nktZsqpv97oOjCzp2sblHnCL6rBc9TDeJAg1IGl2UpdwnSD05Zu/5wLB4watOUMyg==", "license": "MIT", "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-insights": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.1.tgz", - "integrity": "sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.53.0.tgz", + "integrity": "sha512-qAcYTDJE6m924FDDUQvdD6vh7DYaqOeSpFS74IP37/JRV0v4cGBauyxTF2WzDnokUylQDbqreoFIJZfg0Fitmw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.1.tgz", - "integrity": "sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.53.0.tgz", + "integrity": "sha512-fQaY+DkSJOpuUVUe8MQTwrdiKAqkJGhpDarB08duBn/sUv7Bkib6MDRQauCcWTWTe4HIW+EbwQP9R4kci1V/Yw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.1.tgz", - "integrity": "sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.53.0.tgz", + "integrity": "sha512-o72tsiEZGfeS/dxL9IADfzcZWGEwKDEe5CvtrBuT//3JR+SHuTtHRI2ZTf7D7bcKagcbojvO8hnkHdfoakSlYg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.1.tgz", - "integrity": "sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.53.0.tgz", + "integrity": "sha512-Ds16IyPm/dNJPCU8OzApo2gwGrgWT5BYHhE3NFwZbpCveqyvPDB9sZDDkJ5DsdOGT2aC+R3i0/M1OVXF2qdgPg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" @@ -184,81 +184,81 @@ "license": "MIT" }, "node_modules/@algolia/ingestion": { - "version": "1.52.1", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.1.tgz", - "integrity": "sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.53.0.tgz", + "integrity": "sha512-oNbT6z4NwD8Pou9VPINGlN/tlG1afESh2EbxqnP6rwl95xKVD/Zlciis1PpNeO/9U/rrajc1+7DcfKi03tX1KQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.52.1", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.1.tgz", - "integrity": "sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.53.0.tgz", + "integrity": "sha512-G+KZb/yd+qAOFn/cEvTGeLxQm8aP3a0od50l3z/ylccY+/o4YG3TNcjU1tFQHW4mXC137GPyR7W70R0kRQDLnA==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.1.tgz", - "integrity": "sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.53.0.tgz", + "integrity": "sha512-6aVfYd55Un6IUgPLbo84WfgFZlS3L0vA1ttzXL5vahHewUJ8jYgd89TzlWRTeej7w70mb9RWsVlFYGmJ/diQww==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "@algolia/client-common": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.1.tgz", - "integrity": "sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.53.0.tgz", + "integrity": "sha512-ke27DqgzCOlt+RbeEdCxtXxMQOnAOi8ujr2wid0DmDKzR95Kw/f9sBsuhBxtjevCqJRJszfRTLY0B1pbO6IhkA==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1" + "@algolia/client-common": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.1.tgz", - "integrity": "sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.53.0.tgz", + "integrity": "sha512-GngiOqt2Gq4oLno6yXQVj9om+qSO9SWAoduoTOEg79dKZ62brB8OOIvSJG/vDNoanYi6a7Al9uDZwXvi+bcVTg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1" + "@algolia/client-common": "5.53.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.1.tgz", - "integrity": "sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==", + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.53.0.tgz", + "integrity": "sha512-6mF9LZMUk0QqWvrnxkxBqhswwz6Xfiwy6/gmTzL5HrlhdVG3ITAqGV2k3XmVThP1h0Ulc3VQwiNCD7/Nr4JNlQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.52.1" + "@algolia/client-common": "5.53.0" }, "engines": { "node": ">= 14.0.0" @@ -277,22 +277,13 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@antfu/utils": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz", - "integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -301,29 +292,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -349,13 +340,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -365,25 +356,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -402,17 +393,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -432,13 +423,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -458,65 +449,65 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -526,35 +517,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -564,14 +555,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -581,79 +572,79 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -663,13 +654,13 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -679,12 +670,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -694,12 +685,28 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -709,14 +716,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -726,13 +733,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -766,12 +773,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -781,12 +788,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -796,12 +803,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -811,12 +818,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -842,12 +849,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -857,14 +864,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -874,14 +881,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -891,12 +898,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -906,12 +913,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", - "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -921,13 +928,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -937,13 +944,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -953,17 +960,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -973,13 +980,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -989,13 +996,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1005,13 +1012,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1021,12 +1028,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1036,13 +1043,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1052,12 +1059,12 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1067,13 +1074,13 @@ } }, "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1083,12 +1090,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1098,12 +1105,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1113,13 +1120,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1129,14 +1136,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1146,12 +1153,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1161,12 +1168,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1176,12 +1183,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1191,12 +1198,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1206,13 +1213,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1222,13 +1229,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1238,15 +1245,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1256,13 +1263,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1272,13 +1279,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1288,12 +1295,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1303,12 +1310,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1318,12 +1325,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1333,16 +1340,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1352,13 +1359,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1368,12 +1375,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1383,13 +1390,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1399,12 +1406,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1414,13 +1421,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1430,14 +1437,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1447,12 +1454,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1462,12 +1469,12 @@ } }, "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", - "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.29.7.tgz", + "integrity": "sha512-J0wGhKan+rIiE2OhfhRptySLrJ6SjQYM6b6N1FMlhyhCcw1Mig8vQjWchyB+bgHGDvaWo6Diu6CLRMra2uMtmg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1477,12 +1484,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1492,16 +1499,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", - "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1511,12 +1518,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" + "@babel/plugin-transform-react-jsx": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1526,13 +1533,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1542,12 +1549,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1557,13 +1564,13 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1573,12 +1580,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1588,13 +1595,13 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", - "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", @@ -1617,12 +1624,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1632,13 +1639,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1648,12 +1655,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1663,12 +1670,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1678,12 +1685,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1693,16 +1700,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", - "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1712,12 +1719,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1727,13 +1734,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1743,13 +1750,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1759,13 +1766,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1775,80 +1782,81 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-dotall-regex": "^7.27.1", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "engines": { @@ -1858,6 +1866,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1882,17 +1903,17 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", - "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.27.1", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1902,16 +1923,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1921,40 +1942,40 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1962,61 +1983,28 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@braintree/sanitize-url": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", - "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", - "license": "Apache-2.0" - }, "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", "license": "Apache-2.0" }, "node_modules/@colors/colors": { @@ -4200,19 +4188,14 @@ "license": "MIT" }, "node_modules/@iconify/utils": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.2.tgz", - "integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", "license": "MIT", "dependencies": { "@antfu/install-pkg": "^1.1.0", - "@antfu/utils": "^9.2.0", "@iconify/types": "^2.0.0", - "debug": "^4.4.1", - "globals": "^15.15.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.1.1", - "mlly": "^1.7.4" + "import-meta-resolve": "^4.2.0" } }, "node_modules/@jest/schemas": { @@ -4316,9 +4299,9 @@ } }, "node_modules/@jsonjoy.com/buffers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", - "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "license": "Apache-2.0", "engines": { "node": ">=10.0" @@ -4347,18 +4330,14 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz", - "integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==", + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.6.tgz", + "integrity": "sha512-uI++Wx6VkBJqVmkb4ZeExwAVpZiA2Do5NrEtXoDk0Pdvce3ytFXJoviT1sLOj16+qDIMnD5nWPfOhVpnDmRJKg==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.1", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", + "@jsonjoy.com/fs-node-builtins": "4.57.6", + "@jsonjoy.com/fs-node-utils": "4.57.6", "thingies": "^2.5.0" }, "engines": { @@ -4372,14 +4351,16 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.6.tgz", + "integrity": "sha512-pKkw/yC5CzSZKhIIUIsH1przOa+K5jGmZIg1sWaSF24JojyrUFbjcQv7QrcGAudriei6HQ6R0BFj+V8NbQinJw==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" + "@jsonjoy.com/fs-core": "4.57.6", + "@jsonjoy.com/fs-node-builtins": "4.57.6", + "@jsonjoy.com/fs-node-utils": "4.57.6", + "thingies": "^2.5.0" }, "engines": { "node": ">=10.0" @@ -4392,14 +4373,19 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.6.tgz", + "integrity": "sha512-Kbn1jdkvDN4F2+BhoB6mMu7NCbhP0bgA5NcI1aJj/Q5UcU+I1JLLW+dEQean33iV4tXv35AzBVKPICnDltBpxw==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" + "@jsonjoy.com/fs-core": "4.57.6", + "@jsonjoy.com/fs-node-builtins": "4.57.6", + "@jsonjoy.com/fs-node-utils": "4.57.6", + "@jsonjoy.com/fs-print": "4.57.6", + "@jsonjoy.com/fs-snapshot": "4.57.6", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" }, "engines": { "node": ">=10.0" @@ -4412,154 +4398,616 @@ "tslib": "2" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "license": "MIT" - }, - "node_modules/@mdx-js/mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", - "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "acorn": "^8.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-scope": "^1.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "recma-build-jsx": "^1.0.0", - "recma-jsx": "^1.0.0", - "recma-stringify": "^1.0.0", - "rehype-recma": "^1.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.6.tgz", + "integrity": "sha512-V4DgEFT3Cg5S9fCMOZSCVdTxdJWWLBO0WnAazV7hnCM96u5zXHyW/ubDAfcSVwqjkMJ50W1Y44IXtxRoIwaCVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", - "license": "MIT", + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.6.tgz", + "integrity": "sha512-+JptNw3iifihxH2rEXrninDzX4FFVW8JD/wPR8GbJPAeL9CQUSblrlumOPB5gZuS7tYRX+PJPLtT7XzKoRhv/Q==", + "license": "Apache-2.0", "dependencies": { - "@types/mdx": "^2.0.0" + "@jsonjoy.com/fs-fsa": "4.57.6", + "@jsonjoy.com/fs-node-builtins": "4.57.6", + "@jsonjoy.com/fs-node-utils": "4.57.6" + }, + "engines": { + "node": ">=10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "type": "github", + "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" + "tslib": "2" } }, - "node_modules/@mermaid-js/parser": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz", - "integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==", - "license": "MIT", + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.6.tgz", + "integrity": "sha512-foyUrfS7WmYEUzqYXSNxmJBcSj04TABrkpFabwO9SCDCpVCfJ+qG+2sk5FjfiflG2n0SDFZDCJ6vYlJAEpxJFg==", + "license": "Apache-2.0", "dependencies": { - "langium": "3.3.1" + "@jsonjoy.com/fs-node-builtins": "4.57.6" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.6.tgz", + "integrity": "sha512-96eAn4Dudtt67LTeuU47yUD+pg9/G/oKpI10zei9ljk3X3WK4lYKc+n3cpaPCAbKPzoyfxl0mXm8f8Y7BOSFXw==", + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@jsonjoy.com/fs-node-utils": "4.57.6", + "tree-dump": "^1.1.0" }, "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.6.tgz", + "integrity": "sha512-V57CMzbOgTzUWGOWQ8GzHQdpJP6JnrYVNCtTBNxVYEnlVRvo4uEJqHhtAT8vhDFrIuJOXLrTL1Fki4h5oI7xxg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.57.6", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", - "license": "MIT", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", "engines": { - "node": ">=12.22.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", - "license": "MIT", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", "dependencies": { - "graceful-fs": "4.2.10" + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" }, "engines": { - "node": ">=12.22.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "license": "ISC" - }, - "node_modules/@pnpm/npm-conf": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", - "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", - "license": "MIT", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" + "@jsonjoy.com/util": "17.67.0" }, "engines": { - "node": ">=12" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz", + "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz", + "integrity": "sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "@peculiar/asn1-x509-attr": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.7.0.tgz", + "integrity": "sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.7.0.tgz", + "integrity": "sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.7.0.tgz", + "integrity": "sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.7.0", + "@peculiar/asn1-pkcs8": "^2.7.0", + "@peculiar/asn1-rsa": "^2.7.0", + "@peculiar/asn1-schema": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.7.0.tgz", + "integrity": "sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.7.0.tgz", + "integrity": "sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.7.0", + "@peculiar/asn1-pfx": "^2.7.0", + "@peculiar/asn1-pkcs8": "^2.7.0", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "@peculiar/asn1-x509-attr": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz", + "integrity": "sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz", + "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==", + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz", + "integrity": "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz", + "integrity": "sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, @@ -4882,15 +5330,6 @@ "node": ">=14.16" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -5137,9 +5576,9 @@ "license": "MIT" }, "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -5183,38 +5622,18 @@ } }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -5227,21 +5646,21 @@ } }, "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -5295,9 +5714,9 @@ "license": "MIT" }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "license": "MIT" }, "node_modules/@types/http-errors": { @@ -5307,9 +5726,9 @@ "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -5373,33 +5792,24 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "license": "MIT", "dependencies": { - "undici-types": "~7.14.0" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", "license": "MIT" }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "license": "MIT" }, "node_modules/@types/range-parser": { @@ -5409,12 +5819,12 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.1.tgz", - "integrity": "sha512-1U5NQWh/GylZQ50ZMnnPjkYHEaGhg6t5i/KI0LDDh3t4E3h3T3vzm+GLY2BRzMfIjSBwzm6tginoZl5z0O/qsA==", + "version": "19.2.16", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.16.tgz", + "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-router": { @@ -5465,9 +5875,9 @@ } }, "node_modules/@types/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", - "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -5483,9 +5893,9 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", - "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -5494,9 +5904,9 @@ } }, "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -5550,11 +5960,21 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -5757,9 +6177,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5790,9 +6210,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -5824,9 +6244,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -5869,25 +6289,25 @@ } }, "node_modules/algoliasearch": { - "version": "5.52.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.1.tgz", - "integrity": "sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==", - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.18.1", - "@algolia/client-abtesting": "5.52.1", - "@algolia/client-analytics": "5.52.1", - "@algolia/client-common": "5.52.1", - "@algolia/client-insights": "5.52.1", - "@algolia/client-personalization": "5.52.1", - "@algolia/client-query-suggestions": "5.52.1", - "@algolia/client-search": "5.52.1", - "@algolia/ingestion": "1.52.1", - "@algolia/monitoring": "1.52.1", - "@algolia/recommend": "5.52.1", - "@algolia/requester-browser-xhr": "5.52.1", - "@algolia/requester-fetch": "5.52.1", - "@algolia/requester-node-http": "5.52.1" + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.53.0.tgz", + "integrity": "sha512-OGW1q6b91CRSSeiOnM8LxuR5NYJ2esvw66jUZ4IIvdv+ItNkx3pwLuyR+jaCdbGee4ov5WgUnyPryyh11xvByQ==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.19.0", + "@algolia/client-abtesting": "5.53.0", + "@algolia/client-analytics": "5.53.0", + "@algolia/client-common": "5.53.0", + "@algolia/client-insights": "5.53.0", + "@algolia/client-personalization": "5.53.0", + "@algolia/client-query-suggestions": "5.53.0", + "@algolia/client-search": "5.53.0", + "@algolia/ingestion": "1.53.0", + "@algolia/monitoring": "1.53.0", + "@algolia/recommend": "5.53.0", + "@algolia/requester-browser-xhr": "5.53.0", + "@algolia/requester-fetch": "5.53.0", + "@algolia/requester-node-http": "5.53.0" }, "engines": { "node": ">= 14.0.0" @@ -6019,6 +6439,20 @@ "node": ">=8" } }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -6091,13 +6525,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { @@ -6127,12 +6561,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6149,15 +6583,18 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", - "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "version": "2.10.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", + "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -6194,9 +6631,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -6207,7 +6644,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -6235,26 +6672,6 @@ "ms": "2.0.0" } }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6273,19 +6690,10 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.0.tgz", + "integrity": "sha512-fGQtj1qdR9vIKjFiWPQd52qIqwjaYqhcI40JEiDuvlZ86E7ZBPBwY9fPgHy9r2rYGIjiRfctNPYz6OQU73ww2w==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6321,13 +6729,15 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -6405,6 +6815,15 @@ "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -6523,9 +6942,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001790", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", - "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", "funding": [ { "type": "opencollective", @@ -6655,32 +7074,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", - "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "chevrotain": "^11.0.0" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6972,12 +7365,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "license": "MIT" - }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -7056,18 +7443,18 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, "node_modules/copy-text-to-clipboard": { @@ -7150,9 +7537,9 @@ } }, "node_modules/core-js": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", - "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -7161,12 +7548,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", - "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", "license": "MIT", "dependencies": { - "browserslist": "^4.25.3" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", @@ -7668,15 +8055,15 @@ "license": "CC0-1.0" }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "license": "MIT", "engines": { "node": ">=0.10" @@ -7936,9 +8323,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -8172,9 +8559,9 @@ } }, "node_modules/dagre-d3-es": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", - "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -8182,9 +8569,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, "node_modules/debounce": { @@ -8211,9 +8598,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -8269,9 +8656,9 @@ } }, "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -8285,9 +8672,9 @@ } }, "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "license": "MIT", "engines": { "node": ">=18" @@ -8349,9 +8736,9 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -8496,9 +8883,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -8585,9 +8972,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.344", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", - "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "version": "1.5.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -8631,13 +9018,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", + "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -8683,15 +9070,15 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -8700,6 +9087,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", + "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -8896,9 +9293,9 @@ } }, "node_modules/estree-util-value-to-estree": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.4.0.tgz", - "integrity": "sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -9011,14 +9408,14 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -9037,7 +9434,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -9084,9 +9481,9 @@ "license": "MIT" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/express/node_modules/range-parser": { @@ -9098,12 +9495,6 @@ "node": ">= 0.6" } }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9151,9 +9542,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -9167,9 +9558,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -9233,9 +9624,9 @@ } }, "node_modules/file-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -9294,17 +9685,17 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -9368,9 +9759,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -9436,9 +9827,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -9593,42 +9984,6 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", @@ -9644,18 +9999,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -9841,9 +10184,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -9966,15 +10309,15 @@ } }, "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" @@ -9984,16 +10327,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-parse5/node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -10163,9 +10496,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", - "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", + "version": "5.6.7", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", + "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -10256,19 +10589,23 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-parser-js": { @@ -10428,6 +10765,16 @@ "node": ">=8" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -10471,9 +10818,9 @@ } }, "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, "node_modules/internmap": { @@ -10495,9 +10842,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", "license": "MIT", "engines": { "node": ">= 10" @@ -10558,12 +10905,12 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -10696,9 +11043,9 @@ } }, "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", "license": "MIT", "engines": { "node": ">=16" @@ -10915,9 +11262,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10969,9 +11326,9 @@ } }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -10981,9 +11338,9 @@ } }, "node_modules/katex": { - "version": "0.16.23", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.23.tgz", - "integrity": "sha512-7VlC1hsEEolL9xNO05v9VjrvWZePkCVBJqj8ruICxYjZfHaHbaU53AlP+PODyFIXEnaEIEWi3wJy7FPZ95JAVg==", + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -11037,28 +11394,6 @@ "node": ">=6" } }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "license": "MIT" - }, - "node_modules/langium": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", - "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", - "license": "MIT", - "dependencies": { - "chevrotain": "~11.0.3", - "chevrotain-allstar": "~0.3.0", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.0.8" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/latest-version": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", @@ -11075,13 +11410,13 @@ } }, "node_modules/launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "node_modules/layout-base": { @@ -11118,12 +11453,16 @@ "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -11140,23 +11479,6 @@ "node": ">=8.9.0" } }, - "node_modules/local-pkg": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", @@ -11173,15 +11495,15 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -11277,9 +11599,9 @@ } }, "node_modules/marked": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz", - "integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==", + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -11347,9 +11669,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -11715,11 +12037,19 @@ } }, "node_modules/memfs": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.49.0.tgz", - "integrity": "sha512-L9uC9vGuc4xFybbdOpRLoOAOq1YEBBsocCs5NVW32DfU+CZWWIn3OVF+lB8Gp4ttBVSMazwrTrjv8ussX/e3VQ==", + "version": "4.57.6", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.6.tgz", + "integrity": "sha512-WQK+DGjKCnPdpSyJUXphz+COF2uEhhsxQ3VIWBSbzpbbXuch3h4FePMqXrXGdLjsTgo4JFzBFsP6AWd9pVazGw==", "license": "Apache-2.0", "dependencies": { + "@jsonjoy.com/fs-core": "4.57.6", + "@jsonjoy.com/fs-fsa": "4.57.6", + "@jsonjoy.com/fs-node": "4.57.6", + "@jsonjoy.com/fs-node-builtins": "4.57.6", + "@jsonjoy.com/fs-node-to-fsa": "4.57.6", + "@jsonjoy.com/fs-node-utils": "4.57.6", + "@jsonjoy.com/fs-print": "4.57.6", + "@jsonjoy.com/fs-snapshot": "4.57.6", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", @@ -11730,6 +12060,9 @@ "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, "node_modules/merge-descriptors": { @@ -11757,31 +12090,32 @@ } }, "node_modules/mermaid": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.0.tgz", - "integrity": "sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==", + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", + "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.1", - "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^0.6.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.11", - "dayjs": "^1.11.18", - "dompurify": "^3.2.5", - "katex": "^0.16.22", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "es-toolkit": "^1.45.1", + "katex": "^0.16.25", "khroma": "^2.1.0", - "lodash-es": "^4.17.21", - "marked": "^16.2.1", + "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "node_modules/methods": { @@ -13671,15 +14005,18 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -13700,35 +14037,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -13758,9 +14066,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -13815,21 +14123,15 @@ "node": ">=18" } }, - "node_modules/node-forge": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", - "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", - "license": "(BSD-3-Clause OR GPL-2.0)", + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", "engines": { - "node": ">= 6.13.0" + "node": ">=18" } }, - "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "license": "MIT" - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -13840,9 +14142,9 @@ } }, "node_modules/normalize-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", - "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "license": "MIT", "engines": { "node": ">=14.16" @@ -14195,9 +14497,9 @@ } }, "node_modules/package-manager-detector": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.0.tgz", - "integrity": "sha512-rRZ+pR1Usc+ND9M2NkmCvE/LYJS+8ORVV9X0KuNSY/gFsp7RBHJM/ADh9LYq4Vvfq6QkKrW6/weuh8SMEtN5gw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT" }, "node_modules/param-case": { @@ -14380,9 +14682,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -14406,12 +14708,6 @@ "node": ">=8" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -14419,9 +14715,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -14445,15 +14741,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "license": "MIT", + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "license": "BSD-3-Clause", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/points-on-curve": { @@ -14473,9 +14775,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -14492,7 +14794,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -16007,9 +16309,9 @@ } }, "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "license": "MIT", "funding": { "type": "github", @@ -16068,36 +16370,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", "dependencies": { - "side-channel": "^1.1.0" - }, + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=16.0.0" } }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -16131,15 +16435,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", @@ -16173,26 +16468,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -16205,15 +16480,6 @@ "node": ">=0.10.0" } }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -16245,24 +16511,24 @@ } }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.7" } }, "node_modules/react-fast-compare": { @@ -16480,6 +16746,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -16516,12 +16788,12 @@ } }, "node_modules/registry-auth-token": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", - "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", "license": "MIT", "dependencies": { - "@pnpm/npm-conf": "^2.1.0" + "@pnpm/npm-conf": "^3.0.2" }, "engines": { "node": ">=14" @@ -16549,9 +16821,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" @@ -16851,12 +17123,13 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -16926,9 +17199,9 @@ } }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, "node_modules/roughjs": { @@ -17029,10 +17302,13 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.27.0", @@ -17092,22 +17368,22 @@ "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -17132,24 +17408,24 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -17170,15 +17446,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/send/node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -17189,12 +17456,12 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/serve-handler": { @@ -17212,6 +17479,34 @@ "range-parser": "1.2.0" } }, + "node_modules/serve-handler/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/serve-handler/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/serve-handler/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/serve-handler/node_modules/path-to-regexp": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", @@ -17219,21 +17514,25 @@ "license": "MIT" }, "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "license": "MIT", "dependencies": { - "accepts": "~1.3.4", + "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-index/node_modules/debug": { @@ -17255,25 +17554,41 @@ } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" + "node_modules/serve-index/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", @@ -17281,12 +17596,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "license": "ISC" - }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -17297,15 +17606,15 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -17374,9 +17683,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -17405,13 +17714,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -17550,15 +17859,6 @@ "websocket-driver": "^0.7.4" } }, - "node_modules/sockjs/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/sort-css-media-queries": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", @@ -17664,9 +17964,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -17717,12 +18017,12 @@ } }, "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -17802,21 +18102,21 @@ } }, "node_modules/style-to-js": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", - "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", "license": "MIT", "dependencies": { - "style-to-object": "1.0.9" + "style-to-object": "1.0.14" } }, "node_modules/style-to-object": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", - "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", "dependencies": { - "inline-style-parser": "0.2.4" + "inline-style-parser": "0.2.7" } }, "node_modules/stylehacks": { @@ -17836,9 +18136,9 @@ } }, "node_modules/stylis": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", "license": "MIT" }, "node_modules/supports-color": { @@ -17872,18 +18172,18 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -17906,9 +18206,9 @@ } }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", "engines": { "node": ">=6" @@ -17919,9 +18219,9 @@ } }, "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -17937,15 +18237,14 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -17959,12 +18258,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -18006,9 +18332,9 @@ "license": "MIT" }, "node_modules/thingies": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", - "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", "license": "MIT", "engines": { "node": ">=10.18" @@ -18040,10 +18366,13 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "license": "MIT" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinypool": { "version": "1.1.1", @@ -18135,6 +18464,24 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -18204,16 +18551,10 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "license": "MIT" - }, "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -18300,9 +18641,9 @@ } }, "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -18352,9 +18693,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18367,9 +18708,9 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18539,9 +18880,9 @@ } }, "node_modules/url-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -18639,9 +18980,9 @@ } }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -18708,59 +19049,10 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", - "license": "MIT" - }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -18790,36 +19082,34 @@ } }, "node_modules/webpack": { - "version": "5.102.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.0.tgz", - "integrity": "sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==", + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.5", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.2.3", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.5.0", + "watchpack": "^2.5.1", + "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" @@ -18911,15 +19201,19 @@ } }, "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/webpack-dev-middleware/node_modules/range-parser": { @@ -18932,14 +19226,14 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", - "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz", + "integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==", "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", + "@types/express": "^4.17.25", "@types/express-serve-static-core": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", @@ -18949,9 +19243,9 @@ "bonjour-service": "^1.2.1", "chokidar": "^3.6.0", "colorette": "^2.0.10", - "compression": "^1.7.4", + "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", + "express": "^4.22.1", "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", @@ -18959,7 +19253,7 @@ "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", + "selfsigned": "^5.5.0", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", @@ -19019,9 +19313,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -19054,31 +19348,19 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/webpack/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, "engines": { "node": ">= 0.6" } @@ -19111,9 +19393,9 @@ } }, "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", @@ -19211,12 +19493,12 @@ } }, "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -19238,9 +19520,9 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -19274,9 +19556,9 @@ } }, "node_modules/wsl-utils/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" diff --git a/site/package.json b/site/package.json index 61daa78871..2c69315e8a 100644 --- a/site/package.json +++ b/site/package.json @@ -49,5 +49,9 @@ "engines": { "node": ">=22.0" }, - "packageManager": "npm@10.9.0" + "packageManager": "npm@10.9.0", + "overrides": { + "serialize-javascript": "^7.0.5", + "uuid": "^11.1.1" + } } From 67e49260653d82c62219e8e5d42ce394ab7cf104 Mon Sep 17 00:00:00 2001 From: Aaron Choo Date: Sun, 21 Jun 2026 13:36:50 -0400 Subject: [PATCH 32/59] docs: update api comment for quota policy (#2265) **Description** 1. Update the costExpression example to work 2. Only headers are implemented for ClientSelectors Signed-off-by: achoo30 --- api/v1alpha1/quota_policy.go | 4 ++-- .../templates/aigateway.envoyproxy.io_quotapolicies.yaml | 3 ++- site/docs/api/api.mdx | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/quota_policy.go b/api/v1alpha1/quota_policy.go index 7db87fe064..02d8c2f919 100644 --- a/api/v1alpha1/quota_policy.go +++ b/api/v1alpha1/quota_policy.go @@ -64,7 +64,7 @@ type ServiceQuotaDefinition struct { // If no expression is specified the "total_tokens" value is used. // For example: // - // * "input_tokens + cached_input_tokens * 0.1 + output_tokens * 6" + // "input_tokens + cached_input_tokens + output_tokens" // // +optional CostExpression *string `json:"costExpression,omitempty"` @@ -138,7 +138,7 @@ type QuotaRule struct { // specific clients using attributes from the traffic flow. // All individual select conditions must hold True for this rule // and its limit to be applied. - // + // Currently, only header types are honored. // // If no client selectors are specified, the rule applies to all traffic of // the targeted AIServiceBackend. diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml index 8c033f0b62..170b2c7b19 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml @@ -91,6 +91,7 @@ spec: specific clients using attributes from the traffic flow. All individual select conditions must hold True for this rule and its limit to be applied. + Currently, only header types are honored. If no client selectors are specified, the rule applies to all traffic of the targeted AIServiceBackend. @@ -379,7 +380,7 @@ spec: If no expression is specified the "total_tokens" value is used. For example: - * "input_tokens + cached_input_tokens * 0.1 + output_tokens * 6" + "input_tokens + cached_input_tokens + output_tokens" type: string quota: description: |- diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 4397a1c5b0..00cad11ce9 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -2385,7 +2385,7 @@ QuotaPolicyStatus contains the conditions by the reconciliation result. name="clientSelectors" type="RateLimitSelectCondition array" required="false" - description="ClientSelectors holds the list of conditions to select
    specific clients using attributes from the traffic flow.
    All individual select conditions must hold True for this rule
    and its limit to be applied.
    If no client selectors are specified, the rule applies to all traffic of
    the targeted AIServiceBackend." + description="ClientSelectors holds the list of conditions to select
    specific clients using attributes from the traffic flow.
    All individual select conditions must hold True for this rule
    and its limit to be applied.
    Currently, only header types are honored.
    If no client selectors are specified, the rule applies to all traffic of
    the targeted AIServiceBackend." /> Date: Mon, 22 Jun 2026 19:49:13 +0400 Subject: [PATCH 33/59] fix: preserve inferencepool metadata on extension hooks (#2142) **Description** PostClusterModify and PostRouteModify dropped InferencePool metadata when Envoy Gateway sent a cluster or route with nil Metadata. The helper allocated metadata, but only in a local pointer, so later InferencePool handling could not read it. Small fix, real footgun. Repro: 1. Revert this patch, or apply only the added assertions. 2. Run `go test ./internal/extensionserver -run 'TestPost(Cluster|Route)Modify/with_InferencePool' -count=1`. 3. Both hook tests fail because getInferencePoolByMetadata returns nil. This is reachable for normal InferencePool backendRefs; Metadata is optional in the Envoy proto and the repo example uses AIGatewayRoute to InferencePool. No cloud quota or provider API ceiling blocks this path. Verification: `go test ./internal/extensionserver -run 'TestPost(Cluster|Route)Modify/with_InferencePool' -count=1` `go test ./internal/extensionserver` `make test` `make precommit` **Related Issues/PRs (if applicable)** Related PR: #2071 **Special notes for reviewers (if applicable)** AI-assisted patch prep; I reviewed the code and own the change. Signed-off-by: immanuwell Co-authored-by: Ignasi Barrera --- internal/extensionserver/extensionserver_test.go | 2 ++ internal/extensionserver/inferencepool.go | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/extensionserver/extensionserver_test.go b/internal/extensionserver/extensionserver_test.go index 127c986a5e..af4115655a 100644 --- a/internal/extensionserver/extensionserver_test.go +++ b/internal/extensionserver/extensionserver_test.go @@ -1355,6 +1355,7 @@ func TestPostClusterModify(t *testing.T) { require.NotNil(t, cluster.LbConfig) require.Nil(t, cluster.LoadBalancingPolicy) require.Nil(t, cluster.EdsClusterConfig) + require.NotNil(t, getInferencePoolByMetadata(cluster.Metadata)) }) } @@ -1419,6 +1420,7 @@ func TestPostRouteModify(t *testing.T) { // Verify route was modified. require.Equal(t, wrapperspb.Bool(false), route.GetRoute().GetAutoHostRewrite()) require.NotNil(t, route.TypedPerFilterConfig) + require.NotNil(t, getInferencePoolByMetadata(route.Metadata)) }) t.Run("with InferencePool extension and DirectResponse route action", func(t *testing.T) { diff --git a/internal/extensionserver/inferencepool.go b/internal/extensionserver/inferencepool.go index 642143af55..34eaf5aae0 100644 --- a/internal/extensionserver/inferencepool.go +++ b/internal/extensionserver/inferencepool.go @@ -130,24 +130,26 @@ func getInferencePoolByMetadata(meta *corev3.Metadata) *gwaiev1.InferencePool { } // buildMetadataForInferencePool adds InferencePool metadata to the cluster for reference by other components. -// encoded as a string in the format: "namespace/name/serviceName/port". +// encoded as a string in the format: "namespace/name/serviceName/port/bodyMode/allowModeOverride". func buildEPPMetadataForCluster(cluster *clusterv3.Cluster, inferencePool *gwaiev1.InferencePool) { // Initialize cluster metadata structure if not present. + if cluster.Metadata == nil { + cluster.Metadata = &corev3.Metadata{} + } buildEPPMetadata(cluster.Metadata, inferencePool) } // buildMetadataForInferencePool adds InferencePool metadata to the route for reference by other components. func buildEPPMetadataForRoute(route *routev3.Route, inferencePool *gwaiev1.InferencePool) { // Initialize route metadata structure if not present. + if route.Metadata == nil { + route.Metadata = &corev3.Metadata{} + } buildEPPMetadata(route.Metadata, inferencePool) } // buildEPPMetadata adds InferencePool metadata to the given metadata structure. func buildEPPMetadata(metadata *corev3.Metadata, inferencePool *gwaiev1.InferencePool) { - // Initialize cluster metadata structure if not present. - if metadata == nil { - metadata = &corev3.Metadata{} - } if metadata.FilterMetadata == nil { metadata.FilterMetadata = make(map[string]*structpb.Struct) } From d029955b29dd76da10e15430bd36a5bb714a1fcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:20:45 +0000 Subject: [PATCH 34/59] chore(deps): bump github.com/containerd/containerd from 1.7.32 to 1.7.33 in /tools (#2264) Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.32 to 1.7.33.
    Release notes

    Sourced from github.com/containerd/containerd's releases.

    containerd 1.7.33

    Welcome to the v1.7.33 release of containerd!

    The thirty-third patch release for containerd 1.7 contains various fixes and updates including security patches.

    Security Updates

    Please try out the release binaries and report any issues at https://github.com/containerd/containerd/issues.

    Contributors

    • Samuel Karp
    • Chris Henzie
    • Akihiro Suda
    • Akhil Mohan
    • Ben Cressey
    • Davanum Srinivas
    • Sopho Merkviladze

    Changes

    • Prepare release notes for v1.7.33 (#13631)
      • 7517e6737 Prepare release notes for v1.7.33
      • ab306518a Merge commit from fork
      • d34cdafda Merge commit from fork
      • 9ab2b7a89 Bound user-database file reads in openBoundedUserFile
      • 1e9806f90 Merge commit from fork
      • 4d8ba4d23 Do not propagate reserved labels from image configs
    • update runc binary to v1.3.6 (#13615)
    • update go to 1.26.4/1.25.11 (#13579)
    • Configure udevd children-max for root-test (#13564)
      • e884e964e Configure udevd children-max for root-test
    • Clean up disk space in node e2e workflow (#13552)
      • b9e756888 Clean up disk space in node e2e workflow
    • Bump go-jose/go-jose/v3 to v3.0.5 to fix GHSA-78h2-9frx-2jm8 (#13467)
      • 4dfc1844e Bump go-jose to v3.0.5 to address CVE-2026-34986

    ... (truncated)

    Commits
    • e8b1a9b Merge pull request #13631 from samuelkarp/prepare-1.7.33
    • 7517e67 Prepare release notes for v1.7.33
    • ab30651 Merge commit from fork
    • 0962898 Merge pull request #13615 from k8s-infra-cherrypick-robot/cherry-pick-13606-t...
    • 74c728c update runc binary to v1.3.6
    • d34cdaf Merge commit from fork
    • 1e9806f Merge commit from fork
    • 9ab2b7a Bound user-database file reads in openBoundedUserFile
    • d805d96 Merge pull request #13579 from akhilerm/1.7-go1.26.4
    • 947caa4 update go to 1.26.4/1.25.11
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/containerd/containerd&package-manager=go_modules&previous-version=1.7.32&new-version=1.7.33)](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 this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/envoyproxy/ai-gateway/network/alerts).
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ignasi Barrera --- tools/go.mod | 2 +- tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index 4fa927f55d..828a41b347 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -97,7 +97,7 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect - github.com/containerd/containerd v1.7.32 // indirect + github.com/containerd/containerd v1.7.33 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect diff --git a/tools/go.sum b/tools/go.sum index e16de8b3fe..23e1baba51 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -157,8 +157,8 @@ github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7Lsp github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/containerd/containerd v1.7.32 h1:S54xuVcPxeLaYgaRABtpJ2VyVUVsy0IGf7qHBs+sbY8= -github.com/containerd/containerd v1.7.32/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E= +github.com/containerd/containerd v1.7.33 h1:iAkYGC/ifR/V+0eR4iXWHNGYUF0DF2PmGV5iz4Irj5M= +github.com/containerd/containerd v1.7.33/go.mod h1:gSbSCVjPCdkfJCjyrzz7aRC+xFlqVbatNpfHfVCYGUM= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= From 1fd9dfeceb11225ca5538d88c7b66c4b520b3b1a Mon Sep 17 00:00:00 2001 From: Aaron Choo Date: Mon, 22 Jun 2026 13:16:02 -0400 Subject: [PATCH 35/59] docs: fixed incorrect documentation (#2271) **Description** These limits are quota mode which behaves as the inverse of traditional rate limit. Updating the document to reflect that. Signed-off-by: achoo30 --- api/v1alpha1/quota_policy.go | 2 +- .../templates/aigateway.envoyproxy.io_quotapolicies.yaml | 2 +- site/docs/api/api.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/quota_policy.go b/api/v1alpha1/quota_policy.go index 02d8c2f919..00a8b39a9a 100644 --- a/api/v1alpha1/quota_policy.go +++ b/api/v1alpha1/quota_policy.go @@ -101,7 +101,7 @@ type QuotaDefinition struct { CostExpression *string `json:"costExpression,omitempty"` // The "Mode" determines how quota is charged to the "DefaultBucket" and matching "BucketRules". // In the "shared" mode the quota is charged to all matching "BucketRules" AND the "DefaultBucket" - // and request is allowed only if the quota is available in all matching buckets. + // and request is allowed only if the quota is available in at least one matching buckets. // Defaults to "Shared". // // +optional diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml index 170b2c7b19..52b8890613 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml @@ -355,7 +355,7 @@ spec: description: |- The "Mode" determines how quota is charged to the "DefaultBucket" and matching "BucketRules". In the "shared" mode the quota is charged to all matching "BucketRules" AND the "DefaultBucket" - and request is allowed only if the quota is available in all matching buckets. + and request is allowed only if the quota is available in at least one matching buckets. Defaults to "Shared". enum: - Shared diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 00cad11ce9..3a3ef29d9b 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -2302,7 +2302,7 @@ QuotaDefinition specified expression for computing request cost and rules for ma type="[QuotaBucketMode](#github.1485827954.workers.dev-envoyproxy-ai-gateway-api-v1alpha1-quotabucketmode)" required="false" defaultValue="Shared" - description="The `Mode` determines how quota is charged to the `DefaultBucket` and matching `BucketRules`.
    In the `shared` mode the quota is charged to all matching `BucketRules` AND the `DefaultBucket`
    and request is allowed only if the quota is available in all matching buckets.
    Defaults to `Shared`." + description="The `Mode` determines how quota is charged to the `DefaultBucket` and matching `BucketRules`.
    In the `shared` mode the quota is charged to all matching `BucketRules` AND the `DefaultBucket`
    and request is allowed only if the quota is available in at least one matching buckets.
    Defaults to `Shared`." /> Date: Mon, 22 Jun 2026 18:27:27 +0000 Subject: [PATCH 36/59] chore(deps): bump the go group across 1 directory with 9 updates (#2256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the go group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `1.41.7` | `1.41.11` | | [github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream](https://github.com/aws/aws-sdk-go-v2) | `1.7.10` | `1.7.12` | | [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) | `1.32.18` | `1.32.22` | | [github.com/bytedance/sonic](https://github.com/bytedance/sonic) | `1.15.1` | `1.15.2` | | [github.com/openai/openai-go/v3](https://github.com/openai/openai-go) | `3.37.0` | `3.39.0` | | [github.com/prometheus/common](https://github.com/prometheus/common) | `0.67.5` | `0.68.1` | | [google.golang.org/api](https://github.com/googleapis/google-api-go-client) | `0.282.0` | `0.283.0` | | [google.golang.org/genai](https://github.com/googleapis/go-genai) | `1.58.0` | `1.59.0` | Updates `github.com/aws/aws-sdk-go-v2` from 1.41.7 to 1.41.11
    Commits

    Updates `github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream` from 1.7.10 to 1.7.12
    Commits

    Updates `github.com/aws/aws-sdk-go-v2/config` from 1.32.18 to 1.32.22
    Commits

    Updates `github.com/aws/aws-sdk-go-v2/service/sts` from 1.42.1 to 1.43.1
    Commits

    Updates `github.com/bytedance/sonic` from 1.15.1 to 1.15.2
    Release notes

    Sourced from github.com/bytedance/sonic's releases.

    v1.15.2

    What's Changed

    Full Changelog: https://github.com/bytedance/sonic/compare/v1.15.1...v1.15.2

    Commits
    • 579b6ff fix: use legacy map iterator shim
    • d774513 fix: align map runtime tags with Go 1.24
    • 47cc3ed Revert "rebase: update dev_hw with latest main" (#941)
    • fdf817a chore: update llvm
    • 93044d2 fix:add pretouchRecX86 in arm64 and add !arm64 to loader_go117_test.go
    • 59cca7c feat: update sve_linkname|sve_wrapgoc with asm2arm_tool
    • 76dcf4a chore: add asm2arm_tool execution and test scripts
    • ccddb06 feat: add SL mode support for asm2arm_tool
    • a756ce4 feat: add JIT mode support for asm2arm_tool
    • 1d17626 feat: use SVE acceleration in linkname and wrapgoc
    • Additional commits viewable in compare view

    Updates `github.com/openai/openai-go/v3` from 3.37.0 to 3.39.0
    Release notes

    Sourced from github.com/openai/openai-go/v3's releases.

    v3.39.0

    3.39.0 (2026-06-03)

    Full Changelog: v3.38.0...v3.39.0

    Features

    • api: responses.moderation and chat_completions.moderation (7a2dac0)

    v3.38.0

    3.38.0 (2026-06-01)

    Full Changelog: v3.37.0...v3.38.0

    Features

    • api: manual updates (d7dac81)
    • api: workload identity in audit logs, additional_tools item in responses, fix ActionSearch.query to be optional. (4c3981c)
    Changelog

    Sourced from github.com/openai/openai-go/v3's changelog.

    3.39.0 (2026-06-03)

    Full Changelog: v3.38.0...v3.39.0

    Features

    • api: responses.moderation and chat_completions.moderation (7a2dac0)

    3.38.0 (2026-06-01)

    Full Changelog: v3.37.0...v3.38.0

    Features

    • api: manual updates (d7dac81)
    • api: workload identity in audit logs, additional_tools item in responses, fix ActionSearch.query to be optional. (4c3981c)
    Commits
    • bfe9298 release: 3.39.0
    • b256c9a feat(api): responses.moderation and chat_completions.moderation
    • fd8c615 codegen metadata
    • f25b030 release: 3.38.0
    • 08e89a6 feat(api): workload identity in audit logs, additional_tools item in response...
    • See full diff in compare view

    Updates `github.com/prometheus/common` from 0.67.5 to 0.68.1
    Release notes

    Sourced from github.com/prometheus/common's releases.

    v0.68.1

    What's Changed

    Full Changelog: https://github.com/prometheus/common/compare/v0.68.0...v0.68.1

    v0.68.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/prometheus/common/compare/v0.67.5...v0.68.0

    Changelog

    Sourced from github.com/prometheus/common's changelog.

    Changelog

    main / unreleased

    What's Changed

    v0.69.0 / 2026-06-17

    Security / behavior changes

    • config: credentials are no longer forwarded across cross-host redirects. When FollowRedirects is enabled, the HTTP client now strips Authorization, Cookie, Proxy-Authorization and other sensitive headers, and skips basic-auth, bearer-token and OAuth2 credentials, when a redirect points to a different host. This aligns with Go's net/http behavior. Callers that relied on credentials being sent to a redirect target on another host will need to target that host directly. #901 #920 #921
    • config: LoadHTTPConfigFile now resolves relative file paths (e.g. *_file credentials, http_headers files) against the config file's own directory instead of its parent directory. Configs that worked around the old behavior by prefixing paths with the config's directory name must drop that prefix. #925

    Bugfixes

    • expfmt: fix nil pointer panic when parsing empty braces {}. #922
    • model: fix Time.UnmarshalJSON for larger negative numbers. #918

    Performance

    • model: reduce allocations in Time.UnmarshalJSON. #918

    Internal

    • Synchronize common files from prometheus/prometheus. #917
    • Modernize Go. #919

    Full Changelog: https://github.com/prometheus/common/compare/v0.68.1...v0.69.0

    v0.67.2 / 2025-10-28

    What's Changed

    New Contributors

    Full Changelog: https://github.com/prometheus/common/compare/v0.67.1...v0.67.2

    v0.67.1 / 2025-10-07

    What's Changed

    Full Changelog: https://github.com/prometheus/common/compare/v0.67.0...v0.67.1

    v0.67.0 / 2025-10-07

    What's Changed

    ... (truncated)

    Commits
    • 2120573 Update common Prometheus files (#915)
    • 228386a build(deps): bump golang.org/x/net from 0.53.0 to 0.55.0 (#914)
    • b8c88b4 build(deps): bump golang.org/x/net from 0.52.0 to 0.53.0 (#903)
    • 1e0ae83 config: apply DialContextFunc to OAuth2 token-fetch transport (#911)
    • b51d01b Remove CircleCI (#910)
    • 0f3c348 Merge pull request #908 from machine424/ttlsco
    • 732a9cf fix(http_config): fix client cert rotation when no CA is configured
    • ce9215c Move interface assertions to a test file (#839)
    • 1ba5ed7 build(deps): bump golang.org/x/oauth2 from 0.34.0 to 0.36.0 (#892)
    • 8f8ada6 build(deps): bump go.yaml.in/yaml/v2 from 2.4.3 to 2.4.4 (#891)
    • Additional commits viewable in compare view

    Updates `google.golang.org/api` from 0.282.0 to 0.283.0
    Release notes

    Sourced from google.golang.org/api's releases.

    v0.283.0

    0.283.0 (2026-06-01)

    Features

    Changelog

    Sourced from google.golang.org/api's changelog.

    0.283.0 (2026-06-01)

    Features

    Commits

    Updates `google.golang.org/genai` from 1.58.0 to 1.59.0
    Release notes

    Sourced from google.golang.org/genai's releases.

    v1.59.0

    1.59.0 (2026-06-03)

    Features

    • Add Agent Platform MCP support to async generate_content (4b138c2)
    • Add transcription language code. (cc4dd9c)
    • Add TranslationConfig for live translation. (76f4126)
    • additional computer_use field support for vertex. (8831eb3)
    • Support 'additionalProperties', 'defs' and 'ref' in the GenerateContent.Schema type. (996b831)
    • Support Reinforcement Tuning in GenAI SDK (fecb49e)
    • Support ReinforcementTuning in GenAI SDK including ValidateReward API method. (c95d115)
    Changelog

    Sourced from google.golang.org/genai's changelog.

    1.59.0 (2026-06-03)

    Features

    • Add Agent Platform MCP support to async generate_content (4b138c2)
    • Add transcription language code. (cc4dd9c)
    • Add TranslationConfig for live translation. (76f4126)
    • additional computer_use field support for vertex. (8831eb3)
    • Support 'additionalProperties', 'defs' and 'ref' in the GenerateContent.Schema type. (996b831)
    • Support Reinforcement Tuning in GenAI SDK (fecb49e)
    • Support ReinforcementTuning in GenAI SDK including ValidateReward API method. (c95d115)
    Commits
    • bba8883 chore(main): release 1.59.0 (#799)
    • c95d115 feat: Support ReinforcementTuning in GenAI SDK including ValidateReward API m...
    • cc4dd9c feat: Add transcription language code.
    • d1ef633 chore: deprecate Google Maps grounding widget API fields
    • 76f4126 feat: Add TranslationConfig for live translation.
    • 4b138c2 feat: Add Agent Platform MCP support to async generate_content
    • 2468757 chore: Remove 'additionalProperties', 'defs' and 'ref' in the GenerateContent...
    • 1265072 chore: Internal cleanup
    • 8831eb3 feat: additional computer_use field support for vertex.
    • 996b831 feat: Support 'additionalProperties', 'defs' and 'ref' in the GenerateContent...
    • Additional commits viewable in compare view

    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 | 40 ++++++++++++++--------------- go.sum | 80 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/go.mod b/go.mod index 1df135ffec..236c23a031 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,11 @@ require ( github.com/alecthomas/kong v1.15.0 github.com/andybalholm/brotli v1.2.1 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 - github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 - github.com/bytedance/sonic v1.15.1 + github.com/aws/aws-sdk-go-v2 v1.41.11 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 + github.com/aws/aws-sdk-go-v2/config v1.32.22 + github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 + github.com/bytedance/sonic v1.15.2 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cohere-ai/cohere-go/v2 v2.18.0 github.com/coreos/go-oidc/v3 v3.18.0 @@ -31,10 +31,10 @@ require ( github.com/moby/moby/api v1.54.2 github.com/modelcontextprotocol/go-sdk v1.6.1 github.com/openai/openai-go v1.12.0 - github.com/openai/openai-go/v3 v3.37.0 + github.com/openai/openai-go/v3 v3.39.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/prometheus/common v0.68.1 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.42.0 github.com/tetratelabs/func-e v1.6.0 @@ -57,8 +57,8 @@ 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.282.0 - google.golang.org/genai v1.58.0 + google.golang.org/api v0.283.0 + google.golang.org/genai v1.59.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af @@ -90,17 +90,17 @@ 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.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 - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect - 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.36.0 // indirect - github.com/aws/smithy-go v1.25.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.21 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 // indirect + github.com/aws/smithy-go v1.27.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect diff --git a/go.sum b/go.sum index 51bca80c92..88f0186da2 100644 --- a/go.sum +++ b/go.sum @@ -51,36 +51,36 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k= github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9zEwjc3eH8= -github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= -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.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= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= -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.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= -github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.41.11 h1:9PRf7jyTMEUM6fuNRAJa2mO/skJfrF50rENJwf2LXqw= +github.com/aws/aws-sdk-go-v2 v1.41.11/go.mod h1:iiUX27gOXRuYaoeUVXhUpPwjJHzISfPAjjcuhUbLSVs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 h1:oRtsqWgxbpeXrOlxOoQStx2M9WNbIkPq4C4Xn1or6bc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12/go.mod h1:Zg0Oe9qT+9wcezlm1a64wGJp2qZdRElVxo/seJf7jYU= +github.com/aws/aws-sdk-go-v2/config v1.32.22 h1:Vfvp7+fYKsVCADcWOEllqEV47aIBXhNchvyDFu1B5fY= +github.com/aws/aws-sdk-go-v2/config v1.32.22/go.mod h1:0+H+0nPKbvWltf5vSIGkApv+hGbaQ4FfwTjGIYQREcw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.21 h1:0+HscFXtNa4+3buV4IlG6v5lnOdzi5TrpicFGjKHgh4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.21/go.mod h1:UE8+9t5zudFwu5k5ShC1PKArVEdOkQQdCXIHQAVNUcU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 h1:BEfN1sjtiKEdikRDxYkjZNE4tyvw/YbGWCbl3xDZgRw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27/go.mod h1:ISGSFNbOHRS+JV/17yStzRTPBUHHqF92kCpRLLyH3Nk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 h1:8sPbKi1/KRHwl5oR3qN9mUXestCeHuaRutxylnr/eVY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27/go.mod h1:QV9IVIopJ1dpQUno0f9VYDUwOEjj8u0iEJ4JiZVre3Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 h1:9d8AoASQY9UwrOSmiJ7uSM0MGUPFhnenwSvpaFfat2c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27/go.mod h1:x0rldpsnUQaQIs4Rh+Vwm9Z/0vI6BxadGtsgJfZFb8s= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 h1:eaS9vwQ5ym4Y9S6+G/K3d3lgZhxs9Sldcn/YS7cmdKY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28/go.mod h1:oTdbDr+BMs7gAYrNpD0LDTyqQfv6yOYgTDv46+xbwFY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11 h1:rFSsqDfCMPAmG70JOsYqFZCHXkyatoGa1K4YEt/BggQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.11/go.mod h1:XG68qW+YLLFH0vnSDCou43Cgj5TeAG83O5NRSJgt04Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27 h1:2/pUo42hhVmQcM21ttZoBOLHQymyUH8qEnZGTIuGBT8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.27/go.mod h1:p7hwgbwompjCRNTdB3ytlldddNt1rDBgVVMqWEVG1II= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 h1:t6U7sowMfOjTeZXtDOtgEJXsoJyX4MDag+sfWGwUM9M= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.3/go.mod h1:WhO1EH3phjFWValQDsExaxncgEWJsHeoTvuyQAj3jwU= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 h1:TUV8oytPCX1PfVgZn0N8/sPZx7T0YasaMCBHox1erlw= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.1/go.mod h1:tEL1hqCrkgwrDVL04HuLxz1SLUXdh+4kKhWv1pXKeiY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 h1:p9+Fizo2sUB6wI5Yb3K5lmykQAGs5JrKLBV/me6613Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4/go.mod h1:0x10Wy0dVS4Gn552xhHY5th2QdYpfJf44EsfyYGV194= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 h1:r/vUkpLilfCA3sxbRnkHbJejaoVHEdj4FEhv+Zva4DU= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.1/go.mod h1:t01JURC8Fe5M+7R1K0vzIZ2NT04HqvZR+FjlHrHDT2A= +github.com/aws/smithy-go v1.27.0 h1:ZoFioDKJxkSIW2otF9T0aPtNlUwhdVCcuZh/rzH9Hus= +github.com/aws/smithy-go v1.27.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -91,8 +91,8 @@ github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJ github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw= -github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= +github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo= +github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -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.37.0 h1:4OG68yZgnxZpwzebO+ZDUNkFJKKwKgzilMQq30nsouE= -github.com/openai/openai-go/v3 v3.37.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.39.0 h1:WgLGgMOOdQDkZyo8YIhzUNXRXlEc+OJfU4EKP5Qp6AA= +github.com/openai/openai-go/v3 v3.39.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= @@ -384,8 +384,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= +github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= @@ -624,10 +624,10 @@ 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.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I= -google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= -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/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik= +google.golang.org/api v0.283.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= +google.golang.org/genai v1.59.0 h1:xp+ydkJFW8hO0hTUaAkr8TrLM9HFP3NYAwFhPd0nDqA= +google.golang.org/genai v1.59.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU= 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-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= From 08487cac3d3f108e80d4ba3e1e0953fd6be5172f Mon Sep 17 00:00:00 2001 From: Erica Hughberg Date: Mon, 22 Jun 2026 22:23:11 +0200 Subject: [PATCH 37/59] docs: add security policy (#2269) --- SECURITY.md | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..54dd586452 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,115 @@ +# Security Policy + +The Envoy AI Gateway maintainers and community take security seriously. We +appreciate your efforts to responsibly disclose your findings, and will make +every effort to acknowledge your contributions. + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues, +discussions, pull requests, or any other public channel.** + +Instead, please report vulnerabilities privately using one of the following +channels: + +1. **Preferred:** Open a + [GitHub Security Advisory](https://github.com/envoyproxy/ai-gateway/security/advisories/new) + ("Report a vulnerability" on the repository's **Security** tab). This allows + the maintainers to triage the report and collaborate with you privately, + directly on GitHub. +2. **Alternative:** Email the Envoy AI Gateway security team at + [envoy-ai-gateway-security@googlegroups.com](mailto:envoy-ai-gateway-security@googlegroups.com). + +If the vulnerability relates to Envoy Proxy or Envoy Gateway (the components +that Envoy AI Gateway is built on top of) rather than Envoy AI Gateway itself, +please report it to the relevant upstream project: + +- Envoy Proxy: https://github.com/envoyproxy/envoy/security/advisories/new +- Envoy Gateway: email envoy-gateway-security@googlegroups.com (see the + [Envoy Gateway security policy](https://github.com/envoyproxy/gateway/blob/main/SECURITY.md)) + +### What to Include + +To help us triage and resolve the issue as quickly as possible, please include +as much of the following information as you can: + +- A description of the vulnerability and its potential impact. +- The affected version(s) of Envoy AI Gateway. +- Steps to reproduce the issue, including any sample configuration, requests, or + proof-of-concept code. +- Any known workarounds or suggested remediation. + +> **Note**: If your report includes data that has privacy concerns, please +> sanitize the data prior to sharing it. + +## Disclosure Process + +After a vulnerability is reported, the maintainers will follow this process: + +1. **Acknowledgement** — We aim to acknowledge receipt of your report within + **3 business days**. +2. **Triage** — We will investigate to validate the report, determine the + affected versions, and assess the severity and impact. +3. **Fix and coordination** — We will work on a fix and coordinate a release + timeline with you. We aim to resolve or publicly disclose privately reported + issues within **90 days** of the initial report. +4. **Release and disclosure** — Once a fix is available, we will publish the + patched release and an accompanying security advisory. + +We will keep you informed of the progress throughout the process. If you would +like to be credited for the discovery, please let us know, and we will include +an acknowledgement in the advisory (with your consent). + +Please keep the details of any reported vulnerability confidential until a fix +has been released and the embargo has been lifted, so that users have a chance +to upgrade. + +## Supported Versions + +Envoy AI Gateway follows the support policy described in +[RELEASES.md](./RELEASES.md), aligned with the +[Envoy Gateway release support policy](https://gateway.envoyproxy.io/). Minor +releases are cut on a roughly quarterly cadence, and each minor release is +supported for **6 months** after its release date. In practice this means the +**two most recent minor releases** (the current and the previous `v1.x` line) +are supported at any given time. + +Security fixes are developed on the `main` branch and backported as patch +releases to every minor release that is still within its support window. We +strongly recommend that all users run a supported, up-to-date `v1.x` version. +Reports against unsupported or end-of-life versions may be addressed by asking +you to reproduce the issue on a supported version. + +## Security Updates + +Security patches and advisories are announced through: + +- The [GitHub Releases page](https://github.com/envoyproxy/ai-gateway/releases) +- The [GitHub Security Advisories page](https://github.com/envoyproxy/ai-gateway/security/advisories) + +We recommend watching the repository and subscribing to these channels to stay +informed about security updates. + +## Best Practices for Secure Usage + +To minimize security risks when running Envoy AI Gateway: + +- Run the latest supported version and apply patches promptly. +- Follow the principle of least privilege when configuring credentials and + `BackendSecurityPolicy` resources, and store secrets using Kubernetes Secrets + (or an external secret manager) rather than in plaintext configuration. +- Restrict network access to the control plane and management interfaces. +- Regularly review the security-related documentation under the project + [docs](./site/docs/capabilities/security). + +## Contact + +If you have any general (non-sensitive) questions about this security policy, +please reach out to the maintainers listed in [MAINTAINERS.md](./MAINTAINERS.md). +To report a vulnerability, always use one of the private channels described +above — the [GitHub Security Advisory](https://github.com/envoyproxy/ai-gateway/security/advisories/new) +flow or the security team at +[envoy-ai-gateway-security@googlegroups.com](mailto:envoy-ai-gateway-security@googlegroups.com) +— rather than a public channel. + +Thank you for helping to keep Envoy AI Gateway and its users secure. From 8766e8cca5a94fc4dc2b5b6682a10682b75159e7 Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Mon, 22 Jun 2026 22:42:34 +0200 Subject: [PATCH 38/59] controller: add consistency check for gateway object namespaces (#2274) **Description** The previous gateway controller code made assumptions about the namespace consistency across gateway resources. This change adds an explicit check and returns an error if there are inconsistencies. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** N/A Signed-off-by: Ignasi Barrera --- cmd/aigw/translate.go | 2 +- cmd/controller/main.go | 8 ++ cmd/controller/main_test.go | 3 + internal/controller/controller.go | 5 +- internal/controller/gateway.go | 83 +++++++++++-------- internal/controller/gateway_test.go | 59 +++++++++---- .../ai-gateway-helm/templates/deployment.yaml | 1 + 7 files changed, 108 insertions(+), 53 deletions(-) diff --git a/cmd/aigw/translate.go b/cmd/aigw/translate.go index 12f11ecbf6..eb0d20876f 100644 --- a/cmd/aigw/translate.go +++ b/cmd/aigw/translate.go @@ -247,7 +247,7 @@ func translateCustomResourceObjects( mcpC := controller.NewMCPRouteController(fakeClient, fakeClientSet, logr.FromSlogHandler(logger.Handler()), make(chan event.GenericEvent, eventChanBuffer), ) - gwC := controller.NewGatewayController(fakeClient, fakeClientSet, logr.FromSlogHandler(logger.Handler()), + gwC := controller.NewGatewayController(fakeClient, fakeClientSet, logr.FromSlogHandler(logger.Handler()), "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "debug", true, func() string { return "aigw-translate" }, false, diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 627b21c28f..0cf92704b6 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -38,6 +38,7 @@ import ( ) type flags struct { + envoyGatewayNamespace string extProcLogLevel string extProcEnableRedaction bool extProcImage string @@ -108,6 +109,11 @@ func parseWatchNamespaces(s string) []string { func parseAndValidateFlags(args []string) (*flags, error) { fs := flag.NewFlagSet("AI Gateway Controller", flag.ContinueOnError) + envoyGatewayNamespace := fs.String( + "envoyGatewayNamespace", + "envoy-gateway-system", + "The namespace where Envoy Gateway is deployed.", + ) extProcLogLevelPtr := fs.String( "extProcLogLevel", "info", @@ -328,6 +334,7 @@ func parseAndValidateFlags(args []string) (*flags, error) { } return &flags{ + envoyGatewayNamespace: *envoyGatewayNamespace, extProcLogLevel: *extProcLogLevelPtr, extProcEnableRedaction: *extProcEnableRedactionPtr, extProcImage: *extProcImagePtr, @@ -444,6 +451,7 @@ func main() { // Start the controller. if err := controller.StartControllers(ctx, mgr, k8sConfig, ctrl.Log.WithName("controller"), &controller.Options{ + EnvoyGatewayNamespace: parsedFlags.envoyGatewayNamespace, ExtProcImage: parsedFlags.extProcImage, ExtProcImagePullPolicy: parsedFlags.extProcImagePullPolicy, ExtProcLogLevel: parsedFlags.extProcLogLevel, diff --git a/cmd/controller/main_test.go b/cmd/controller/main_test.go index 9ab36f72c5..a48bc080fe 100644 --- a/cmd/controller/main_test.go +++ b/cmd/controller/main_test.go @@ -22,6 +22,7 @@ import ( func Test_parseAndValidateFlags(t *testing.T) { t.Run("no flags", func(t *testing.T) { f, err := parseAndValidateFlags([]string{}) + require.Equal(t, "envoy-gateway-system", f.envoyGatewayNamespace) require.Equal(t, "info", f.extProcLogLevel) require.False(t, f.extProcEnableRedaction) require.Equal(t, "docker.io/envoyproxy/ai-gateway-extproc:latest", f.extProcImage) @@ -48,6 +49,7 @@ func Test_parseAndValidateFlags(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { args := []string{ + tc.dash + "envoyGatewayNamespace=eg-system", tc.dash + "extProcLogLevel=debug", tc.dash + "extProcEnableRedaction=true", tc.dash + "extProcImage=example.com/extproc:latest", @@ -70,6 +72,7 @@ func Test_parseAndValidateFlags(t *testing.T) { tc.dash + "mcpFallbackSessionEncryptionIterations=200", } f, err := parseAndValidateFlags(args) + require.Equal(t, "eg-system", f.envoyGatewayNamespace) require.Equal(t, "debug", f.extProcLogLevel) require.True(t, f.extProcEnableRedaction) require.Equal(t, "example.com/extproc:latest", f.extProcImage) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 9af761dbc7..3c98c72b30 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -106,6 +106,8 @@ type Options struct { EndpointPrefixes string // RateLimitRunner is the xDS runner that serves rate limit configs to the rate limit service. RateLimitRunner *runner.Runner + // EnvoyGatewayNamespace is the namespace where Envoy Gateway is deployed. + EnvoyGatewayNamespace string } // StartControllers starts the controllers for the AI Gateway. @@ -127,7 +129,8 @@ func StartControllers(ctx context.Context, mgr manager.Manager, config *rest.Con gatewayEventChan := make(chan event.GenericEvent, 100) gatewayC := NewGatewayController(c, kubernetes.NewForConfigOrDie(config), - logger.WithName("gateway"), options.ExtProcImage, options.ExtProcLogLevel, false, uuid.NewString, isKubernetes133OrLater(versionInfo, logger)) + logger.WithName("gateway"), options.EnvoyGatewayNamespace, options.ExtProcImage, options.ExtProcLogLevel, + false, uuid.NewString, isKubernetes133OrLater(versionInfo, logger)) if err = TypedControllerBuilderForCRD(mgr, &gwapiv1.Gateway{}). WatchesRawSource(source.Channel( gatewayEventChan, diff --git a/internal/controller/gateway.go b/internal/controller/gateway.go index 12acd9ae38..6e331f0f8e 100644 --- a/internal/controller/gateway.go +++ b/internal/controller/gateway.go @@ -49,7 +49,7 @@ const ( // extProcImage is the image of the external processor sidecar container which will be used // to check if the pods of the gateway deployment need to be rolled out. func NewGatewayController( - client client.Client, kube kubernetes.Interface, logger logr.Logger, + client client.Client, kube kubernetes.Interface, logger logr.Logger, envoyGatewayNamespace string, extProcImage string, extProcLogLevel string, standAlone bool, uuidFn func() string, extProcAsSideCar bool, ) *GatewayController { uf := uuidFn @@ -57,24 +57,26 @@ func NewGatewayController( uf = uuid.NewString } return &GatewayController{ - client: client, - kube: kube, - logger: logger, - extProcImage: extProcImage, - extProcLogLevel: extProcLogLevel, - standAlone: standAlone, - uuidFn: uf, - extProcAsSideCar: extProcAsSideCar, + client: client, + kube: kube, + logger: logger, + envoyGatewayNamespace: envoyGatewayNamespace, + extProcImage: extProcImage, + extProcLogLevel: extProcLogLevel, + standAlone: standAlone, + uuidFn: uf, + extProcAsSideCar: extProcAsSideCar, } } // GatewayController implements reconcile.TypedReconciler for gwapiv1.Gateway. type GatewayController struct { - client client.Client - kube kubernetes.Interface - logger logr.Logger - extProcImage string // The image of the external processor sidecar container. - extProcLogLevel string // The log level for the extproc container. + client client.Client + kube kubernetes.Interface + logger logr.Logger + envoyGatewayNamespace string // The namespace where Envoy Gateway is deployed. + extProcImage string // The image of the external processor sidecar container. + extProcLogLevel string // The log level for the extproc container. // standAlone indicates whether the controller is running in standalone mode. standAlone bool uuidFn func() string // Function to generate a new UUID for the filter config. @@ -1145,32 +1147,45 @@ func (c *GatewayController) getObjectsForGateway(ctx context.Context, gw *gwapiv listOption := metav1.ListOptions{LabelSelector: fmt.Sprintf( "%s=%s,%s=%s", egOwningGatewayNameLabel, gw.Name, egOwningGatewayNamespaceLabel, gw.Namespace, )} - var ps *corev1.PodList - ps, err = c.kube.CoreV1().Pods("").List(ctx, listOption) - if err != nil { - err = fmt.Errorf("failed to list pods: %w", err) - return - } - pods = ps.Items - var ds *appsv1.DeploymentList - ds, err = c.kube.AppsV1().Deployments("").List(ctx, listOption) - if err != nil { - err = fmt.Errorf("failed to list deployments: %w", err) - return + var distinctNamespaces []string + for _, ns := range []string{gw.Namespace, c.envoyGatewayNamespace} { + var ps *corev1.PodList + ps, err = c.kube.CoreV1().Pods(ns).List(ctx, listOption) + if err != nil { + err = fmt.Errorf("failed to list pods in namespace %s: %w", ns, err) + return + } + pods = append(pods, ps.Items...) + + var ds *appsv1.DeploymentList + ds, err = c.kube.AppsV1().Deployments(ns).List(ctx, listOption) + if err != nil { + err = fmt.Errorf("failed to list deployments in namespace %s: %w", ns, err) + return + } + deployments = append(deployments, ds.Items...) + + var dss *appsv1.DaemonSetList + dss, err = c.kube.AppsV1().DaemonSets(ns).List(ctx, listOption) + if err != nil { + err = fmt.Errorf("failed to list daemonsets in namespace %s: %w", ns, err) + return + } + daemonSets = append(daemonSets, dss.Items...) + + if len(ps.Items) > 0 || len(ds.Items) > 0 || len(dss.Items) > 0 { + distinctNamespaces = append(distinctNamespaces, ns) + } } - deployments = ds.Items - var dss *appsv1.DaemonSetList - dss, err = c.kube.AppsV1().DaemonSets("").List(ctx, listOption) - if err != nil { - err = fmt.Errorf("failed to list daemonsets: %w", err) + // All pods, deployments, and daemonsets should be in the same namespace. + // Otherwise, it would be a bug in the EG or the disruptive configuration change of EG. + if len(distinctNamespaces) > 1 { + err = fmt.Errorf("found gateway-labeled objects in multiple namespaces: %v", distinctNamespaces) return } - daemonSets = dss.Items - // We assume that all pods, deployments, and daemonsets are in the same namespace. Otherwise, it would be a bug in the EG - // or the disruptive configuration change of EG. if len(pods) > 0 { namespace = pods[0].Namespace } diff --git a/internal/controller/gateway_test.go b/internal/controller/gateway_test.go index 4cf4809acf..bd1063fd1a 100644 --- a/internal/controller/gateway_test.go +++ b/internal/controller/gateway_test.go @@ -53,7 +53,7 @@ func TestGatewayController_Reconcile(t *testing.T) { fakeClient := requireNewFakeClientWithIndexes(t) fakeKube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, fakeKube, ctrl.Log, + c := NewGatewayController(fakeClient, fakeKube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const namespace = "ns" @@ -175,7 +175,7 @@ func TestGatewayController_reconcileFilterConfigSecret(t *testing.T) { fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -329,7 +329,7 @@ func TestGatewayController_reconcileFilterConfigSecret_HostnameScopedModels(t *t fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -425,7 +425,7 @@ func TestGatewayController_reconcileFilterConfigSecret_AllUnscopedRoutesLeaveUns fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -479,7 +479,7 @@ func TestGatewayController_reconcileFilterConfigSecret_RouteLevelLLMRequestCostA fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -580,7 +580,7 @@ func TestGatewayController_reconcileFilterConfigSecret_RouteLevelLLMRequestCostA fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -636,7 +636,7 @@ func TestGatewayController_reconcileFilterConfigSecret_InvalidCELExpression(t *t fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -675,7 +675,7 @@ func TestGatewayController_reconcileFilterConfigSecret_SkipsDeletedRoutes(t *tes fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -786,8 +786,7 @@ func TestGatewayController_bspToFilterAPIBackendAuth(t *testing.T) { fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, - + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const namespace = "ns" @@ -987,7 +986,7 @@ func TestGatewayController_bspToFilterAPIBackendAuth(t *testing.T) { func TestGatewayController_bspToFilterAPIBackendAuth_ErrorCases(t *testing.T) { fakeClient := requireNewFakeClientWithIndexes(t) - c := NewGatewayController(fakeClient, fake2.NewClientset(), ctrl.Log, + c := NewGatewayController(fakeClient, fake2.NewClientset(), ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) ctx := context.Background() @@ -1048,7 +1047,7 @@ func TestGatewayController_bspToFilterAPIBackendAuth_ErrorCases(t *testing.T) { func TestGatewayController_GetSecretData_ErrorCases(t *testing.T) { fakeClient := requireNewFakeClientWithIndexes(t) - c := NewGatewayController(fakeClient, fake2.NewClientset(), ctrl.Log, + c := NewGatewayController(fakeClient, fake2.NewClientset(), ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) ctx := context.Background() @@ -1074,7 +1073,7 @@ func TestGatewayController_annotateGatewayPods(t *testing.T) { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) const v2Container = "ai-gateway-extproc:v2" const logLevel = "info" - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", v2Container, logLevel, false, nil, true) t.Run("pod with extproc", func(t *testing.T) { pod, err := kube.CoreV1().Pods(egNamespace).Create(t.Context(), &corev1.Pod{ @@ -1635,7 +1634,7 @@ func TestGatewayController_annotateDaemonSetGatewayPods(t *testing.T) { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) const v2Container = "ai-gateway-extproc:v2" const logLevel = "info" - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", v2Container, logLevel, false, nil, true) t.Run("pod without extproc", func(t *testing.T) { @@ -2048,7 +2047,7 @@ func TestGatewayController_backendWithMaybeBSP(t *testing.T) { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) const v2Container = "ai-gateway-extproc:v2" const logLevel = "info" - c := NewGatewayController(fakeClient, kube, ctrl.Log, v2Container, logLevel, false, nil, true) + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", v2Container, logLevel, false, nil, true) _, _, err := c.backendWithMaybeBSP(t.Context(), "foo", "bar") require.ErrorContains(t, err, `aiservicebackends.aigateway.envoyproxy.io "bar" not found`) @@ -2107,7 +2106,7 @@ func TestGatewayController_reconcileFilterMCPConfigSecret(t *testing.T) { fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -2599,7 +2598,7 @@ func TestGatewayController_reconcileFilterConfigSecret_GlobalDefaults(t *testing fakeClient := requireNewFakeClientWithIndexes(t) kube := fake2.NewClientset() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zap.Options{Development: true, Level: zapcore.DebugLevel}))) - c := NewGatewayController(fakeClient, kube, ctrl.Log, + c := NewGatewayController(fakeClient, kube, ctrl.Log, "envoy-gateway-system", "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) const gwNamespace = "ns" @@ -2812,3 +2811,29 @@ func Test_mergeBodyMutations(t *testing.T) { }) } } + +func TestGatewayController_getObjectsForGatewayNamespaceInconsistency(t *testing.T) { + const gwName, gwNamespace, egNamespace = "gw", "ns", "envoy-gateway-system" + labels := map[string]string{ + egOwningGatewayNameLabel: gwName, + egOwningGatewayNamespaceLabel: gwNamespace, + } + gw := &gwapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: gwName, Namespace: gwNamespace}} + + kube := fake2.NewClientset() + c := NewGatewayController(requireNewFakeClientWithIndexes(t), kube, ctrl.Log, egNamespace, + "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) + + // Place a pod in the gateway namespace and a pod in the envoy-gateway namespace so that + // objects are found in two distinct namespaces, which should trigger the error. + for _, ns := range []string{gwNamespace, egNamespace} { + _, err := kube.CoreV1().Pods(ns).Create(t.Context(), &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-" + ns, Namespace: ns, Labels: labels}, + }, metav1.CreateOptions{}) + require.NoError(t, err) + } + + _, _, _, _, err := c.getObjectsForGateway(t.Context(), gw) + require.Error(t, err) + require.Contains(t, err.Error(), "found gateway-labeled objects in multiple namespaces") +} diff --git a/manifests/charts/ai-gateway-helm/templates/deployment.yaml b/manifests/charts/ai-gateway-helm/templates/deployment.yaml index 65556cdfd6..40862befdf 100644 --- a/manifests/charts/ai-gateway-helm/templates/deployment.yaml +++ b/manifests/charts/ai-gateway-helm/templates/deployment.yaml @@ -50,6 +50,7 @@ spec: - --extProcImage={{ .Values.extProc.image.repository }}:{{ .Values.extProc.image.tag | default .Chart.AppVersion }} - --extProcImagePullPolicy={{ .Values.extProc.imagePullPolicy }} - --extProcLogLevel={{ .Values.extProc.logLevel }} + - --envoyGatewayNamespace={{ .Values.envoyGateway.namespace }} {{- if .Values.extProc.enableRedaction }} - --extProcEnableRedaction=true {{- end }} From 7f613356cec2523c86689d7d23de0e9cebecfd5f Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Mon, 22 Jun 2026 23:14:42 +0200 Subject: [PATCH 39/59] mcp: cover all branches on nil checks for json response (#2275) **Description** Fix the a nil check on MCP session to capture all branches. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** N/A Signed-off-by: Ignasi Barrera --- internal/mcpproxy/handlers.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/mcpproxy/handlers.go b/internal/mcpproxy/handlers.go index 74dd528d4e..9a82bf8118 100644 --- a/internal/mcpproxy/handlers.go +++ b/internal/mcpproxy/handlers.go @@ -302,17 +302,17 @@ func (m *mcpRequestContext) servePOST(w http.ResponseWriter, r *http.Request) { switch msg := rawMsg.(type) { case *jsonrpc.Response: + // We do require a Session ID. If it is not present, a 400 Bad Request response should be returned: + // https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#session-management + if s == nil { + errType = metrics.MCPErrorInvalidSessionID + onErrorResponse(w, http.StatusBadRequest, "missing session ID") + return + } if doNotForwardResponseToBackends(msg) { w.Header().Set(sessionIDHeader, string(s.clientGatewaySessionID())) w.WriteHeader(http.StatusAccepted) } else { - // We do require a Session ID. If it is not present, a 400 Bad Request response should be returned: - // https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#session-management - if s == nil { - errType = metrics.MCPErrorInvalidSessionID - onErrorResponse(w, http.StatusBadRequest, "missing session ID") - return - } m.l.Debug("Decoded MCP response", slog.Any("response", msg)) result, err = m.handleClientToServerResponse(ctx, s, w, msg) } From 21d65d604e4cdc7956a680717d913821a03c7bb7 Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Mon, 22 Jun 2026 23:42:28 +0200 Subject: [PATCH 40/59] tracing: set an upper bound for content blocks (#2246) **Description** Set an upper bound for content blocks to prevent unreasonable values from eating up all memory. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** N/A Signed-off-by: Ignasi Barrera --- .../openinference/anthropic/messages.go | 5 ++ .../openinference/anthropic/messages_test.go | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/internal/tracing/openinference/anthropic/messages.go b/internal/tracing/openinference/anthropic/messages.go index fe031a489e..9c920400f1 100644 --- a/internal/tracing/openinference/anthropic/messages.go +++ b/internal/tracing/openinference/anthropic/messages.go @@ -270,6 +270,11 @@ func convertSSEToResponse(chunks []*anthropic.MessagesStreamChunk) *anthropic.Me case event.ContentBlockStart != nil: idx := event.ContentBlockStart.Index + // Guard against negative or unreasonably large indices from a hostile upstream. + const maxContentBlocks = 1000 + if idx < 0 || idx >= maxContentBlocks { + continue + } // Grow slice if needed. if idx >= len(response.Content) { newContent := make([]anthropic.MessagesContentBlock, idx+1) diff --git a/internal/tracing/openinference/anthropic/messages_test.go b/internal/tracing/openinference/anthropic/messages_test.go index 6a6cf3bbff..707a96d454 100644 --- a/internal/tracing/openinference/anthropic/messages_test.go +++ b/internal/tracing/openinference/anthropic/messages_test.go @@ -210,6 +210,62 @@ func TestConvertSSEToResponse(t *testing.T) { StopSequence: func() *string { s := ""; return &s }(), }, }, + { + name: "negative content block index is ignored", + chunks: []*anthropic.MessagesStreamChunk{ + { + MessageStart: &anthropic.MessagesStreamChunkMessageStart{ + ID: "msg_neg", + Model: "claude-3", + Role: "assistant", + Usage: &anthropic.Usage{InputTokens: 5}, + }, + }, + { + ContentBlockStart: &anthropic.MessagesStreamChunkContentBlockStart{ + Index: -1, + ContentBlock: anthropic.MessagesContentBlock{ + Text: &anthropic.TextBlock{Type: "text", Text: "bad"}, + }, + }, + }, + }, + want: &anthropic.MessagesResponse{ + ID: "msg_neg", + Model: "claude-3", + Role: "assistant", + Usage: &anthropic.Usage{InputTokens: 5}, + Content: []anthropic.MessagesContentBlock{}, + }, + }, + { + name: "content block index at cap is ignored", + chunks: []*anthropic.MessagesStreamChunk{ + { + MessageStart: &anthropic.MessagesStreamChunkMessageStart{ + ID: "msg_cap", + Model: "claude-3", + Role: "assistant", + Usage: &anthropic.Usage{InputTokens: 5}, + }, + }, + { + ContentBlockStart: &anthropic.MessagesStreamChunkContentBlockStart{ + Index: 1000, // == maxContentBlocks, should be rejected + ContentBlock: anthropic.MessagesContentBlock{ + Text: &anthropic.TextBlock{Type: "text", Text: "bad"}, + }, + }, + }, + }, + want: &anthropic.MessagesResponse{ + ID: "msg_cap", + Model: "claude-3", + Role: "assistant", + Usage: &anthropic.Usage{InputTokens: 5}, + Content: []anthropic.MessagesContentBlock{}, + }, + }, } for _, tt := range tests { From 7eead6c7c169c3a1ab1ba8f5bfdc09e410f2ed5d Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Tue, 23 Jun 2026 00:02:39 +0200 Subject: [PATCH 41/59] translator: add nil checks for tool call ids before dereferencing the pointers (#2242) **Description** Adds nil checks for tool call IDs before dereferencing the pointers. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** N/A Signed-off-by: Ignasi Barrera --- internal/translator/anthropic_helper.go | 4 ++++ internal/translator/gemini_helper.go | 3 +++ internal/translator/openai_awsbedrock.go | 3 +++ 3 files changed, 10 insertions(+) diff --git a/internal/translator/anthropic_helper.go b/internal/translator/anthropic_helper.go index a852d1a678..914d70f212 100644 --- a/internal/translator/anthropic_helper.go +++ b/internal/translator/anthropic_helper.go @@ -429,6 +429,10 @@ func openAIMessageToAnthropicMessageRoleAssistant(openAiMessage *openai.ChatComp // Handle tool_calls (if any). for i := range openAiMessage.ToolCalls { toolCall := &openAiMessage.ToolCalls[i] + if toolCall.ID == nil { + err = fmt.Errorf("%w: tool_call at index %d is missing required field 'id'", internalapi.ErrInvalidRequestBody, i) + return + } var input map[string]any if err = json.Unmarshal([]byte(toolCall.Function.Arguments), &input); err != nil { err = fmt.Errorf("failed to unmarshal tool call arguments: %w", err) diff --git a/internal/translator/gemini_helper.go b/internal/translator/gemini_helper.go index d3ac0f5627..be32bd9a64 100644 --- a/internal/translator/gemini_helper.go +++ b/internal/translator/gemini_helper.go @@ -277,6 +277,9 @@ func assistantMsgToGeminiParts(msg *openai.ChatCompletionAssistantMessageParam) // Handle tool calls in the assistant message. knownToolCalls := make(map[string]string) for i, toolCall := range msg.ToolCalls { + if toolCall.ID == nil { + return nil, nil, fmt.Errorf("%w: tool_call at index %d is missing required field 'id'", internalapi.ErrInvalidRequestBody, i) + } knownToolCalls[*toolCall.ID] = toolCall.Function.Name var parsedArgs map[string]any if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &parsedArgs); err != nil { diff --git a/internal/translator/openai_awsbedrock.go b/internal/translator/openai_awsbedrock.go index 39afb833af..7f10ad6764 100644 --- a/internal/translator/openai_awsbedrock.go +++ b/internal/translator/openai_awsbedrock.go @@ -414,6 +414,9 @@ func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) openAIMessageToBedrockMes for i := range openAiMessage.ToolCalls { toolCall := &openAiMessage.ToolCalls[i] + if toolCall.ID == nil { + return nil, fmt.Errorf("%w: tool_call at index %d is missing required field 'id'", internalapi.ErrInvalidRequestBody, i) + } input, err := unmarshalToolCallArguments(toolCall.Function.Arguments) if err != nil { return nil, err From 345ee3e1b11f6d818097cf077d442b1ad2f78aa6 Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Tue, 23 Jun 2026 00:20:50 +0200 Subject: [PATCH 42/59] chore: configure LogValue in filterapi to redact sensitive info from logs (#2245) **Description** Implements the `slog.LogValuer` interface in the internal FilterAPI structs to avoid leaking sensitive fields into the logs if those types are used in log messages. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** N/A --------- Signed-off-by: Ignasi Barrera Co-authored-by: Aaron Choo --- internal/filterapi/filterconfig.go | 46 ++++++++++++++++++++ internal/filterapi/filterconfig_test.go | 57 +++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/internal/filterapi/filterconfig.go b/internal/filterapi/filterconfig.go index c61d1970b1..d4b0b15335 100644 --- a/internal/filterapi/filterconfig.go +++ b/internal/filterapi/filterconfig.go @@ -12,6 +12,7 @@ package filterapi import ( + "log/slog" "os" "time" @@ -222,30 +223,58 @@ type AWSAuth struct { Region string `json:"region"` } +// LogValue implements slog.LogValuer for AWSAuth to redact sensitive information. +func (a AWSAuth) LogValue() slog.Value { + return slog.GroupValue( + slog.String("credentialFileLiteral", "[REDACTED]"), + slog.String("region", a.Region), + ) +} + // APIKeyAuth defines the file that will be mounted to the external proc. type APIKeyAuth struct { // Key is the API key as a literal string. Key string `json:"key"` } +// LogValue implements slog.LogValuer for APIKeyAuth to redact sensitive information. +func (a APIKeyAuth) LogValue() slog.Value { + return slog.GroupValue(slog.String("key", "[REDACTED]")) +} + // AzureAPIKeyAuth defines the Azure OpenAI API key. type AzureAPIKeyAuth struct { // Key is the Azure API key as a literal string. Key string `json:"key"` } +// LogValue implements slog.LogValuer for AzureAPIKeyAuth to redact sensitive information. +func (a AzureAPIKeyAuth) LogValue() slog.Value { + return slog.GroupValue(slog.String("key", "[REDACTED]")) +} + // AnthropicAPIKeyAuth defines the Anthropic API key. type AnthropicAPIKeyAuth struct { // Key is the Anthropic API key as a literal string. Key string `json:"key"` } +// LogValue implements slog.LogValuer for AnthropicAPIKeyAuth to redact sensitive information. +func (a AnthropicAPIKeyAuth) LogValue() slog.Value { + return slog.GroupValue(slog.String("key", "[REDACTED]")) +} + // AzureAuth defines the file containing azure access token that will be mounted to the external proc. type AzureAuth struct { // AccessToken is the access token as a literal string. AccessToken string `json:"accessToken"` } +// LogValue implements slog.LogValuer for AzureAuth to redact sensitive information. +func (a AzureAuth) LogValue() slog.Value { + return slog.GroupValue(slog.String("accessToken", "[REDACTED]")) +} + // GCPAuth defines the GCP authentication configuration used to access Google Cloud AI services. type GCPAuth struct { // AccessToken is the access token as a literal string. @@ -262,6 +291,15 @@ type GCPAuth struct { ProjectName string `json:"projectName"` } +// LogValue implements slog.LogValuer for GCPAuth to redact sensitive information. +func (g GCPAuth) LogValue() slog.Value { + return slog.GroupValue( + slog.String("accessToken", "[REDACTED]"), + slog.String("region", g.Region), + slog.String("projectName", g.ProjectName), + ) +} + // HTTPHeaderMutation defines the mutation of HTTP headers that will be applied to the request type HTTPHeaderMutation struct { // Set overwrites the request with the given header (name, value) @@ -282,6 +320,14 @@ type HTTPHeader struct { Value string `json:"value"` } +// LogValue implements slog.LogValuer for HTTPHeader to redact sensitive information. +func (h HTTPHeader) LogValue() slog.Value { + return slog.GroupValue( + slog.String("name", h.Name), + slog.String("value", "[REDACTED]"), + ) +} + // HTTPBodyMutation defines the mutation of HTTP request body JSON fields that will be applied to the request type HTTPBodyMutation struct { // Set overwrites/adds the request body with the given JSON field (name, value) diff --git a/internal/filterapi/filterconfig_test.go b/internal/filterapi/filterconfig_test.go index 455827ea74..7522e1bc03 100644 --- a/internal/filterapi/filterconfig_test.go +++ b/internal/filterapi/filterconfig_test.go @@ -6,6 +6,7 @@ package filterapi_test import ( + "log/slog" "os" "path" "testing" @@ -61,3 +62,59 @@ func TestVersionedAPISchemaAnthropicPrefix(t *testing.T) { Prefix: "gateway/v1", }.AnthropicPrefix()) } + +// logAttrs extracts the key→value map from a slog.KindGroup Value. +func logAttrs(v slog.Value) map[string]string { + result := make(map[string]string) + for _, a := range v.Group() { + result[a.Key] = a.Value.String() + } + return result +} + +func TestAWSAuthLogValue(t *testing.T) { + a := filterapi.AWSAuth{CredentialFileLiteral: "secret-creds", Region: "us-east-1"} + attrs := logAttrs(a.LogValue()) + require.Equal(t, "[REDACTED]", attrs["credentialFileLiteral"]) + require.Equal(t, "us-east-1", attrs["region"]) +} + +func TestAPIKeyAuthLogValue(t *testing.T) { + a := filterapi.APIKeyAuth{Key: "my-api-key"} + attrs := logAttrs(a.LogValue()) + require.Equal(t, "[REDACTED]", attrs["key"]) + require.NotContains(t, attrs["key"], "my-api-key") +} + +func TestAzureAPIKeyAuthLogValue(t *testing.T) { + a := filterapi.AzureAPIKeyAuth{Key: "azure-secret-key"} + attrs := logAttrs(a.LogValue()) + require.Equal(t, "[REDACTED]", attrs["key"]) +} + +func TestAnthropicAPIKeyAuthLogValue(t *testing.T) { + a := filterapi.AnthropicAPIKeyAuth{Key: "anthropic-secret-key"} + attrs := logAttrs(a.LogValue()) + require.Equal(t, "[REDACTED]", attrs["key"]) +} + +func TestAzureAuthLogValue(t *testing.T) { + a := filterapi.AzureAuth{AccessToken: "my-access-token"} + attrs := logAttrs(a.LogValue()) + require.Equal(t, "[REDACTED]", attrs["accessToken"]) +} + +func TestGCPAuthLogValue(t *testing.T) { + g := filterapi.GCPAuth{AccessToken: "gcp-token", Region: "us-central1", ProjectName: "my-project"} + attrs := logAttrs(g.LogValue()) + require.Equal(t, "[REDACTED]", attrs["accessToken"]) + require.Equal(t, "us-central1", attrs["region"]) + require.Equal(t, "my-project", attrs["projectName"]) +} + +func TestHTTPHeaderLogValue(t *testing.T) { + h := filterapi.HTTPHeader{Name: "authorization", Value: "Bearer secret-token"} + attrs := logAttrs(h.LogValue()) + require.Equal(t, "authorization", attrs["name"]) + require.Equal(t, "[REDACTED]", attrs["value"]) +} From 2721126c6e5fdd3be9e51d62e059dc12c7187af9 Mon Sep 17 00:00:00 2001 From: Erica Hughberg Date: Tue, 23 Jun 2026 00:35:35 +0200 Subject: [PATCH 43/59] docs: declare v1beta1 stable for v1.0 and add v1.0.x compatibility row (#2270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** Prepare release documentation for the upcoming **v1.0.0** release. Docs-only change in two parts: - **`RELEASES.md`** — record the API stability commitment that motivates the major version bump. As of v1.0.0, `v1beta1` is the committed **stable** control-plane API and is covered by the existing "never break unless critical security" guarantee. `v1alpha1` remains served but deprecated and will be removed in a future release per the deprecation policy. `QuotaPolicy` stays `v1alpha1`-only and is therefore outside the stable surface; in particular its `serviceQuota` field is accepted but not yet enforced end-to-end. - **`site/docs/compatibility.md`** — add the `v1.0.x` row to the compatibility matrix (Envoy Gateway v1.8.x+, Envoy Proxy v1.38.x, Kubernetes v1.32+, Gateway API v1.5.x). No API, controller, or data-plane code changes. AI usage: these docs were drafted with assistance from Claude (Claude Code) and reviewed by the author. **Related Issues/PRs (if applicable)** **Special notes for reviewers (if applicable)** This is forward-looking v1.0 release-prep. The new `v1.0.x` compatibility row is marked `Supported` and the `RELEASES.md` wording says "as of the v1.0.0 release", even though v1.0.0 has not been tagged yet. Intent is for this to land as part of, or immediately before, the v1.0 cut rather than necessarily merging standalone today. Two items intentionally left to maintainers: - EOL rolling in the compatibility matrix. With the v0.7 to v1.0 major jump, which of v0.7.x / v0.6.x / v0.5.x roll to End of Life is a judgment call; this PR only adds the `v1.0.x` row and does not change existing support statuses. - The `serviceQuota` docs accuracy fix tracked on the `docs/quota-policy-accuracy` branch is complementary to the `RELEASES.md` note added here. --------- Signed-off-by: Erica Hughberg Co-authored-by: Claude Opus 4.8 --- RELEASES.md | 6 ++++++ site/docs/compatibility.md | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 537fc3258a..4226ec20c0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -46,6 +46,12 @@ Migration paths for alpha versions will be the best effort and will be documente **For stable versions**, we will never break the APIs unless there is a critical security issue. We will provide a migration path in the release notes in case we need to break the APIs. +As of the v1.0.0 release, `v1beta1` is the committed **stable** control-plane API and is +covered by the stable-version guarantee above. The `v1alpha1` API remains served but +deprecated, and will be removed in a future release per the deprecation policy. Note that +`QuotaPolicy` is `v1alpha1`-only and is therefore **not** part of the stable surface; in +particular, its `serviceQuota` field is accepted but not yet enforced end-to-end. + ### Upgrading the Envoy AI Gateway controller We guarantee that simply upgrading the controller will not break the existing configuration assuming there's no _un-migrated_ resources including breaking change left in the k8s API server. In other words, after the proper use of the API and migration path described above, the user should be able to upgrade the controller without any issue. However, this does mean that we do NOT guarantee that the existing configuration will work across more than two version of the controller. For example if you are using the version N of the controller, and you want to upgrade to the version N+2, you should first upgrade to the version N+1 while following the migration path if applicable, and then upgrade to the version N+2. diff --git a/site/docs/compatibility.md b/site/docs/compatibility.md index 68c73de85e..4fdfe5221c 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.7.x+ (Envoy Proxy v1.38.x) | v1.32+ | v1.5.x | Development | +| main | v1.8.1+ (Envoy Proxy v1.38.x) | v1.32+ | v1.5.x | Development | +| v1.0.x | v1.8.1+ (Envoy Proxy v1.38.x) | v1.32+ | v1.5.x | Supported | | 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 | From 5a8584a4740e19611e07377162a283c4ff7a7fff Mon Sep 17 00:00:00 2001 From: Erica Hughberg Date: Tue, 23 Jun 2026 01:47:11 +0200 Subject: [PATCH 44/59] docs: add QuotaPolicy capability guide (#1947) **Description** Adds comprehensive documentation for the QuotaPolicy CRD, which was fully implemented but had no user-facing documentation. Covers service-wide quotas, per-model quotas, CEL cost expressions, Exclusive vs Shared bucket modes, client-selector based bucket rules, shadow mode, and duration format. Follows the structure of existing traffic capability guides. Distinguishes QuotaPolicy (total consumption budgets) from rate limiting (request velocity) to help adopters choose the right mechanism. **Related Issues/PRs (if applicable)** Related #1571 **Special notes for reviewers (if applicable)** All field definitions and semantics sourced from `api/v1alpha1/quota_policy.go` GoDoc comments. Includes complete, copy-pasteable YAML examples for every configuration option. --------- Signed-off-by: Erica Hughberg Signed-off-by: Aaron Choo Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Aaron Choo --- api/v1alpha1/quota_policy.go | 2 +- ...aigateway.envoyproxy.io_quotapolicies.yaml | 2 +- site/docs/api/api.mdx | 2 +- site/docs/capabilities/index.md | 1 + site/docs/capabilities/traffic/index.md | 2 +- .../docs/capabilities/traffic/quota-policy.md | 327 ++++++++++++++++++ .../traffic/usage-based-ratelimiting.md | 4 + 7 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 site/docs/capabilities/traffic/quota-policy.md diff --git a/api/v1alpha1/quota_policy.go b/api/v1alpha1/quota_policy.go index 00a8b39a9a..ea4db7175c 100644 --- a/api/v1alpha1/quota_policy.go +++ b/api/v1alpha1/quota_policy.go @@ -95,7 +95,7 @@ type QuotaDefinition struct { // If no expression is specified the "total_tokens" value is used. // For example: // - // * "input_tokens + cached_input_tokens * 0.1 + output_tokens * 6" + // "input_tokens + cached_input_tokens + output_tokens" // // +optional CostExpression *string `json:"costExpression,omitempty"` diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml index 52b8890613..d969eef6f2 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_quotapolicies.yaml @@ -325,7 +325,7 @@ spec: If no expression is specified the "total_tokens" value is used. For example: - * "input_tokens + cached_input_tokens * 0.1 + output_tokens * 6" + "input_tokens + cached_input_tokens + output_tokens" type: string defaultBucket: description: |- diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 3a3ef29d9b..43d513f26d 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -2296,7 +2296,7 @@ QuotaDefinition specified expression for computing request cost and rules for ma name="costExpression" type="string" required="false" - description="CostExpression specifies a CEL expression for computing the quota burndown of the LLM-related request.
    If no expression is specified the `total_tokens` value is used.
    For example:
    * `input_tokens + cached_input_tokens * 0.1 + output_tokens * 6`" + description="CostExpression specifies a CEL expression for computing the quota burndown of the LLM-related request.
    If no expression is specified the `total_tokens` value is used.
    For example:
    `input_tokens + cached_input_tokens + output_tokens`" /> Date: Tue, 23 Jun 2026 05:20:51 -0400 Subject: [PATCH 45/59] translator: guard nil response body in OpenAI ResponseBody (#2240) **Description** A JSON `null` response body decodes into a nil `*ChatCompletionResponse` without an error, because the decode target is a `**ChatCompletionResponse` (`Decode(&resp)`). Some OpenAI-compatible backends return a `null` body with a 200 status (e.g. an upstream that aborts the response after committing headers under load). In the non-streaming path `ResponseBody` then dereferenced the nil response at `resp.Usage.PromptTokens`, panicking the ext-proc with a SIGSEGV (addr 0x80). `RedactBody` already guards `resp == nil`, so the nil case is known; the main path just missed it. This treats a nil response as an empty response: report zero token usage and fall back to the request model, mirroring the streaming path's behavior when no usage is present, rather than crashing. A regression test feeds a literal `null` body and asserts no panic and zero usage. Per the project's generative-AI policy: this change was prepared with the assistance of an AI coding tool (Claude Code). The submitter understands and takes full ownership of the change. **Related Issues/PRs (if applicable)** Fixes #2273 **Special notes for reviewers (if applicable)** N/A Signed-off-by: Kunwar Srivastav Co-authored-by: Ignasi Barrera --- internal/translator/openai_openai.go | 8 ++++++++ internal/translator/openai_openai_test.go | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/internal/translator/openai_openai.go b/internal/translator/openai_openai.go index 2f30125016..afbe4bac02 100644 --- a/internal/translator/openai_openai.go +++ b/internal/translator/openai_openai.go @@ -147,6 +147,14 @@ func (o *openAIToOpenAITranslatorV1ChatCompletion) ResponseBody(_ map[string]str if err := json.NewDecoder(body).Decode(&resp); err != nil { return nil, nil, tokenUsage, responseModel, fmt.Errorf("failed to unmarshal body: %w", err) } + // A JSON `null` body decodes into a nil *resp without an error (the decode + // target is a **ChatCompletionResponse), which some upstreams return with a + // 200 status. Treat it as an empty response so we report zero usage and fall + // back to the request model, mirroring the streaming path, instead of + // dereferencing nil below. + if resp == nil { + resp = &openai.ChatCompletionResponse{} + } // Redact and log response when enabled if o.debugLogEnabled && o.enableRedaction && o.logger != nil { diff --git a/internal/translator/openai_openai_test.go b/internal/translator/openai_openai_test.go index 23adc97cc8..b0b709031a 100644 --- a/internal/translator/openai_openai_test.go +++ b/internal/translator/openai_openai_test.go @@ -350,6 +350,16 @@ data: [DONE] _, _, _, _, err := o.ResponseBody(nil, bytes.NewBuffer([]byte("invalid")), false, nil) require.Error(t, err) }) + t.Run("null body", func(t *testing.T) { + // A literal JSON `null` decodes to a nil response without an error; + // it must not panic and should report zero usage. + s := &testotel.MockSpan{} + o := &openAIToOpenAITranslatorV1ChatCompletion{requestModel: "gpt-4o-mini"} + _, _, usedToken, responseModel, err := o.ResponseBody(nil, bytes.NewBuffer([]byte("null")), false, s) + require.NoError(t, err) + require.Equal(t, tokenUsageFrom(0, -1, -1, 0, 0, -1), usedToken) + require.Equal(t, "gpt-4o-mini", responseModel) + }) t.Run("valid body", func(t *testing.T) { s := &testotel.MockSpan{} var resp openai.ChatCompletionResponse From 3e7b0bf02dad2cf6b88a523af829a7b93eb5b8e1 Mon Sep 17 00:00:00 2001 From: aias00 Date: Tue, 23 Jun 2026 17:39:38 +0800 Subject: [PATCH 46/59] fix: use correct slice index for Bedrock tool result coalescing (#2237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** Fix two bugs in the Anthropic-to-Bedrock translator message merging logic at `internal/translator/anthropic_awsbedrock.go`. **Bug 1: Wrong slice index (original report)** When system messages are present in the request body, `promoteAnthropicSystemMessagesToParam` filters them out, producing a shorter `messages` slice. The coalescing loop was using `body.Messages[i+1]` (the original unfiltered slice) instead of `messages[i+1]` (the filtered slice), causing index misalignment that could read wrong messages or panic with out-of-bounds access. **Bug 2: Coalescing logic was unreachable (root cause)** Tool result messages have `Role: "user"`, so they match `case anthropicschema.MessageRoleUser` and go through `convertUserMessage()` — which handles tool results individually but never coalesces consecutive ones. The coalescing logic in the `default` branch was dead code for tool results since they never reach that branch. **Changes** - Moved the `hasToolResult` check + coalescing loop into the `MessageRoleUser` case branch - Non-tool-result user messages still go through `convertUserMessage()` - Changed `body.Messages[i+1]` → `messages[i+1]` to use the correct filtered slice - Simplified the `default` branch to just return an error for unexpected roles - Updated existing test to expect coalesced output (3 messages instead of 4) - Added regression test with system messages to verify index alignment **Testing** - All existing translator tests pass - New `TestAnthropicToAWSBedrockTranslator_RequestBody_ToolResultMessagesWithSystemMessages` verifies the fix works when system messages are present alongside consecutive tool results --------- Signed-off-by: liuhy Co-authored-by: Ignasi Barrera --- .gitignore | 1 + internal/translator/anthropic_awsbedrock.go | 37 +-- .../translator/anthropic_awsbedrock_test.go | 305 ++++++++++++++++-- 3 files changed, 300 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 51882ed99d..bfaea25280 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ inference-extension-conformance-test-report.yaml /aigw site/.cursorrules /.cursor +.worktrees/ diff --git a/internal/translator/anthropic_awsbedrock.go b/internal/translator/anthropic_awsbedrock.go index 5bcc532164..2e4cb779ff 100644 --- a/internal/translator/anthropic_awsbedrock.go +++ b/internal/translator/anthropic_awsbedrock.go @@ -78,36 +78,29 @@ func (a *anthropicToAWSBedrockTranslator) RequestBody(_ []byte, body *anthropics msg := &messages[i] switch msg.Role { case anthropicschema.MessageRoleUser: - bedrockMsg, convErr := a.convertUserMessage(msg) - if convErr != nil { - return nil, nil, convErr - } - bedrockReq.Messages = append(bedrockReq.Messages, bedrockMsg) - i++ - case anthropicschema.MessageRoleAssistant: - bedrockMsg, convErr := a.convertAssistantMessage(msg) - if convErr != nil { - return nil, nil, convErr - } - bedrockReq.Messages = append(bedrockReq.Messages, bedrockMsg) - i++ - default: - // Check for tool_result content blocks (these come as "user" role in Anthropic - // but we handle them specially). if hasToolResult(msg) { bedrockMsg := a.convertToolResultMessage(msg) // Coalesce consecutive tool result messages. - for i+1 < msgLen && hasToolResult(&body.Messages[i+1]) { - nextMsg := &body.Messages[i+1] + for i+1 < msgLen && hasToolResult(&messages[i+1]) { + nextMsg := &messages[i+1] nextBedrockMsg := a.convertToolResultMessage(nextMsg) bedrockMsg.Content = append(bedrockMsg.Content, nextBedrockMsg.Content...) i++ } bedrockReq.Messages = append(bedrockReq.Messages, bedrockMsg) } else { - return nil, nil, fmt.Errorf("%w: unexpected role: %s", internalapi.ErrInvalidRequestBody, msg.Role) + bedrockMsg, convErr := a.convertUserMessage(msg) + if convErr != nil { + return nil, nil, convErr + } + bedrockReq.Messages = append(bedrockReq.Messages, bedrockMsg) } i++ + case anthropicschema.MessageRoleAssistant: + bedrockReq.Messages = append(bedrockReq.Messages, a.convertAssistantMessage(msg)) + i++ + default: + return nil, nil, fmt.Errorf("%w: unexpected role: %s", internalapi.ErrInvalidRequestBody, msg.Role) } } @@ -236,13 +229,13 @@ func (a *anthropicToAWSBedrockTranslator) convertUserMessage(msg *anthropicschem return bedrockMsg, nil } -func (a *anthropicToAWSBedrockTranslator) convertAssistantMessage(msg *anthropicschema.MessageParam) (*awsbedrock.Message, error) { +func (a *anthropicToAWSBedrockTranslator) convertAssistantMessage(msg *anthropicschema.MessageParam) *awsbedrock.Message { bedrockMsg := &awsbedrock.Message{Role: awsbedrock.ConversationRoleAssistant} if msg.Content.Text != "" { bedrockMsg.Content = []*awsbedrock.ContentBlock{ {Text: ptr.To(msg.Content.Text)}, } - return bedrockMsg, nil + return bedrockMsg } bedrockMsg.Content = make([]*awsbedrock.ContentBlock, 0, len(msg.Content.Array)) for i := range msg.Content.Array { @@ -277,7 +270,7 @@ func (a *anthropicToAWSBedrockTranslator) convertAssistantMessage(msg *anthropic }) } } - return bedrockMsg, nil + return bedrockMsg } func (a *anthropicToAWSBedrockTranslator) convertToolResultMessage(msg *anthropicschema.MessageParam) *awsbedrock.Message { diff --git a/internal/translator/anthropic_awsbedrock_test.go b/internal/translator/anthropic_awsbedrock_test.go index 29f8cb92fb..9c8ed2d637 100644 --- a/internal/translator/anthropic_awsbedrock_test.go +++ b/internal/translator/anthropic_awsbedrock_test.go @@ -955,6 +955,39 @@ func TestAnthropicToAWSBedrockTranslator_RequestBody_UserArrayContent(t *testing assert.Equal(t, []string{"END", "STOP"}, bedrockReq.InferenceConfig.StopSequences) } +func TestAnthropicToAWSBedrockTranslator_RequestBody_UserArrayContentError(t *testing.T) { + translator := NewAnthropicToAWSBedrockTranslator("") + req := &anthropicschema.MessagesRequest{ + Model: "test-model", + MaxTokens: 100, + Messages: []anthropicschema.MessageParam{ + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {Image: &anthropicschema.ImageBlockParam{ + Type: "image", + Source: anthropicschema.ImageSource{ + Base64: &anthropicschema.Base64ImageSource{ + Type: "base64", + MediaType: "application/pdf", + Data: "not-used", + }, + }, + }}, + }, + }, + }, + }, + } + rawBody, err := json.Marshal(req) + require.NoError(t, err) + + _, _, err = translator.RequestBody(rawBody, req, false) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported image format application/pdf") +} + func TestAnthropicToAWSBedrockTranslator_RequestBody_ToolResultMessages(t *testing.T) { translator := NewAnthropicToAWSBedrockTranslator("") req := &anthropicschema.MessagesRequest{ @@ -1032,28 +1065,27 @@ func TestAnthropicToAWSBedrockTranslator_RequestBody_ToolResultMessages(t *testi err = json.Unmarshal(body, &bedrockReq) require.NoError(t, err) - // Should be: user, assistant, user (tool result 1), user (tool result 2). - // Tool result messages with role "user" go through convertUserMessage individually. - require.Len(t, bedrockReq.Messages, 4) + // Should be: user, assistant, user (coalesced tool results). + // Consecutive tool_result messages are coalesced into a single Bedrock message. + require.Len(t, bedrockReq.Messages, 3) + + // Coalesced tool result message contains both tool results. + toolResultMsg := bedrockReq.Messages[2] + assert.Equal(t, awsbedrock.ConversationRoleUser, toolResultMsg.Role) + require.Len(t, toolResultMsg.Content, 2) + + // First tool result. + require.NotNil(t, toolResultMsg.Content[0].ToolResult) + assert.Equal(t, "tu_abc", *toolResultMsg.Content[0].ToolResult.ToolUseID) + require.Len(t, toolResultMsg.Content[0].ToolResult.Content, 1) + assert.Equal(t, "72°F and sunny", *toolResultMsg.Content[0].ToolResult.Content[0].Text) - // First tool result message. - toolResultMsg1 := bedrockReq.Messages[2] - assert.Equal(t, awsbedrock.ConversationRoleUser, toolResultMsg1.Role) - require.Len(t, toolResultMsg1.Content, 1) - require.NotNil(t, toolResultMsg1.Content[0].ToolResult) - assert.Equal(t, "tu_abc", *toolResultMsg1.Content[0].ToolResult.ToolUseID) - require.Len(t, toolResultMsg1.Content[0].ToolResult.Content, 1) - assert.Equal(t, "72°F and sunny", *toolResultMsg1.Content[0].ToolResult.Content[0].Text) - - // Second tool result message (error). - toolResultMsg2 := bedrockReq.Messages[3] - assert.Equal(t, awsbedrock.ConversationRoleUser, toolResultMsg2.Role) - require.Len(t, toolResultMsg2.Content, 1) - require.NotNil(t, toolResultMsg2.Content[0].ToolResult) - assert.Equal(t, "tu_def", *toolResultMsg2.Content[0].ToolResult.ToolUseID) - assert.Equal(t, "error", *toolResultMsg2.Content[0].ToolResult.Status) - require.Len(t, toolResultMsg2.Content[0].ToolResult.Content, 1) - assert.Equal(t, "Error: city not found", *toolResultMsg2.Content[0].ToolResult.Content[0].Text) + // Second tool result (error) coalesced into the same message. + require.NotNil(t, toolResultMsg.Content[1].ToolResult) + assert.Equal(t, "tu_def", *toolResultMsg.Content[1].ToolResult.ToolUseID) + assert.Equal(t, "error", *toolResultMsg.Content[1].ToolResult.Status) + require.Len(t, toolResultMsg.Content[1].ToolResult.Content, 1) + assert.Equal(t, "Error: city not found", *toolResultMsg.Content[1].ToolResult.Content[0].Text) // Verify tool choice "any". require.NotNil(t, bedrockReq.ToolConfig) @@ -1061,6 +1093,237 @@ func TestAnthropicToAWSBedrockTranslator_RequestBody_ToolResultMessages(t *testi assert.NotNil(t, bedrockReq.ToolConfig.ToolChoice.Any) } +func TestAnthropicToAWSBedrockTranslator_RequestBody_ToolResultMessagesWithSystemMessages(t *testing.T) { + // Regression test: when system messages are present in the messages array, + // promoteAnthropicSystemMessagesToParam filters them out, creating a shorter + // `messages` slice. The coalescing loop must use the filtered `messages` slice + // (not body.Messages) to avoid index misalignment and potential out-of-bounds access. + translator := NewAnthropicToAWSBedrockTranslator("") + req := &anthropicschema.MessagesRequest{ + Model: "test-model", + MaxTokens: 100, + Tools: []anthropicschema.ToolUnion{ + {Tool: &anthropicschema.Tool{ + Type: "custom", + Name: "get_weather", + Description: "Get weather", + InputSchema: anthropicschema.ToolInputSchema{Type: "object"}, + }}, + }, + Messages: []anthropicschema.MessageParam{ + // System message at the start — will be promoted out of the messages slice. + { + Role: "system", + Content: anthropicschema.MessageContent{Text: "You are a helpful assistant."}, + }, + // Another system message — also promoted. + { + Role: "system", + Content: anthropicschema.MessageContent{Text: "Always be concise."}, + }, + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{Text: "What's the weather?"}, + }, + { + Role: anthropicschema.MessageRoleAssistant, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolUse: &anthropicschema.ToolUseBlockParam{ + Type: "tool_use", + ID: "tu_abc", + Name: "get_weather", + Input: map[string]any{"city": "NYC"}, + }}, + }, + }, + }, + // First tool result message. + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_abc", + Content: &anthropicschema.ToolResultContent{Text: "72°F and sunny"}, + }}, + }, + }, + }, + // Second consecutive tool result message (should be coalesced with the first). + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_def", + IsError: true, + Content: &anthropicschema.ToolResultContent{ + Array: []anthropicschema.ToolResultContentItem{ + {Text: &anthropicschema.TextBlockParam{Type: "text", Text: "Error: city not found"}}, + }, + }, + }}, + }, + }, + }, + }, + } + 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) + + // After promoting 2 system messages, messages has 4 entries: + // user, assistant, user(tool_result), user(tool_result) + // The two consecutive tool_result messages should be coalesced into one, + // yielding 3 Bedrock messages: user, assistant, user(coalesced tool results). + require.Len(t, bedrockReq.Messages, 3) + + // First message: user text. + assert.Equal(t, awsbedrock.ConversationRoleUser, bedrockReq.Messages[0].Role) + require.Len(t, bedrockReq.Messages[0].Content, 1) + assert.NotNil(t, bedrockReq.Messages[0].Content[0].Text) + + // Second message: assistant tool_use. + assert.Equal(t, awsbedrock.ConversationRoleAssistant, bedrockReq.Messages[1].Role) + + // Third message: coalesced tool results (both tool_use_ids in one message). + toolResultMsg := bedrockReq.Messages[2] + assert.Equal(t, awsbedrock.ConversationRoleUser, toolResultMsg.Role) + require.Len(t, toolResultMsg.Content, 2) + require.NotNil(t, toolResultMsg.Content[0].ToolResult) + assert.Equal(t, "tu_abc", *toolResultMsg.Content[0].ToolResult.ToolUseID) + assert.Equal(t, "72°F and sunny", *toolResultMsg.Content[0].ToolResult.Content[0].Text) + require.NotNil(t, toolResultMsg.Content[1].ToolResult) + assert.Equal(t, "tu_def", *toolResultMsg.Content[1].ToolResult.ToolUseID) + assert.Equal(t, "error", *toolResultMsg.Content[1].ToolResult.Status) + assert.Equal(t, "Error: city not found", *toolResultMsg.Content[1].ToolResult.Content[0].Text) + + // Verify system prompt was promoted. + require.NotNil(t, bedrockReq.System) + require.Len(t, bedrockReq.System, 1) + assert.Contains(t, *bedrockReq.System[0].Text, "You are a helpful assistant.") + assert.Contains(t, *bedrockReq.System[0].Text, "Always be concise.") +} + +func TestAnthropicToAWSBedrockTranslator_RequestBody_SingleToolResultNotCoalesced(t *testing.T) { + // Verify that a single tool_result message (without a consecutive one) is NOT coalesced + // but still goes through the tool result path (not convertUserMessage). + translator := NewAnthropicToAWSBedrockTranslator("") + req := &anthropicschema.MessagesRequest{ + Model: "test-model", + MaxTokens: 100, + Tools: []anthropicschema.ToolUnion{ + {Tool: &anthropicschema.Tool{ + Type: "custom", + Name: "get_weather", + Description: "Get weather", + InputSchema: anthropicschema.ToolInputSchema{Type: "object"}, + }}, + }, + Messages: []anthropicschema.MessageParam{ + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{Text: "What's the weather?"}, + }, + { + Role: anthropicschema.MessageRoleAssistant, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolUse: &anthropicschema.ToolUseBlockParam{ + Type: "tool_use", + ID: "tu_abc", + Name: "get_weather", + Input: map[string]any{"city": "NYC"}, + }}, + }, + }, + }, + // Single tool result — no consecutive tool result to coalesce with. + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_abc", + Content: &anthropicschema.ToolResultContent{Text: "72°F and sunny"}, + }}, + }, + }, + }, + // Follow-up user text message — should NOT be coalesced with tool result. + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{Text: "Thanks!"}, + }, + }, + } + 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) + + // 4 messages: user, assistant, user(tool_result), user(text) + require.Len(t, bedrockReq.Messages, 4) + + // Third message: tool result (not coalesced). + toolResultMsg := bedrockReq.Messages[2] + assert.Equal(t, awsbedrock.ConversationRoleUser, toolResultMsg.Role) + require.Len(t, toolResultMsg.Content, 1) + require.NotNil(t, toolResultMsg.Content[0].ToolResult) + assert.Equal(t, "tu_abc", *toolResultMsg.Content[0].ToolResult.ToolUseID) + + // Fourth message: plain text (not coalesced with tool result). + textMsg := bedrockReq.Messages[3] + assert.Equal(t, awsbedrock.ConversationRoleUser, textMsg.Role) + require.Len(t, textMsg.Content, 1) + assert.NotNil(t, textMsg.Content[0].Text) + assert.Equal(t, "Thanks!", *textMsg.Content[0].Text) +} + +func TestAnthropicToAWSBedrockTranslator_RequestBody_UnexpectedRole(t *testing.T) { + // Verify that an unexpected role returns an error. + translator := NewAnthropicToAWSBedrockTranslator("") + req := &anthropicschema.MessagesRequest{ + Model: "test-model", + MaxTokens: 100, + Messages: []anthropicschema.MessageParam{ + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{Text: "Hello"}, + }, + { + Role: "system", // Will be promoted to system param. + Content: anthropicschema.MessageContent{Text: "You are helpful."}, + }, + { + Role: "invalid_role", + Content: anthropicschema.MessageContent{Text: "This should fail"}, + }, + }, + } + rawBody, err := json.Marshal(req) + require.NoError(t, err) + + _, _, err = translator.RequestBody(rawBody, req, false) + require.Error(t, err) + assert.Contains(t, err.Error(), "unexpected role: invalid_role") +} + func TestAnthropicToAWSBedrockTranslator_ResponseBody_NonStreamingToolUseAndReasoning(t *testing.T) { translator := NewAnthropicToAWSBedrockTranslator("") req := &anthropicschema.MessagesRequest{ From 60fcd8cbe3243b020b4f2656d21d53060907e6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=8F=AC=EC=B9=B4?= Date: Wed, 1 Jul 2026 15:49:53 +0900 Subject: [PATCH 47/59] fix(extproc): preserve original client path in x-ai-eg-original-path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit router.ProcessRequestBody re-set x-ai-eg-original-path unconditionally, overwriting the pre-rewrite path stored by ProcessRequestHeaders. By the time ProcessRequestBody runs, r.requestHeaders[":path"] already holds the rewritten form (e.g. /cachedContents after /v1beta/cachedContents → /cachedContents), so upstream-filter's processorForPath then looked up /cachedContents — which is not a registered prefix — and returned no processor. The upstream filter silently produced no response and Envoy 60s-timed the request out (504 UT). The v0.7-era code had this exact guard; it was dropped in the v1.0.0 merge conflict resolution (upstream removed it, we accepted upstream's version). Restore the fork's guard so bump-through GET/DELETE bodyless requests still get the header from ProcessRequestHeaders and body-carrying requests don't clobber it in ProcessRequestBody. Symptom: POST /v1beta/cachedContents 504s with response_flags=UT after v1.0.0 deploy. Regression only affects paths that go through rewriteDeveloperAPIPath (cachedContents Developer-API form) — generateContent is unaffected because its :path rewrite happens in the upstream stage. Co-Authored-By: Claude Opus 4.7 --- internal/extproc/processor_impl.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/internal/extproc/processor_impl.go b/internal/extproc/processor_impl.go index c61d8c10b9..c9f7c7cf88 100644 --- a/internal/extproc/processor_impl.go +++ b/internal/extproc/processor_impl.go @@ -395,10 +395,17 @@ func (r *routerProcessor[ReqT, RespT, RespChunkT, EndpointSpecT]) ProcessRequest Header: &corev3.HeaderValue{Key: internalapi.ModelNameHeaderKeyDefault, RawValue: []byte(originalModel)}, }) originalPath := r.requestHeaders[":path"] - r.requestHeaders[originalPathHeader] = originalPath - additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{ - Header: &corev3.HeaderValue{Key: originalPathHeader, RawValue: []byte(originalPath)}, - }) + // Guard: ProcessRequestHeaders may have already stored the pre-rewrite path here. + // At this point r.requestHeaders[":path"] is the *rewritten* path (e.g. /cachedContents + // after /v1beta/cachedContents → /cachedContents), so an unconditional overwrite would + // clobber the original client path that upstream-filter's processorForPath needs to + // resolve the right EndpointSpec via prefix match. + if r.requestHeaders[originalPathHeader] == "" { + r.requestHeaders[originalPathHeader] = originalPath + additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{ + Header: &corev3.HeaderValue{Key: originalPathHeader, RawValue: []byte(originalPath)}, + }) + } if r.requestHeaders[internalapi.EnvoyOriginalPathHeader] == "" { r.requestHeaders[internalapi.EnvoyOriginalPathHeader] = originalPath additionalHeaders = append(additionalHeaders, &corev3.HeaderValueOption{ From 54fe80ed1da045920cfd15d15e21fb62e5691ea8 Mon Sep 17 00:00:00 2001 From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:55:36 +0100 Subject: [PATCH 48/59] fix: treat empty body as non-fatal in decodeContentIfNeeded (#2291) **Description** When an upstream response is gzip-encoded but has an empty body, the gateway aborts a response that was actually fine. `decodeContentIfNeeded` builds a `gzip.NewReader` to decompress the body. `gzip.NewReader` reads the gzip header immediately, and over a zero-byte body that read returns `io.EOF`. The function returns that as `failed to decode gzip: EOF`, which the ext_proc layer treats as fatal, so the client gets nothing even though the upstream returned a successful response. We saw this in production as an ext_proc abort with `response_code: 200` but `bytes_sent: 0`. This happens when an upstream sends `Content-Encoding: gzip` with an empty body, or when a streaming response is buffered to end-of-stream with no payload (for example, the connection terminated before any body bytes arrived). The streaming path also runs the accumulated buffer through `decodeContentIfNeeded`, so an empty buffer hits the same error. The fix returns an empty body as-is instead of trying to build a decompressor. It is intentionally narrow: a non-empty but malformed gzip body still errors exactly as before, and only the zero-length case is treated as a pass-through. Tests: added an `empty gzip body` case to `TestDecodeContentIfNeeded` covering an empty body with `Content-Encoding: gzip`. `gofmt`, `go vet`, and the `internal/extproc` package tests pass. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** The guard lives in `decodeContentIfNeeded` rather than at the call sites because both the buffered and streaming paths flow through it, so a single check covers both. Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> --- internal/extproc/util.go | 13 +++++++++++++ internal/extproc/util_test.go | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/internal/extproc/util.go b/internal/extproc/util.go index 774056cc69..d2aeb3f2c6 100644 --- a/internal/extproc/util.go +++ b/internal/extproc/util.go @@ -30,6 +30,19 @@ type contentDecodingResult struct { // Currently, supports gzip and brotli encoding, but can be extended to support other encodings in the future. // Returns a reader for the (potentially decompressed) body and metadata about the encoding. func decodeContentIfNeeded(body []byte, contentEncoding string) (contentDecodingResult, error) { + // An empty body has nothing to decompress. gzip.NewReader reads the gzip + // header eagerly, so building it over zero bytes fails immediately with + // io.EOF. That gets surfaced as a fatal "failed to decode gzip: EOF" and + // aborts an otherwise-successful upstream response. This can happen when the + // upstream sends Content-Encoding: gzip with an empty body, or when a + // streaming response is buffered to end-of-stream with no payload. Treat an + // empty body as nothing to decode and pass it through unchanged. + if len(body) == 0 { + return contentDecodingResult{ + reader: bytes.NewReader(body), + isEncoded: false, + }, nil + } switch contentEncoding { case "gzip": reader, err := gzip.NewReader(bytes.NewReader(body)) diff --git a/internal/extproc/util_test.go b/internal/extproc/util_test.go index 3d7774ffc4..e85d767cba 100644 --- a/internal/extproc/util_test.go +++ b/internal/extproc/util_test.go @@ -75,6 +75,17 @@ func TestDecodeContentIfNeeded(t *testing.T) { wantEncoding: "", wantErr: true, }, + { + // An empty body with Content-Encoding: gzip must not error. gzip.NewReader + // over zero bytes returns io.EOF; treating that as a decode failure aborts + // an otherwise-successful upstream response. + name: "empty gzip body", + body: []byte{}, + encoding: "gzip", + wantEncoded: false, + wantEncoding: "", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 190a01b84206707ece596072bd0b9e1c7b4887b6 Mon Sep 17 00:00:00 2001 From: aias00 Date: Fri, 26 Jun 2026 03:52:53 +0800 Subject: [PATCH 49/59] fix: return error instead of panic on streaming marshal failure (#2289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** The OpenAI-to-AWS Bedrock translator panics when streaming event serialization fails, causing the extproc container to crash. Instead of propagating the error upstream for graceful handling, the code calls `panic()` at `internal/translator/openai_awsbedrock.go:740`. **Location** `internal/translator/openai_awsbedrock.go:740` **Before:** ```go err = serializeOpenAIChatCompletionChunk(oaiEvent, &newBody) if err != nil { panic(fmt.Errorf("failed to marshal event: %w", err)) // ❌ PANIC } ``` **After:** ```go err = serializeOpenAIChatCompletionChunk(oaiEvent, &newBody) if err != nil { return nil, nil, metrics.TokenUsage{}, "", fmt.Errorf("failed to marshal streaming event: %w", err) } ``` **Root Cause** `serializeOpenAIChatCompletionChunk()` wraps `json.Marshal()` and returns an error when marshaling fails. When marshal fails (e.g., malformed chunk data or edge cases in converted Bedrock events), the streaming handler panics instead of returning an error. **Impact** - **Critical**: extproc crash disrupts all ongoing requests through the gateway - **No graceful degradation**: Clients receive connection errors, no meaningful error response - **Hard to debug**: Panic crashes the process without proper error logging - **Affects**: All streaming requests through AWS Bedrock backend **Fix** Replace `panic()` with proper error return, allowing Envoy to handle the failure gracefully. This pattern matches the error handling used elsewhere in the same file for non-streaming responses (e.g., line 754-756). **Testing** All existing Bedrock translator tests pass: ``` === RUN TestOpenAIToAWSBedrockTranslator --- PASS: TestOpenAIToAWSBedrockTranslator ``` **Files Changed** | File | Change | |------|--------| | `internal/translator/openai_awsbedrock.go` | panic → return error | --------- Signed-off-by: liuhy Co-authored-by: Claude --- internal/translator/anthropic_awsbedrock.go | 26 ++- .../translator/anthropic_awsbedrock_test.go | 184 ++++++++++++++++++ internal/translator/openai_awsbedrock.go | 2 +- internal/translator/openai_awsbedrock_test.go | 49 +++++ 4 files changed, 253 insertions(+), 8 deletions(-) diff --git a/internal/translator/anthropic_awsbedrock.go b/internal/translator/anthropic_awsbedrock.go index 2e4cb779ff..27e1d9c945 100644 --- a/internal/translator/anthropic_awsbedrock.go +++ b/internal/translator/anthropic_awsbedrock.go @@ -78,10 +78,10 @@ func (a *anthropicToAWSBedrockTranslator) RequestBody(_ []byte, body *anthropics msg := &messages[i] switch msg.Role { case anthropicschema.MessageRoleUser: - if hasToolResult(msg) { + if isOnlyToolResult(msg) { bedrockMsg := a.convertToolResultMessage(msg) - // Coalesce consecutive tool result messages. - for i+1 < msgLen && hasToolResult(&messages[i+1]) { + // Coalesce consecutive tool-result-only messages. + for i+1 < msgLen && isOnlyToolResult(&messages[i+1]) { nextMsg := &messages[i+1] nextBedrockMsg := a.convertToolResultMessage(nextMsg) bedrockMsg.Content = append(bedrockMsg.Content, nextBedrockMsg.Content...) @@ -188,16 +188,28 @@ func promoteAnthropicSystemMessagesToParam(body *anthropicschema.MessagesRequest return filtered } -func hasToolResult(msg *anthropicschema.MessageParam) bool { +// isOnlyToolResult returns true if the message is a user message whose content +// array consists entirely of tool_result blocks. This is used to decide whether +// the message should go through the tool-result coalescing path. +// Mixed-content messages (e.g. text + tool_result) are handled by +// convertUserMessage, which preserves all block types. +func isOnlyToolResult(msg *anthropicschema.MessageParam) bool { if msg.Role != anthropicschema.MessageRoleUser { return false } + // Simple text-only content is never a tool-result-only message. + if msg.Content.Text != "" { + return false + } + if len(msg.Content.Array) == 0 { + return false + } for i := range msg.Content.Array { - if msg.Content.Array[i].ToolResult != nil { - return true + if msg.Content.Array[i].ToolResult == nil { + return false } } - return false + return true } func (a *anthropicToAWSBedrockTranslator) convertUserMessage(msg *anthropicschema.MessageParam) (*awsbedrock.Message, error) { diff --git a/internal/translator/anthropic_awsbedrock_test.go b/internal/translator/anthropic_awsbedrock_test.go index 9c8ed2d637..69a8ef8bf8 100644 --- a/internal/translator/anthropic_awsbedrock_test.go +++ b/internal/translator/anthropic_awsbedrock_test.go @@ -1600,3 +1600,187 @@ func TestPromoteAnthropicSystemMessagesToParam(t *testing.T) { assert.Equal(t, "You are helpful.", *bedrockReq.System[0].Text) }) } + +func TestIsOnlyToolResult(t *testing.T) { + tests := []struct { + name string + expected bool + msg anthropicschema.MessageParam + }{ + { + name: "single tool_result block", + expected: true, + msg: anthropicschema.MessageParam{ + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_1", + Content: &anthropicschema.ToolResultContent{Text: "result"}, + }}, + }, + }, + }, + }, + { + name: "multiple tool_result blocks", + expected: true, + msg: anthropicschema.MessageParam{ + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_1", + Content: &anthropicschema.ToolResultContent{Text: "result 1"}, + }}, + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_2", + Content: &anthropicschema.ToolResultContent{Text: "result 2"}, + }}, + }, + }, + }, + }, + { + name: "mixed text and tool_result blocks", + expected: false, + msg: anthropicschema.MessageParam{ + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {Text: &anthropicschema.TextBlockParam{Type: "text", Text: "Here is the result:"}}, + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_1", + Content: &anthropicschema.ToolResultContent{Text: "72°F and sunny"}, + }}, + }, + }, + }, + }, + { + name: "plain text user message", + expected: false, + msg: anthropicschema.MessageParam{ + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{Text: "Hello"}, + }, + }, + { + name: "assistant message is never tool-result-only", + expected: false, + msg: anthropicschema.MessageParam{ + Role: anthropicschema.MessageRoleAssistant, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_1", + Content: &anthropicschema.ToolResultContent{Text: "result"}, + }}, + }, + }, + }, + }, + { + name: "empty content array", + expected: false, + msg: anthropicschema.MessageParam{ + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{}, + }, + }, + { + name: "text array without tool_result", + expected: false, + msg: anthropicschema.MessageParam{ + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {Text: &anthropicschema.TextBlockParam{Type: "text", Text: "Hello"}}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isOnlyToolResult(&tt.msg)) + }) + } +} + +func TestAnthropicToAWSBedrockTranslator_RequestBody_MixedUserContentWithToolResult(t *testing.T) { + // Regression test: a user message containing both text and tool_result blocks + // must preserve all content blocks, not silently drop the text. + translator := NewAnthropicToAWSBedrockTranslator("") + req := &anthropicschema.MessagesRequest{ + Model: "test-model", + MaxTokens: 100, + Messages: []anthropicschema.MessageParam{ + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{Text: "What's the weather?"}, + }, + { + Role: anthropicschema.MessageRoleAssistant, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {ToolUse: &anthropicschema.ToolUseBlockParam{ + Type: "tool_use", + ID: "tu_abc", + Name: "get_weather", + Input: map[string]any{"city": "NYC"}, + }}, + }, + }, + }, + // Mixed content: text + tool_result in the same user message. + // This should go through convertUserMessage (not convertToolResultMessage), + // preserving the text block. + { + Role: anthropicschema.MessageRoleUser, + Content: anthropicschema.MessageContent{ + Array: []anthropicschema.ContentBlockParam{ + {Text: &anthropicschema.TextBlockParam{Type: "text", Text: "Here is the result:"}}, + {ToolResult: &anthropicschema.ToolResultBlockParam{ + Type: "tool_result", + ToolUseID: "tu_abc", + Content: &anthropicschema.ToolResultContent{Text: "72°F and sunny"}, + }}, + }, + }, + }, + }, + } + 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) + + // 3 messages: user(text), assistant(tool_use), user(text+tool_result) + require.Len(t, bedrockReq.Messages, 3) + + // The third message should have both a text block and a tool_result block. + mixedMsg := bedrockReq.Messages[2] + assert.Equal(t, awsbedrock.ConversationRoleUser, mixedMsg.Role) + require.Len(t, mixedMsg.Content, 2, "mixed user message should preserve both text and tool_result blocks") + + // First block: text. + require.NotNil(t, mixedMsg.Content[0].Text) + assert.Equal(t, "Here is the result:", *mixedMsg.Content[0].Text) + + // Second block: tool result. + require.NotNil(t, mixedMsg.Content[1].ToolResult) + assert.Equal(t, "tu_abc", *mixedMsg.Content[1].ToolResult.ToolUseID) + require.Len(t, mixedMsg.Content[1].ToolResult.Content, 1) + assert.Equal(t, "72°F and sunny", *mixedMsg.Content[1].ToolResult.Content[0].Text) +} diff --git a/internal/translator/openai_awsbedrock.go b/internal/translator/openai_awsbedrock.go index 7f10ad6764..486a1f1195 100644 --- a/internal/translator/openai_awsbedrock.go +++ b/internal/translator/openai_awsbedrock.go @@ -737,7 +737,7 @@ func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) ResponseBody(_ map[string } err = serializeOpenAIChatCompletionChunk(oaiEvent, &newBody) if err != nil { - panic(fmt.Errorf("failed to marshal event: %w", err)) + return nil, nil, metrics.TokenUsage{}, "", fmt.Errorf("failed to marshal streaming event: %w", err) } if span != nil { span.RecordResponseChunk(oaiEvent) diff --git a/internal/translator/openai_awsbedrock_test.go b/internal/translator/openai_awsbedrock_test.go index 4cc83339f6..e10a622106 100644 --- a/internal/translator/openai_awsbedrock_test.go +++ b/internal/translator/openai_awsbedrock_test.go @@ -2685,3 +2685,52 @@ func requireNoEmptyAssistantContent(t *testing.T, messages []*awsbedrock.Message } } } + +func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_Streaming_ResponseBody_MarshalError(t *testing.T) { + // Build a valid Bedrock streaming event that will be successfully decoded, + // but inject a json.Marshal failure to exercise the error return path + // that was previously a panic. + inputEvents := []awsbedrock.ConverseStreamEvent{ + {Role: ptr.To(awsbedrock.ConversationRoleAssistant)}, + { + ContentBlockIndex: 0, + EventType: awsbedrock.ConverseStreamEventTypeContentBlockDelta.String(), + Delta: &awsbedrock.ConverseStreamEventContentBlockDelta{ + Text: ptr.To("hello"), + }, + }, + {StopReason: ptr.To(awsbedrock.StopReasonEndTurn)}, + } + + buf := bytes.NewBuffer(nil) + encoder := eventstream.NewEncoder() + for _, event := range inputEvents { + payload, err := json.Marshal(event) + require.NoError(t, err) + err = encoder.Encode(buf, eventstream.Message{ + Headers: eventstream.Headers{ + {Name: ":event-type", Value: eventstream.StringValue("chunk")}, + }, + Payload: payload, + }) + require.NoError(t, err) + } + + // Override json.Marshal to force a failure during serializeOpenAIChatCompletionChunk. + orig := json.Marshal + t.Cleanup(func() { json.Marshal = orig }) + json.Marshal = func(v interface{}) ([]byte, error) { + // Allow the eventstream decoding (internal json.Unmarshal) to succeed + // by only failing on ChatCompletionResponseChunk marshaling. + if _, ok := v.(*openai.ChatCompletionResponseChunk); ok { + return nil, fmt.Errorf("injected marshal error") + } + return orig(v) + } + + o := &openAIToAWSBedrockTranslatorV1ChatCompletion{stream: true, requestModel: "test-model"} + _, _, _, _, err := o.ResponseBody(nil, buf, true, nil) + require.Error(t, err) + require.ErrorContains(t, err, "failed to marshal streaming event") + require.ErrorContains(t, err, "injected marshal error") +} From cb0749cc815a4cff65d7a5831720d16dc48ef42a Mon Sep 17 00:00:00 2001 From: Karan Kothari <152941307+kothari-karan@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:16:32 -0700 Subject: [PATCH 50/59] fix: dedup candidate namespaces in getObjectsForGateway (#2304) --- internal/controller/gateway.go | 8 +++++++- internal/controller/gateway_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/controller/gateway.go b/internal/controller/gateway.go index 6e331f0f8e..058e2783b0 100644 --- a/internal/controller/gateway.go +++ b/internal/controller/gateway.go @@ -1148,8 +1148,14 @@ func (c *GatewayController) getObjectsForGateway(ctx context.Context, gw *gwapiv "%s=%s,%s=%s", egOwningGatewayNameLabel, gw.Name, egOwningGatewayNamespaceLabel, gw.Namespace, )} + candidateNamespaces := make([]string, 1, 2) + candidateNamespaces[0] = gw.Namespace + if c.envoyGatewayNamespace != gw.Namespace { + candidateNamespaces = append(candidateNamespaces, c.envoyGatewayNamespace) + } + var distinctNamespaces []string - for _, ns := range []string{gw.Namespace, c.envoyGatewayNamespace} { + for _, ns := range candidateNamespaces { var ps *corev1.PodList ps, err = c.kube.CoreV1().Pods(ns).List(ctx, listOption) if err != nil { diff --git a/internal/controller/gateway_test.go b/internal/controller/gateway_test.go index bd1063fd1a..7228a0c43f 100644 --- a/internal/controller/gateway_test.go +++ b/internal/controller/gateway_test.go @@ -2837,3 +2837,31 @@ func TestGatewayController_getObjectsForGatewayNamespaceInconsistency(t *testing require.Error(t, err) require.Contains(t, err.Error(), "found gateway-labeled objects in multiple namespaces") } + +func TestGatewayController_getObjectsForGatewaySameNamespace(t *testing.T) { + const gwName, ns = "gw", "shared" + labels := map[string]string{ + egOwningGatewayNameLabel: gwName, + egOwningGatewayNamespaceLabel: ns, + } + gw := &gwapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: gwName, Namespace: ns}} + + kube := fake2.NewClientset() + c := NewGatewayController(requireNewFakeClientWithIndexes(t), kube, ctrl.Log, ns, + "docker.io/envoyproxy/ai-gateway-extproc:latest", "info", false, nil, true) + + _, err := kube.CoreV1().Pods(ns).Create(t.Context(), &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1", Namespace: ns, Labels: labels}, + }, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = kube.AppsV1().Deployments(ns).Create(t.Context(), &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "dep-1", Namespace: ns, Labels: labels}, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + namespace, pods, deployments, _, err := c.getObjectsForGateway(t.Context(), gw) + require.NoError(t, err) + require.Equal(t, ns, namespace) + require.Len(t, pods, 1) + require.Len(t, deployments, 1) +} From 1fd5f4633a54a91694f0a1ff2d84c39071294dd8 Mon Sep 17 00:00:00 2001 From: aias00 Date: Thu, 9 Jul 2026 23:28:43 +0800 Subject: [PATCH 51/59] fix: honor namespace on cross-namespace InferencePool backendRef (#2339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** `AIGatewayRoute` `rules[].backendRefs[]` accepts a `namespace` field, but when a backendRef points at an `InferencePool` in a different namespace, the controller generated the child `HTTPRoute` backendRef with the route's *own* namespace instead of the referenced one. The route then reported `ResolvedRefs: False` with `BackendNotFound: custom backend InferencePool / not found`, even with a valid `ReferenceGrant` in the pool's namespace. This PR makes the controller honor the `namespace` specified on an `InferencePool` backendRef when generating the `HTTPRoute`, and validates cross-namespace `InferencePool` references via `ReferenceGrant` — consistent with the existing `AIServiceBackend` behavior. Same-namespace references are unaffected and require no `ReferenceGrant`. Implementation notes: - `newHTTPRoute` now uses `backendRef.GetNamespace(route.Namespace)` for the generated `InferencePool` backendRef instead of hardcoding the route namespace. - The `referenceGrantValidator` is refactored to be generic over the target group/kind; `validateAIServiceBackendReference` is preserved as a thin wrapper (behavior unchanged) and a new `validateInferencePoolReference` is added. - The Gateway controller already resolved the `InferencePool`/`BackendSecurityPolicy` using the backendRef namespace, so fixing the generated `HTTPRoute` namespace completes the end-to-end path. **Related Issues/PRs (if applicable)** Fixes #2230 **Special notes for reviewers (if applicable)** Added unit tests: - `Test_newHTTPRoute_InferencePool_CrossNamespace` — namespace honored with a valid `ReferenceGrant`, rejected without one, and same-namespace needs no grant. - `TestReferenceGrantValidator_ValidateInferencePoolReference` — cross-namespace grant matching for `InferencePool` targets. Verified with `go build`, `go vet`, and `go test ./internal/controller/...` (all passing), and `gofmt`. Signed-off-by: liuhy --- internal/controller/ai_gateway_route.go | 16 ++- internal/controller/ai_gateway_route_test.go | 82 ++++++++++++++ internal/controller/referencegrant.go | 79 ++++++++++---- internal/controller/referencegrant_test.go | 109 ++++++++++++++++++- 4 files changed, 262 insertions(+), 24 deletions(-) diff --git a/internal/controller/ai_gateway_route.go b/internal/controller/ai_gateway_route.go index 740f49e704..4f96860aa5 100644 --- a/internal/controller/ai_gateway_route.go +++ b/internal/controller/ai_gateway_route.go @@ -262,14 +262,26 @@ func (c *AIGatewayRouteController) newHTTPRoute(ctx context.Context, dst *gwapiv dstName := fmt.Sprintf("%s.%s", br.Name, backendNamespace) if br.IsInferencePool() { - // Handle InferencePool backend reference. + // Handle InferencePool backend reference, honoring the (optionally cross-namespace) + // namespace specified on the backendRef. + if br.IsCrossNamespace(aiGatewayRoute.Namespace) { + if err := c.referenceGrantValidator.validateInferencePoolReference( + ctx, + aiGatewayRoute.Namespace, + backendNamespace, + br.Name, + ); err != nil { + return err + } + } + ns := gwapiv1.Namespace(backendNamespace) backendRefs = append(backendRefs, gwapiv1.HTTPBackendRef{BackendRef: gwapiv1.BackendRef{ BackendObjectReference: gwapiv1.BackendObjectReference{ Group: (*gwapiv1.Group)(br.Group), Kind: (*gwapiv1.Kind)(br.Kind), Name: gwapiv1.ObjectName(br.Name), - Namespace: (*gwapiv1.Namespace)(&aiGatewayRoute.Namespace), + Namespace: &ns, }, Weight: br.Weight, }}, diff --git a/internal/controller/ai_gateway_route_test.go b/internal/controller/ai_gateway_route_test.go index e3a3d58069..d38da246d4 100644 --- a/internal/controller/ai_gateway_route_test.go +++ b/internal/controller/ai_gateway_route_test.go @@ -576,6 +576,88 @@ func Test_newHTTPRoute_InferencePool(t *testing.T) { require.Empty(t, httpRoute.Spec.Rules[1].BackendRefs) // No backend refs for default rule. } +func Test_newHTTPRoute_InferencePool_CrossNamespace(t *testing.T) { + newRoute := func(poolNamespace string) *aigv1b1.AIGatewayRoute { + return &aigv1b1.AIGatewayRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "inference-route", + Namespace: "gw", + }, + Spec: aigv1b1.AIGatewayRouteSpec{ + Rules: []aigv1b1.AIGatewayRouteRule{ + { + BackendRefs: []aigv1b1.AIGatewayRouteRuleBackendRef{ + { + Name: "my-pool", + Namespace: ptr.To(gwapiv1.Namespace(poolNamespace)), + Group: ptr.To("inference.networking.k8s.io"), + Kind: ptr.To("InferencePool"), + Weight: ptr.To(int32(100)), + }, + }, + }, + }, + }, + } + } + + referenceGrant := &gwapiv1b1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{Name: "allow-gw", Namespace: "default"}, + Spec: gwapiv1b1.ReferenceGrantSpec{ + From: []gwapiv1b1.ReferenceGrantFrom{{ + Group: aiServiceBackendGroup, + Kind: aiGatewayRouteKind, + Namespace: "gw", + }}, + To: []gwapiv1b1.ReferenceGrantTo{{ + Group: inferencePoolGroup, + Kind: inferencePoolKind, + }}, + }, + } + + t.Run("cross-namespace with ReferenceGrant honors the pool namespace", func(t *testing.T) { + c := requireNewFakeClientWithIndexes(t) + require.NoError(t, c.Create(t.Context(), referenceGrant)) + + controller := &AIGatewayRouteController{client: c, referenceGrantValidator: newReferenceGrantValidator(c)} + httpRoute := &gwapiv1.HTTPRoute{} + require.NoError(t, controller.newHTTPRoute(t.Context(), httpRoute, newRoute("default"))) + + require.Len(t, httpRoute.Spec.Rules, 2) + require.Len(t, httpRoute.Spec.Rules[0].BackendRefs, 1) + backendRef := httpRoute.Spec.Rules[0].BackendRefs[0] + require.Equal(t, "InferencePool", string(*backendRef.Kind)) + require.Equal(t, "my-pool", string(backendRef.Name)) + // The generated HTTPRoute backendRef must carry the pool's namespace, not the route's. + require.NotNil(t, backendRef.Namespace) + require.Equal(t, "default", string(*backendRef.Namespace)) + }) + + t.Run("cross-namespace without ReferenceGrant is rejected", func(t *testing.T) { + c := requireNewFakeClientWithIndexes(t) + + controller := &AIGatewayRouteController{client: c, referenceGrantValidator: newReferenceGrantValidator(c)} + httpRoute := &gwapiv1.HTTPRoute{} + err := controller.newHTTPRoute(t.Context(), httpRoute, newRoute("default")) + require.Error(t, err) + require.Contains(t, err.Error(), "cross-namespace reference from AIGatewayRoute in namespace gw "+ + "to InferencePool my-pool in namespace default is not permitted") + }) + + t.Run("same-namespace reference needs no ReferenceGrant", func(t *testing.T) { + c := requireNewFakeClientWithIndexes(t) + + controller := &AIGatewayRouteController{client: c, referenceGrantValidator: newReferenceGrantValidator(c)} + httpRoute := &gwapiv1.HTTPRoute{} + require.NoError(t, controller.newHTTPRoute(t.Context(), httpRoute, newRoute("gw"))) + + backendRef := httpRoute.Spec.Rules[0].BackendRefs[0] + require.NotNil(t, backendRef.Namespace) + require.Equal(t, "gw", string(*backendRef.Namespace)) + }) +} + func Test_newHTTPRoute_LabelAndAnnotationPropagation(t *testing.T) { c := requireNewFakeClientWithIndexes(t) diff --git a/internal/controller/referencegrant.go b/internal/controller/referencegrant.go index 79df434e6f..347cb94f95 100644 --- a/internal/controller/referencegrant.go +++ b/internal/controller/referencegrant.go @@ -28,7 +28,7 @@ func newReferenceGrantValidator(c client.Client) *referenceGrantValidator { return &referenceGrantValidator{client: c} } -// ValidateAIServiceBackendReference validates that an AIGatewayRoute can reference an AIServiceBackend +// validateAIServiceBackendReference validates that an AIGatewayRoute can reference an AIServiceBackend // in a different namespace by checking for a valid ReferenceGrant. // // Parameters: @@ -45,39 +45,78 @@ func (v *referenceGrantValidator) validateAIServiceBackendReference( backendNamespace string, backendName string, ) error { - // Same namespace references don't need ReferenceGrant - if routeNamespace == backendNamespace { + return v.validateReference(ctx, routeNamespace, backendNamespace, backendName, aiServiceBackendGroup, aiServiceBackendKind) +} + +// validateInferencePoolReference validates that an AIGatewayRoute can reference an InferencePool +// in a different namespace by checking for a valid ReferenceGrant. +// +// Parameters: +// - ctx: context for the operation +// - routeNamespace: namespace of the AIGatewayRoute +// - poolNamespace: namespace of the InferencePool +// - poolName: name of the InferencePool (optional, for logging) +// +// Returns: +// - error: nil if the reference is valid (same namespace or valid ReferenceGrant exists), error otherwise +func (v *referenceGrantValidator) validateInferencePoolReference( + ctx context.Context, + routeNamespace string, + poolNamespace string, + poolName string, +) error { + return v.validateReference(ctx, routeNamespace, poolNamespace, poolName, inferencePoolGroup, inferencePoolKind) +} + +// validateReference validates that an AIGatewayRoute can reference a target resource (identified by +// targetGroup/targetKind) in a different namespace by checking for a valid ReferenceGrant. +func (v *referenceGrantValidator) validateReference( + ctx context.Context, + routeNamespace string, + targetNamespace string, + targetName string, + targetGroup gwapiv1b1.Group, + targetKind gwapiv1b1.Kind, +) error { + // Same namespace references don't need ReferenceGrant. + if routeNamespace == targetNamespace { return nil } - indexKey := getReferenceGrantIndexKey(backendNamespace, aiServiceBackendKind) + indexKey := getReferenceGrantIndexKey(targetNamespace, string(targetKind)) var referenceGrants gwapiv1b1.ReferenceGrantList if err := v.client.List(ctx, &referenceGrants, client.MatchingFields{k8sClientIndexReferenceGrantToTargetKind: indexKey}, ); err != nil { return fmt.Errorf("failed to list ReferenceGrants in namespace %s for kind %s: %w", - backendNamespace, aiServiceBackendKind, err) + targetNamespace, targetKind, err) } - // Check if any ReferenceGrant allows this cross-namespace reference + // Check if any ReferenceGrant allows this cross-namespace reference. for i := range referenceGrants.Items { grant := &referenceGrants.Items[i] - if v.isReferenceGrantValid(grant, routeNamespace) { + if v.isReferenceGrantValid(grant, routeNamespace, targetGroup, targetKind) { return nil } } return fmt.Errorf( - "cross-namespace reference from AIGatewayRoute in namespace %s to AIServiceBackend %s in namespace %s is not permitted: "+ + "cross-namespace reference from AIGatewayRoute in namespace %s to %s %s in namespace %s is not permitted: "+ "no valid ReferenceGrant found in namespace %s. "+ - "A ReferenceGrant must allow AIGatewayRoute from namespace %s to reference AIServiceBackend in namespace %s", - routeNamespace, backendName, backendNamespace, backendNamespace, routeNamespace, backendNamespace, + "A ReferenceGrant must allow AIGatewayRoute from namespace %s to reference %s in namespace %s", + routeNamespace, targetKind, targetName, targetNamespace, targetNamespace, routeNamespace, targetKind, targetNamespace, ) } -// isReferenceGrantValid checks if a ReferenceGrant allows an AIGatewayRoute to reference an AIServiceBackend. -func (v *referenceGrantValidator) isReferenceGrantValid(grant *gwapiv1b1.ReferenceGrant, fromNamespace string) bool { - // Check if the grant allows references from the route's namespace +// isReferenceGrantValid checks if a ReferenceGrant allows an AIGatewayRoute to reference the +// target resource identified by targetGroup/targetKind. +func (v *referenceGrantValidator) isReferenceGrantValid( + grant *gwapiv1b1.ReferenceGrant, + fromNamespace string, + targetGroup gwapiv1b1.Group, + targetKind gwapiv1b1.Kind, +) bool { + // Check if the grant allows references from the route's namespace. fromAllowed := false for _, from := range grant.Spec.From { if v.matchesFrom(&from, fromNamespace) { @@ -90,9 +129,9 @@ func (v *referenceGrantValidator) isReferenceGrantValid(grant *gwapiv1b1.Referen return false } - // Check if the grant allows references to AIServiceBackend + // Check if the grant allows references to the target resource. for _, to := range grant.Spec.To { - if v.matchesTo(&to) { + if v.matchesTo(&to, targetGroup, targetKind) { return true } } @@ -102,7 +141,7 @@ func (v *referenceGrantValidator) isReferenceGrantValid(grant *gwapiv1b1.Referen // matchesFrom checks if a ReferenceGrantFrom matches the AIGatewayRoute reference. func (v *referenceGrantValidator) matchesFrom(from *gwapiv1b1.ReferenceGrantFrom, fromNamespace string) bool { - // Check group + // Check group. AIGatewayRoute belongs to the aigateway.envoyproxy.io group. if from.Group != aiServiceBackendGroup { return false } @@ -120,15 +159,15 @@ func (v *referenceGrantValidator) matchesFrom(from *gwapiv1b1.ReferenceGrantFrom return true } -// matchesTo checks if a ReferenceGrantTo matches the AIServiceBackend. -func (v *referenceGrantValidator) matchesTo(to *gwapiv1b1.ReferenceGrantTo) bool { +// matchesTo checks if a ReferenceGrantTo matches the target resource identified by targetGroup/targetKind. +func (v *referenceGrantValidator) matchesTo(to *gwapiv1b1.ReferenceGrantTo, targetGroup gwapiv1b1.Group, targetKind gwapiv1b1.Kind) bool { // Check group - if to.Group != aiServiceBackendGroup { + if to.Group != targetGroup { return false } // Check kind - if to.Kind != aiServiceBackendKind { + if to.Kind != targetKind { return false } diff --git a/internal/controller/referencegrant_test.go b/internal/controller/referencegrant_test.go index 266d561f65..0102b23ffb 100644 --- a/internal/controller/referencegrant_test.go +++ b/internal/controller/referencegrant_test.go @@ -280,6 +280,111 @@ func TestReferenceGrantValidator_ValidateAIServiceBackendReference(t *testing.T) }) } +// TestReferenceGrantValidator_ValidateInferencePoolReference tests cross-namespace InferencePool +// references governed by ReferenceGrant, mirroring the AIServiceBackend behavior. +func TestReferenceGrantValidator_ValidateInferencePoolReference(t *testing.T) { + scheme := runtime.NewScheme() + _ = gwapiv1b1.Install(scheme) + _ = aigv1a1.AddToScheme(scheme) + + inferencePoolGrant := func(fromNS string, toGroup gwapiv1b1.Group, toKind gwapiv1b1.Kind) gwapiv1b1.ReferenceGrant { + return gwapiv1b1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{Name: "grant", Namespace: "pool-ns"}, + Spec: gwapiv1b1.ReferenceGrantSpec{ + From: []gwapiv1b1.ReferenceGrantFrom{{ + Group: aiServiceBackendGroup, + Kind: aiGatewayRouteKind, + Namespace: gwapiv1b1.Namespace(fromNS), + }}, + To: []gwapiv1b1.ReferenceGrantTo{{Group: toGroup, Kind: toKind}}, + }, + } + } + + tests := []struct { + name string + routeNamespace string + poolNamespace string + poolName string + referenceGrants []gwapiv1b1.ReferenceGrant + expectedError bool + expectedErrorString string + }{ + { + name: "Same namespace reference - should succeed", + routeNamespace: "default", + poolNamespace: "default", + poolName: "my-pool", + }, + { + name: "Cross-namespace with valid ReferenceGrant - should succeed", + routeNamespace: "route-ns", + poolNamespace: "pool-ns", + poolName: "my-pool", + referenceGrants: []gwapiv1b1.ReferenceGrant{inferencePoolGrant("route-ns", inferencePoolGroup, inferencePoolKind)}, + }, + { + name: "Cross-namespace without ReferenceGrant - should fail", + routeNamespace: "route-ns", + poolNamespace: "pool-ns", + poolName: "my-pool", + expectedError: true, + expectedErrorString: "cross-namespace reference from AIGatewayRoute in namespace route-ns " + + "to InferencePool my-pool in namespace pool-ns is not permitted", + }, + { + name: "Cross-namespace with ReferenceGrant for wrong target kind - should fail", + routeNamespace: "route-ns", + poolNamespace: "pool-ns", + poolName: "my-pool", + referenceGrants: []gwapiv1b1.ReferenceGrant{inferencePoolGrant("route-ns", aiServiceBackendGroup, aiServiceBackendKind)}, + expectedError: true, + // Grant targets AIServiceBackend, so the InferencePool index lookup finds no matching grant. + expectedErrorString: "is not permitted", + }, + { + name: "Cross-namespace with ReferenceGrant for wrong from namespace - should fail", + routeNamespace: "route-ns", + poolNamespace: "pool-ns", + poolName: "my-pool", + referenceGrants: []gwapiv1b1.ReferenceGrant{inferencePoolGrant("other-ns", inferencePoolGroup, inferencePoolKind)}, + expectedError: true, + expectedErrorString: "is not permitted", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + objs := make([]client.Object, len(tt.referenceGrants)) + for i := range tt.referenceGrants { + objs[i] = &tt.referenceGrants[i] + } + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(objs...). + WithIndex(&gwapiv1b1.ReferenceGrant{}, k8sClientIndexReferenceGrantToTargetKind, referenceGrantToTargetKindIndexFunc). + Build() + + validator := newReferenceGrantValidator(fakeClient) + err := validator.validateInferencePoolReference( + context.Background(), + tt.routeNamespace, + tt.poolNamespace, + tt.poolName, + ) + + if tt.expectedError { + require.Error(t, err) + if tt.expectedErrorString != "" { + require.Contains(t, err.Error(), tt.expectedErrorString) + } + } else { + require.NoError(t, err) + } + }) + } +} + // TestReferenceGrantValidator_MatchesFrom_WrongGroup tests matchesFrom with wrong group func TestReferenceGrantValidator_MatchesFrom_WrongGroup(t *testing.T) { scheme := runtime.NewScheme() @@ -351,7 +456,7 @@ func TestReferenceGrantValidator_MatchesTo_WrongGroup(t *testing.T) { Kind: aiServiceBackendKind, } - result := validator.matchesTo(to) + result := validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind) require.False(t, result, "should return false for wrong group") } @@ -369,7 +474,7 @@ func TestReferenceGrantValidator_MatchesTo_WrongKind(t *testing.T) { Kind: "WrongKind", } - result := validator.matchesTo(to) + result := validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind) require.False(t, result, "should return false for wrong kind") } From 4d1e7b9da527a62580298a1224e2a9b7d03e15dd Mon Sep 17 00:00:00 2001 From: aias00 Date: Sat, 11 Jul 2026 18:13:30 +0800 Subject: [PATCH 52/59] fix: prevent double counting of Anthropic streaming cache tokens (#2236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** The `message_delta` event handler in `anthropicStreamParser` was using `Add*` methods for input tokens, cached tokens, and cache creation tokens. However, the Anthropic API reports cumulative (not incremental) token counts in `message_delta`, and `message_start` already sets these values correctly via `ExtractTokenUsageFromExplicitCaching`. Using `Add` on top of the already-set cumulative values caused cache tokens to be counted twice. ### Bug Example With `input_tokens=9` and `cache_read_input_tokens=1`: | Step | Action | InputTokens | CachedInputTokens | |------|--------|-------------|-------------------| | `message_start` | `SetInputTokens(10)`, `SetCachedInputTokens(1)` | 10 ✅ | 1 ✅ | | `message_delta` (before fix) | `AddInputTokens(1)`, `AddCachedInputTokens(1)` | 11 ❌ | 2 ❌ | | `message_delta` (after fix) | Only `SetOutputTokens(16)` | 10 ✅ | 1 ✅ | ### Root Cause `ExtractTokenUsageFromExplicitCaching` computes `InputTokens = input_tokens + cache_read + cache_creation`. In `message_start`, this is correctly set via `Set*` methods. In `message_delta`, the same cumulative values were being added via `Add*` methods, causing double counting. ### Fix The `message_delta` handler now only updates output tokens using `SetOutputTokens`, since `message_start` typically reports `output_tokens=0` and `message_delta` provides the final count. This matches the pattern already used in `anthropic_anthropic.go`'s `reflectStreamingEvent`. The `MessageDeltaUsage` struct uses non-pointer `int64` fields that default to 0 when absent from JSON, making it impossible to distinguish "not provided" from "actually zero" — so we cannot safely `Set` override from delta either, as it would erase correct `message_start` values with zeros. ### Changes - `internal/translator/anthropic_helper.go`: Removed `Add*` calls for input/cache tokens from `message_delta` handler; replaced `AddOutputTokens` with `SetOutputTokens`; removed unused `ExtractTokenUsageFromExplicitCaching` call; added `>= 0` guard for `ThinkingTokens` - `internal/translator/anthropic_helper_test.go`: Added `TestAnthropicStreamParserTokenUsage_NoDoubleCounting` with 6 test cases ### Test Coverage - Cache tokens in both `message_start` and `message_delta` (core double-count scenario) - Cache creation tokens in both events - Both cache_read and cache_creation in both events - No cache tokens (baseline correctness) - Cache tokens only in `message_start`, not in `message_delta` - Cache tokens only in `message_delta` — verifies they are ignored (non-pointer int64 fields cannot distinguish absent from zero) --------- Signed-off-by: liuhy Co-authored-by: Claude Co-authored-by: Ignasi Barrera --- internal/translator/anthropic_helper.go | 94 +++++- internal/translator/anthropic_helper_test.go | 337 +++++++++++++++++++ 2 files changed, 421 insertions(+), 10 deletions(-) diff --git a/internal/translator/anthropic_helper.go b/internal/translator/anthropic_helper.go index 914d70f212..54d2a9074c 100644 --- a/internal/translator/anthropic_helper.go +++ b/internal/translator/anthropic_helper.go @@ -829,6 +829,67 @@ func (p *anthropicStreamParser) writeChunk(eventBlock []byte, buf *[]byte) error return nil } +type messageDeltaUsageFields struct { + Usage *struct { + InputTokens *int64 `json:"input_tokens"` + CacheReadInputTokens *int64 `json:"cache_read_input_tokens"` + CacheCreationInputTokens *int64 `json:"cache_creation_input_tokens"` + } `json:"usage"` +} + +func (p *anthropicStreamParser) updateInputUsageFromMessageDelta(data []byte) error { + // message_delta provides cumulative (not incremental) token counts. + // This function handles input_tokens, cache_read_input_tokens, and cache_creation_input_tokens + // from message_delta. We use Set (not Add) because these are cumulative totals. + // This prevents double-counting when both message_start and message_delta report the same + // cache tokens, and also handles cases where: + // - Cache tokens are reported only in message_delta (not in message_start) + // - message_delta provides corrected/updated cache token values that override message_start + var event messageDeltaUsageFields + if err := json.Unmarshal(data, &event); err != nil { + return fmt.Errorf("unmarshal message_delta usage fields: %w", err) + } + if event.Usage == nil { + return nil + } + + u := event.Usage + inputPresent := u.InputTokens != nil && *u.InputTokens >= 0 + cacheReadPresent := u.CacheReadInputTokens != nil && *u.CacheReadInputTokens >= 0 + cacheCreationPresent := u.CacheCreationInputTokens != nil && *u.CacheCreationInputTokens >= 0 + if !inputPresent && !cacheReadPresent && !cacheCreationPresent { + return nil + } + + baseInputTokens := uint32(0) + if inputPresent { + baseInputTokens = uint32(*u.InputTokens) //nolint:gosec + } else if inputTokens, ok := p.tokenUsage.InputTokens(); ok { + baseInputTokens = inputTokens + if cachedTokens, ok := p.tokenUsage.CachedInputTokens(); ok && baseInputTokens >= cachedTokens { + baseInputTokens -= cachedTokens + } + if cacheCreationTokens, ok := p.tokenUsage.CacheCreationInputTokens(); ok && baseInputTokens >= cacheCreationTokens { + baseInputTokens -= cacheCreationTokens + } + } + + cachedTokens, _ := p.tokenUsage.CachedInputTokens() + if cacheReadPresent { + cachedTokens = uint32(*u.CacheReadInputTokens) //nolint:gosec + p.tokenUsage.SetCachedInputTokens(cachedTokens) + } + + cacheCreationTokens, _ := p.tokenUsage.CacheCreationInputTokens() + if cacheCreationPresent { + cacheCreationTokens = uint32(*u.CacheCreationInputTokens) //nolint:gosec + p.tokenUsage.SetCacheCreationInputTokens(cacheCreationTokens) + } + + p.tokenUsage.SetInputTokens(baseInputTokens + cachedTokens + cacheCreationTokens) + return nil +} + // Process reads from the Anthropic SSE stream, translates events to OpenAI chunks, // and returns the mutations for Envoy. func (p *anthropicStreamParser) Process(body io.Reader, endOfStream bool, span tracingapi.ChatCompletionSpan) ( @@ -1054,18 +1115,31 @@ func (p *anthropicStreamParser) handleAnthropicStreamEvent(eventType []byte, dat if err := json.Unmarshal(data, &event); err != nil { return nil, fmt.Errorf("unmarshal message_delta: %w", err) } + // Update input and cache token usage from message_delta. + // This handles cases where cache tokens are only in message_delta, + // or where message_delta provides corrected totals that override message_start. + if err := p.updateInputUsageFromMessageDelta(data); err != nil { + return nil, err + } u := event.Usage - usage := metrics.ExtractTokenUsageFromExplicitCaching( - u.InputTokens, - u.OutputTokens, - &u.CacheReadInputTokens, - &u.CacheCreationInputTokens, - ) - // For message_delta, accumulate the incremental output tokens - if output, ok := usage.OutputTokens(); ok { - p.tokenUsage.AddOutputTokens(output) + // message_delta provides cumulative (not incremental) output token counts. + // Use Set (not Add) because the value is cumulative. + // message_start typically reports output_tokens=0, and message_delta + // provides the final output token count. + // + // Guard with the SDK's JSON presence fields (Valid()): MessageDeltaUsage + // uses non-pointer int64 fields that default to 0 when absent, so a bare + // value check cannot distinguish "not provided" from "actually zero". A + // stream may emit multiple message_delta events (usage is cumulative), so a + // later message_delta that omits usage must NOT clobber output/reasoning + // tokens set by an earlier one — see + // https://docs.anthropic.com/en/api/messages-streaming + if u.JSON.OutputTokens.Valid() { + p.tokenUsage.SetOutputTokens(uint32(u.OutputTokens)) //nolint:gosec + } + if u.JSON.OutputTokensDetails.Valid() && u.OutputTokensDetails.JSON.ThinkingTokens.Valid() { + p.tokenUsage.SetReasoningTokens(uint32(u.OutputTokensDetails.ThinkingTokens)) //nolint:gosec } - p.tokenUsage.SetReasoningTokens(uint32(u.OutputTokensDetails.ThinkingTokens)) //nolint:gosec if event.Delta.StopReason != "" { p.stopReason = event.Delta.StopReason } diff --git a/internal/translator/anthropic_helper_test.go b/internal/translator/anthropic_helper_test.go index c395094854..fcd61e8d37 100644 --- a/internal/translator/anthropic_helper_test.go +++ b/internal/translator/anthropic_helper_test.go @@ -755,6 +755,343 @@ func TestOutputConfigAvailable(t *testing.T) { } } +func TestAnthropicStreamParserTokenUsage_NoDoubleCounting(t *testing.T) { + // This test verifies that cache tokens are not double-counted when + // both message_start and message_delta report cache token usage. + // The Anthropic API reports cumulative totals in message_delta, not + // incremental deltas, so we must use Set (override) not Add (accumulate) + // for cache tokens from message_delta. + tests := []struct { + name string + messageStartInputTokens int64 + messageStartCacheRead int64 + messageStartCacheCreation int64 + messageDeltaInputTokens *int64 + messageDeltaCacheRead *int64 + messageDeltaCacheCreation *int64 + messageDeltaOutputTokens int64 + expectedInputTokens uint32 + expectedCachedTokens uint32 + expectedCacheCreationToken uint32 + expectedOutputTokens uint32 + }{ + { + name: "cache tokens in both message_start and message_delta should not double count", + messageStartInputTokens: 9, + messageStartCacheRead: 1, + messageStartCacheCreation: 0, + messageDeltaInputTokens: ptr.To[int64](9), + messageDeltaCacheRead: ptr.To[int64](1), + messageDeltaCacheCreation: ptr.To[int64](0), + messageDeltaOutputTokens: 16, + expectedInputTokens: 10, // 9 base + 1 cache_read, NOT 11 (9+1+1 double counted) + expectedCachedTokens: 1, // NOT 2 (1+1 double counted) + expectedCacheCreationToken: 0, + expectedOutputTokens: 16, + }, + { + name: "cache creation tokens in both message_start and message_delta should not double count", + messageStartInputTokens: 5, + messageStartCacheRead: 0, + messageStartCacheCreation: 3, + messageDeltaInputTokens: ptr.To[int64](5), + messageDeltaCacheRead: ptr.To[int64](0), + messageDeltaCacheCreation: ptr.To[int64](3), + messageDeltaOutputTokens: 10, + expectedInputTokens: 8, // 5 base + 3 cache_creation, NOT 11 + expectedCachedTokens: 0, + expectedCacheCreationToken: 3, // NOT 6 + expectedOutputTokens: 10, + }, + { + name: "both cache_read and cache_creation in both events should not double count", + messageStartInputTokens: 9, + messageStartCacheRead: 2, + messageStartCacheCreation: 3, + messageDeltaInputTokens: ptr.To[int64](9), + messageDeltaCacheRead: ptr.To[int64](2), + messageDeltaCacheCreation: ptr.To[int64](3), + messageDeltaOutputTokens: 20, + expectedInputTokens: 14, // 9 + 2 + 3, NOT 19 (9+2+3+2+3) + expectedCachedTokens: 2, // NOT 4 + expectedCacheCreationToken: 3, // NOT 6 + expectedOutputTokens: 20, + }, + { + name: "no cache tokens - baseline correctness", + messageStartInputTokens: 9, + messageStartCacheRead: 0, + messageStartCacheCreation: 0, + messageDeltaOutputTokens: 16, + expectedInputTokens: 9, + expectedCachedTokens: 0, + expectedCacheCreationToken: 0, + expectedOutputTokens: 16, + }, + { + name: "cache only in message_start, not in message_delta", + messageStartInputTokens: 9, + messageStartCacheRead: 5, + messageStartCacheCreation: 2, + messageDeltaOutputTokens: 16, + expectedInputTokens: 16, // 9 + 5 + 2 + expectedCachedTokens: 5, + expectedCacheCreationToken: 2, + expectedOutputTokens: 16, + }, + { + name: "cache tokens only in message_delta are applied", + messageStartInputTokens: 9, + messageStartCacheRead: 0, + messageStartCacheCreation: 0, + messageDeltaInputTokens: ptr.To[int64](9), + messageDeltaCacheRead: ptr.To[int64](5), + messageDeltaCacheCreation: ptr.To[int64](2), + messageDeltaOutputTokens: 16, + expectedInputTokens: 16, // 9 + 5 + 2 from message_delta + expectedCachedTokens: 5, + expectedCacheCreationToken: 2, + expectedOutputTokens: 16, + }, + { + name: "corrected cache tokens in message_delta override message_start", + messageStartInputTokens: 9, + messageStartCacheRead: 5, + messageStartCacheCreation: 2, + messageDeltaInputTokens: ptr.To[int64](9), + messageDeltaCacheRead: ptr.To[int64](1), + messageDeltaCacheCreation: ptr.To[int64](0), + messageDeltaOutputTokens: 16, + expectedInputTokens: 10, // corrected 9 + 1 + 0, NOT stale 9 + 5 + 2 + expectedCachedTokens: 1, + expectedCacheCreationToken: 0, + expectedOutputTokens: 16, + }, + { + name: "message_delta with only cache_read, no input_tokens field", + messageStartInputTokens: 10, + messageStartCacheRead: 0, + messageStartCacheCreation: 0, + messageDeltaInputTokens: nil, // not present in message_delta + messageDeltaCacheRead: ptr.To[int64](3), // only cache_read in delta + messageDeltaCacheCreation: nil, + messageDeltaOutputTokens: 20, + expectedInputTokens: 13, // 10 base + 3 cache_read + expectedCachedTokens: 3, + expectedCacheCreationToken: 0, + expectedOutputTokens: 20, + }, + { + name: "message_delta with only cache_creation, no input_tokens field", + messageStartInputTokens: 8, + messageStartCacheRead: 0, + messageStartCacheCreation: 0, + messageDeltaInputTokens: nil, // not present in message_delta + messageDeltaCacheRead: nil, + messageDeltaCacheCreation: ptr.To[int64](4), // only cache_creation in delta + messageDeltaOutputTokens: 15, + expectedInputTokens: 12, // 8 base + 4 cache_creation + expectedCachedTokens: 0, + expectedCacheCreationToken: 4, + expectedOutputTokens: 15, + }, + { + name: "message_delta with both cache fields but no input_tokens field", + messageStartInputTokens: 7, + messageStartCacheRead: 0, + messageStartCacheCreation: 0, + messageDeltaInputTokens: nil, // not present in message_delta + messageDeltaCacheRead: ptr.To[int64](2), + messageDeltaCacheCreation: ptr.To[int64](3), + messageDeltaOutputTokens: 12, + expectedInputTokens: 12, // 7 base + 2 cache_read + 3 cache_creation + expectedCachedTokens: 2, + expectedCacheCreationToken: 3, + expectedOutputTokens: 12, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := newAnthropicStreamParser("test-model") + + messageDeltaUsageFields := []string{fmt.Sprintf(`"output_tokens":%d`, tt.messageDeltaOutputTokens)} + if tt.messageDeltaInputTokens != nil { + messageDeltaUsageFields = append(messageDeltaUsageFields, fmt.Sprintf(`"input_tokens":%d`, *tt.messageDeltaInputTokens)) + } + if tt.messageDeltaCacheRead != nil { + messageDeltaUsageFields = append(messageDeltaUsageFields, fmt.Sprintf(`"cache_read_input_tokens":%d`, *tt.messageDeltaCacheRead)) + } + if tt.messageDeltaCacheCreation != nil { + messageDeltaUsageFields = append(messageDeltaUsageFields, fmt.Sprintf(`"cache_creation_input_tokens":%d`, *tt.messageDeltaCacheCreation)) + } + + // Build the SSE stream with message_start and message_delta events. + sseStream := fmt.Sprintf(`event: message_start +data: {"type":"message_start","message":{"id":"msg_test","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":%d,"cache_read_input_tokens":%d,"cache_creation_input_tokens":%d,"output_tokens":0}}} + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}} + +event: content_block_stop +data: {"type":"content_block_stop","index":0} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{%s}} + +event: message_stop +data: {"type":"message_stop"} +`, + tt.messageStartInputTokens, + tt.messageStartCacheRead, + tt.messageStartCacheCreation, + strings.Join(messageDeltaUsageFields, ","), + ) + + _, _, tokenUsage, _, err := parser.Process(strings.NewReader(sseStream), true, nil) + require.NoError(t, err) + + inputTokens, inputSet := tokenUsage.InputTokens() + cachedTokens, cachedSet := tokenUsage.CachedInputTokens() + cacheCreationTokens, cacheCreationSet := tokenUsage.CacheCreationInputTokens() + outputTokens, outputSet := tokenUsage.OutputTokens() + + assert.True(t, inputSet, "input tokens should be set") + assert.Equal(t, tt.expectedInputTokens, inputTokens, "input tokens mismatch") + assert.True(t, cachedSet, "cached tokens should be set") + assert.Equal(t, tt.expectedCachedTokens, cachedTokens, "cached tokens mismatch") + assert.True(t, cacheCreationSet, "cache creation tokens should be set") + assert.Equal(t, tt.expectedCacheCreationToken, cacheCreationTokens, "cache creation tokens mismatch") + assert.True(t, outputSet, "output tokens should be set") + assert.Equal(t, tt.expectedOutputTokens, outputTokens, "output tokens mismatch") + }) + } +} + +func TestAnthropicStreamParserTokenUsage_MessageDeltaNoUsagePreservesPrior(t *testing.T) { + // A later message_delta that omits usage must NOT clobber output/reasoning + // tokens set by an earlier message_delta. The SDK's MessageDeltaUsage uses + // non-pointer int64 fields that default to 0 when absent, so the parser must + // use presence (Valid()) rather than a bare value check — otherwise the + // zero default would overwrite a previously set non-zero count. + parser := newAnthropicStreamParser("test-model") + + sseStream := `event: message_start +data: {"type":"message_start","message":{"id":"msg_test","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"output_tokens":0}}} + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}} + +event: content_block_stop +data: {"type":"content_block_stop","index":0} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":16,"output_tokens_details":{"thinking_tokens":4}}} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null}} + +event: message_stop +data: {"type":"message_stop"} +` + + _, _, tokenUsage, _, err := parser.Process(strings.NewReader(sseStream), true, nil) + require.NoError(t, err) + + inputTokens, inputSet := tokenUsage.InputTokens() + outputTokens, outputSet := tokenUsage.OutputTokens() + reasoningTokens, reasoningSet := tokenUsage.ReasoningTokens() + + assert.True(t, inputSet, "input tokens should be set") + assert.Equal(t, uint32(10), inputTokens, "input tokens should be from message_start") + // The first message_delta sets output_tokens=16; the second message_delta has + // no usage field and must not zero it out. + assert.True(t, outputSet, "output tokens should be set from the first message_delta") + assert.Equal(t, uint32(16), outputTokens, "later no-usage message_delta must not zero out output tokens") + assert.True(t, reasoningSet, "reasoning tokens should be set from the first message_delta") + assert.Equal(t, uint32(4), reasoningTokens, "later no-usage message_delta must not zero out reasoning tokens") +} + +func TestAnthropicStreamParserTokenUsage_MessageDeltaCacheWhenInputAlreadyHasCache(t *testing.T) { + // Test the case where message_start has cache tokens and input_tokens, + // and message_delta provides cache tokens but NOT input_tokens. + // The code must subtract the existing cache tokens from the base input_tokens + // before adding the new cache tokens from message_delta. + parser := newAnthropicStreamParser("test-model") + + sseStream := `event: message_start +data: {"type":"message_start","message":{"id":"msg_test","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":20,"cache_read_input_tokens":5,"cache_creation_input_tokens":3,"output_tokens":0}}} + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}} + +event: content_block_stop +data: {"type":"content_block_stop","index":0} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"cache_read_input_tokens":7,"cache_creation_input_tokens":2}} + +event: message_stop +data: {"type":"message_stop"} +` + + _, _, tokenUsage, _, err := parser.Process(strings.NewReader(sseStream), true, nil) + require.NoError(t, err) + + inputTokens, inputSet := tokenUsage.InputTokens() + cachedTokens, cachedSet := tokenUsage.CachedInputTokens() + cacheCreationTokens, cacheCreationSet := tokenUsage.CacheCreationInputTokens() + + assert.True(t, inputSet, "input tokens should be set") + // message_start: inputTokens is set to 20+5+3=28 (total) + // message_delta without input_tokens field: + // - baseInputTokens = 28 (total) - 5 (old cache_read) - 3 (old cache_creation) = 20 (base) + // - Then add new cache: 20 + 7 (new cache_read) + 2 (new cache_creation) = 29 + assert.Equal(t, uint32(29), inputTokens, "input tokens should be 29 (20 base + 7 cache_read + 2 cache_creation)") + assert.True(t, cachedSet, "cached tokens should be set") + assert.Equal(t, uint32(7), cachedTokens, "cached tokens should be from message_delta (7)") + assert.True(t, cacheCreationSet, "cache creation tokens should be set") + assert.Equal(t, uint32(2), cacheCreationTokens, "cache creation tokens should be from message_delta (2)") +} + +func TestAnthropicStreamParserTokenUsage_MessageDeltaInvalidJSON(t *testing.T) { + // Test that message_delta with invalid JSON in usage fields returns an error + parser := newAnthropicStreamParser("test-model") + + sseStream := `event: message_start +data: {"type":"message_start","message":{"id":"msg_test","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0}}} + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}} + +event: content_block_stop +data: {"type":"content_block_stop","index":0} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":"invalid"}} + +event: message_stop +data: {"type":"message_stop"} +` + + _, _, _, _, err := parser.Process(strings.NewReader(sseStream), true, nil) + // Should return error due to invalid JSON in usage field + require.Error(t, err, "should return error for invalid JSON in message_delta usage") + assert.Contains(t, err.Error(), "unmarshal message_delta usage fields", "error message should mention message_delta usage unmarshal") +} + func TestEffortAvailable(t *testing.T) { tests := []struct { name string From 343fc5c513e16c7e985e9e1b2a3745478579fbc6 Mon Sep 17 00:00:00 2001 From: hustxiayang Date: Thu, 16 Jul 2026 12:45:22 -0400 Subject: [PATCH 53/59] fix: sync claude model list for strucutured output and effort capability (#2382) **Description** It should take a bit time to finalize the discussion and implementation of https://github.com/envoyproxy/ai-gateway/issues/2368, and it needs users to config on their side to adapt to this new feature. However, some users already used the newer claude models, so just update the claude models list for a temporal fix to unblock them. Signed-off-by: yxia216 --- internal/translator/anthropic_helper.go | 26 ++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/internal/translator/anthropic_helper.go b/internal/translator/anthropic_helper.go index 54d2a9074c..e8efea3450 100644 --- a/internal/translator/anthropic_helper.go +++ b/internal/translator/anthropic_helper.go @@ -616,14 +616,21 @@ func modelContainsAny(model internalapi.RequestModel, identifiers []string) bool } // outputConfigModels lists model identifiers that support structured outputs (OutputConfig). -// Structured outputs are available on Claude Opus 4.6, Claude Sonnet 4.6, Claude Sonnet 4.5, Claude Opus 4.5, and Claude Haiku 4.5. +// Structured outputs are available on Claude Fable 5, Claude Mythos 5, Claude Opus 4.8, Claude Mythos Preview, +// Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, Claude Sonnet 4.6, Claude Sonnet 4.5, Claude Opus 4.5, and Claude Haiku 4.5. // See: https://platform.claude.com/docs/en/build-with-claude/structured-outputs var outputConfigModels = []string{ - "opus-4-5", // Claude Opus 4.5 - "sonnet-4-5", // Claude Sonnet 4.5 - "haiku-4-5", // Claude Haiku 4.5 - "opus-4-6", // Claude Opus 4.6 - "sonnet-4-6", // Claude Sonnet 4.6 + "opus-4-5", // Claude Opus 4.5 + "sonnet-4-5", // Claude Sonnet 4.5 + "haiku-4-5", // Claude Haiku 4.5 + "opus-4-6", // Claude Opus 4.6 + "sonnet-4-6", // Claude Sonnet 4.6 + "opus-4-7", // Claude Opus 4.7 + "opus-4-8", // Claude Opus 4.8 + "sonnet-5", // Claude Sonnet 5 + "fable-5", // Claude Fable 5 + "mythos-5", // Claude Mythos 5 + "mythos-preview", // Claude Mythos Preview } func outputConfigAvailable(model internalapi.RequestModel) bool { @@ -631,13 +638,18 @@ func outputConfigAvailable(model internalapi.RequestModel) bool { } // effortModels lists model identifiers that support the output_config.effort parameter. -// The effort parameter is supported by Claude Mythos Preview, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 4.6, and Claude Opus 4.5. +// The effort parameter is supported by Claude Fable 5, Claude Mythos 5, Claude Opus 4.8, Claude Mythos Preview, +// Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, Claude Sonnet 4.6, and Claude Opus 4.5. // See: https://platform.claude.com/docs/en/build-with-claude/effort var effortModels = []string{ "opus-4-5", // Claude Opus 4.5 "opus-4-6", // Claude Opus 4.6 "opus-4-7", // Claude Opus 4.7 + "opus-4-8", // Claude Opus 4.8 "sonnet-4-6", // Claude Sonnet 4.6 + "sonnet-5", // Claude Sonnet 5 + "fable-5", // Claude Fable 5 + "mythos-5", // Claude Mythos 5 "mythos-preview", // Claude Mythos Preview } From fe3afed720099c72edf34294149b7542adc0ccc1 Mon Sep 17 00:00:00 2001 From: John Panos Date: Tue, 14 Jul 2026 11:23:59 -0700 Subject: [PATCH 54/59] fix(translator): preserve tool result cache control (#2358) **Description** This commit preserves message-level `cache_control` fields on OpenAI tool result messages when translating requests to Anthropic-compatible backends. Previously, the OpenAI schema dropped the field before translation, so AWS Anthropic requests could not place a cache breakpoint on an outer `tool_result` block. The translator now prefers the message-level marker and falls back to the existing content-part marker. Unit tests cover string and multipart results, consecutive tool messages, and the compatibility path for content-part markers. **Related Issues/PRs (if applicable)** N/A **Special notes for reviewers (if applicable)** I raised this PR because prompt caching worked for plain messages, tool definitions, and `tool_use` blocks, but missed when the breakpoint landed on a tool result through the OpenAI-compatible endpoint. The native Anthropic endpoint already preserved this shape. The E2E matrix checks all five prompt shapes against the in-cluster test upstream and verifies the exact AWS Anthropic request body. It does not call a live provider. I used Cursor to help investigate and implement this change, and I reviewed and verified the code and tests. Signed-off-by: John Panos --- internal/apischema/openai/openai.go | 2 + .../apischema/openai/vendor_fields_test.go | 19 +++ internal/translator/anthropic_helper.go | 7 +- internal/translator/anthropic_helper_test.go | 100 ++++++++++++ tests/e2e/testdata/testupstream.yaml | 21 +++ tests/e2e/testupstream_test.go | 153 ++++++++++++++++++ 6 files changed, 301 insertions(+), 1 deletion(-) diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go index 8c8eb86ffd..fad1357668 100644 --- a/internal/apischema/openai/openai.go +++ b/internal/apischema/openai/openai.go @@ -587,6 +587,8 @@ type ChatCompletionToolMessageParam struct { Role string `json:"role"` // Tool call that this message is responding to. ToolCallID string `json:"tool_call_id"` + // Anthropic-compatible cache breakpoint for the translated tool_result block. + *AnthropicContentFields `json:",inline,omitempty"` } // ChatCompletionAssistantMessageParamAudio Data about a previous audio response from the model. diff --git a/internal/apischema/openai/vendor_fields_test.go b/internal/apischema/openai/vendor_fields_test.go index b7dc726deb..5281d8d3b5 100644 --- a/internal/apischema/openai/vendor_fields_test.go +++ b/internal/apischema/openai/vendor_fields_test.go @@ -212,3 +212,22 @@ func TestChatCompletionRequest_VendorFieldsExtraction(t *testing.T) { }) } } + +func TestChatCompletionToolMessageParam_CacheControl(t *testing.T) { + // Message-level cache_control on OpenAI tool results must survive decoding so + // the Anthropic translator can place it on the outer tool_result block. + raw := []byte(`{ + "role": "tool", + "tool_call_id": "toolu_1", + "content": "search results", + "cache_control": {"type": "ephemeral"} + }`) + + var msg ChatCompletionMessageParamUnion + require.NoError(t, json.Unmarshal(raw, &msg)) + require.NotNil(t, msg.OfTool) + require.NotNil(t, msg.OfTool.AnthropicContentFields) + require.Equal(t, anthropic.CacheControlEphemeralParam{Type: "ephemeral"}, msg.OfTool.CacheControl) + require.Equal(t, "toolu_1", msg.OfTool.ToolCallID) + require.Equal(t, "search results", msg.OfTool.Content.Value) +} diff --git a/internal/translator/anthropic_helper.go b/internal/translator/anthropic_helper.go index e8efea3450..759f29f3cf 100644 --- a/internal/translator/anthropic_helper.go +++ b/internal/translator/anthropic_helper.go @@ -553,7 +553,12 @@ func openAIToAnthropicMessages(openAIMsgs []openai.ChatCompletionMessageParamUni IsError: anthropic.Bool(isError), } - if cacheControl != nil { + // Prefer message-level cache_control so string tool results can + // carry a breakpoint; fall back to content-part markers. + switch { + case isCacheEnabled(toolMsg.AnthropicContentFields): + toolResultBlock.CacheControl = toolMsg.CacheControl + case cacheControl != nil: toolResultBlock.CacheControl = *cacheControl } diff --git a/internal/translator/anthropic_helper_test.go b/internal/translator/anthropic_helper_test.go index fcd61e8d37..0b54e07622 100644 --- a/internal/translator/anthropic_helper_test.go +++ b/internal/translator/anthropic_helper_test.go @@ -1642,3 +1642,103 @@ func splitSSEEvents(data string) []string { } return events } + +func TestOpenAIToAnthropicMessages_ToolResultCacheControl(t *testing.T) { + ephemeral := anthropic.CacheControlEphemeralParam{Type: constant.ValueOf[constant.Ephemeral]()} + + t.Run("string content with message-level cache_control", func(t *testing.T) { + msgs, system, err := openAIToAnthropicMessages([]openai.ChatCompletionMessageParamUnion{ + {OfTool: &openai.ChatCompletionToolMessageParam{ + Role: openai.ChatMessageRoleTool, + ToolCallID: "toolu_1", + Content: openai.ContentUnion{Value: "search results"}, + AnthropicContentFields: &openai.AnthropicContentFields{ + CacheControl: ephemeral, + }, + }}, + }) + require.NoError(t, err) + require.Empty(t, system) + require.Len(t, msgs, 1) + require.Len(t, msgs[0].Content, 1) + require.NotNil(t, msgs[0].Content[0].OfToolResult) + require.Equal(t, "toolu_1", msgs[0].Content[0].OfToolResult.ToolUseID) + require.Equal(t, ephemeral, msgs[0].Content[0].OfToolResult.CacheControl) + }) + + t.Run("multipart content with message-level cache_control", func(t *testing.T) { + msgs, _, err := openAIToAnthropicMessages([]openai.ChatCompletionMessageParamUnion{ + {OfTool: &openai.ChatCompletionToolMessageParam{ + Role: openai.ChatMessageRoleTool, + ToolCallID: "toolu_2", + Content: openai.ContentUnion{Value: []openai.ChatCompletionContentPartTextParam{ + {Type: "text", Text: "part one"}, + {Type: "text", Text: "part two"}, + }}, + AnthropicContentFields: &openai.AnthropicContentFields{ + CacheControl: ephemeral, + }, + }}, + }) + require.NoError(t, err) + require.Len(t, msgs, 1) + require.NotNil(t, msgs[0].Content[0].OfToolResult) + require.Equal(t, ephemeral, msgs[0].Content[0].OfToolResult.CacheControl) + require.Len(t, msgs[0].Content[0].OfToolResult.Content, 2) + }) + + t.Run("consecutive tool results preserve per-message cache markers", func(t *testing.T) { + msgs, _, err := openAIToAnthropicMessages([]openai.ChatCompletionMessageParamUnion{ + {OfTool: &openai.ChatCompletionToolMessageParam{ + Role: openai.ChatMessageRoleTool, + ToolCallID: "toolu_a", + Content: openai.ContentUnion{Value: "first"}, + }}, + {OfTool: &openai.ChatCompletionToolMessageParam{ + Role: openai.ChatMessageRoleTool, + ToolCallID: "toolu_b", + Content: openai.ContentUnion{Value: "second"}, + AnthropicContentFields: &openai.AnthropicContentFields{ + CacheControl: ephemeral, + }, + }}, + }) + require.NoError(t, err) + require.Len(t, msgs, 1) + require.Len(t, msgs[0].Content, 2) + require.Equal(t, anthropic.CacheControlEphemeralParam{}, msgs[0].Content[0].OfToolResult.CacheControl) + require.Equal(t, ephemeral, msgs[0].Content[1].OfToolResult.CacheControl) + }) + + t.Run("unmarked tool result has no cache_control", func(t *testing.T) { + msgs, _, err := openAIToAnthropicMessages([]openai.ChatCompletionMessageParamUnion{ + {OfTool: &openai.ChatCompletionToolMessageParam{ + Role: openai.ChatMessageRoleTool, + ToolCallID: "toolu_plain", + Content: openai.ContentUnion{Value: "plain result"}, + }}, + }) + require.NoError(t, err) + require.Equal(t, anthropic.CacheControlEphemeralParam{}, msgs[0].Content[0].OfToolResult.CacheControl) + }) + + t.Run("content-part cache_control still applies when message-level is absent", func(t *testing.T) { + msgs, _, err := openAIToAnthropicMessages([]openai.ChatCompletionMessageParamUnion{ + {OfTool: &openai.ChatCompletionToolMessageParam{ + Role: openai.ChatMessageRoleTool, + ToolCallID: "toolu_part", + Content: openai.ContentUnion{Value: []openai.ChatCompletionContentPartTextParam{ + { + Type: "text", + Text: "cached via content part", + AnthropicContentFields: &openai.AnthropicContentFields{ + CacheControl: ephemeral, + }, + }, + }}, + }}, + }) + require.NoError(t, err) + require.Equal(t, ephemeral, msgs[0].Content[0].OfToolResult.CacheControl) + }) +} diff --git a/tests/e2e/testdata/testupstream.yaml b/tests/e2e/testdata/testupstream.yaml index 15e1efeed2..eb62b04daa 100644 --- a/tests/e2e/testdata/testupstream.yaml +++ b/tests/e2e/testdata/testupstream.yaml @@ -84,6 +84,13 @@ spec: - name: translation-testupstream-cool-model-backend - name: translation-testupstream-another-cool-model-backend weight: 0 + - matches: + - headers: + - type: Exact + name: x-ai-eg-model + value: anthropic.claude-cache-matrix + backendRefs: + - name: translation-testupstream-aws-anthropic-backend --- apiVersion: aigateway.envoyproxy.io/v1beta1 kind: AIServiceBackend @@ -115,6 +122,20 @@ spec: group: gateway.envoyproxy.io --- apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: AIServiceBackend +metadata: + name: translation-testupstream-aws-anthropic-backend + namespace: default +spec: + schema: + name: AWSAnthropic + version: bedrock-2023-05-31 + backendRef: + name: testupstream + kind: Backend + group: gateway.envoyproxy.io +--- +apiVersion: aigateway.envoyproxy.io/v1beta1 kind: BackendSecurityPolicy metadata: name: translation-testupstream-cool-model-backend-api-key diff --git a/tests/e2e/testupstream_test.go b/tests/e2e/testupstream_test.go index ca5fcd69cc..f87617e87c 100644 --- a/tests/e2e/testupstream_test.go +++ b/tests/e2e/testupstream_test.go @@ -310,3 +310,156 @@ func extractFilterConfigFromSecret(ctx context.Context, name string) (string, er } return string(decoded), nil } + +// TestPromptCacheTranslationMatrix proves OpenAI-shim cache_control markers survive +// translation onto the AWSAnthropic InvokeModel body for the known prompt shapes. +func TestPromptCacheTranslationMatrix(t *testing.T) { + const manifest = "testdata/testupstream.yaml" + require.NoError(t, e2elib.KubectlApplyManifest(t.Context(), manifest)) + t.Cleanup(func() { + _ = e2elib.KubectlDeleteManifest(context.Background(), manifest) + }) + + const egSelector = "gateway.envoyproxy.io/owning-gateway-name=translation-testupstream" + e2elib.RequireWaitForGatewayPodReady(t, egSelector) + + const ( + expPath = "/model/anthropic.claude-cache-matrix/invoke" + expHost = "testupstream.default.svc.cluster.local" + upstreamID = "primary" + ) + fakeResponseBody := `{"id":"msg_cache","type":"message","role":"assistant","stop_reason":"end_turn","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":10,"output_tokens":1}}` + + for _, tc := range []struct { + name string + requestBody string + expRequestBody string + }{ + { + name: "plain_only", + requestBody: `{ + "model":"anthropic.claude-cache-matrix", + "max_tokens":16, + "messages":[ + {"role":"user","content":[{"type":"text","text":"plain prefix","cache_control":{"type":"ephemeral"}}]} + ] +}`, + expRequestBody: `{"max_tokens":16,"messages":[{"content":[{"text":"plain prefix","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"anthropic_version":"bedrock-2023-05-31"}`, + }, + { + name: "tool_defs_only", + requestBody: `{ + "model":"anthropic.claude-cache-matrix", + "max_tokens":16, + "tools":[{ + "type":"function", + "function":{ + "name":"search_groups", + "description":"Search feedback groups", + "parameters":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}, + "cache_control":{"type":"ephemeral"} + } + }], + "messages":[{"role":"user","content":"use tools"}] +}`, + expRequestBody: `{"max_tokens":16,"messages":[{"content":[{"text":"use tools","type":"text"}],"role":"user"}],"tools":[{"input_schema":{"properties":{"query":{"type":"string"}},"required":["query"],"type":"object"},"name":"search_groups","description":"Search feedback groups","cache_control":{"type":"ephemeral"}}],"anthropic_version":"bedrock-2023-05-31"}`, + }, + { + name: "tool_messages_bp_on_result", + requestBody: `{ + "model":"anthropic.claude-cache-matrix", + "max_tokens":16, + "tools":[{ + "type":"function", + "function":{ + "name":"search_groups", + "description":"Search feedback groups", + "parameters":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]} + } + }], + "messages":[ + {"role":"user","content":"search notifications"}, + {"role":"assistant","tool_calls":[{"id":"toolu_1","type":"function","function":{"name":"search_groups","arguments":"{\"query\":\"notifications\"}"}}]}, + {"role":"tool","tool_call_id":"toolu_1","content":"group results","cache_control":{"type":"ephemeral"}} + ] +}`, + expRequestBody: `{"max_tokens":16,"messages":[{"content":[{"text":"search notifications","type":"text"}],"role":"user"},{"content":[{"id":"toolu_1","input":{"query":"notifications"},"name":"search_groups","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_1","is_error":false,"cache_control":{"type":"ephemeral"},"content":[{"text":"group results","type":"text"}],"type":"tool_result"}],"role":"user"}],"tools":[{"input_schema":{"properties":{"query":{"type":"string"}},"required":["query"],"type":"object"},"name":"search_groups","description":"Search feedback groups"}],"anthropic_version":"bedrock-2023-05-31"}`, + }, + { + name: "tool_messages_bp_on_tool_use", + requestBody: `{ + "model":"anthropic.claude-cache-matrix", + "max_tokens":16, + "tools":[{ + "type":"function", + "function":{ + "name":"search_groups", + "description":"Search feedback groups", + "parameters":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]} + } + }], + "messages":[ + {"role":"user","content":"search notifications"}, + {"role":"assistant","tool_calls":[{"id":"toolu_1","type":"function","function":{"name":"search_groups","arguments":"{\"query\":\"notifications\"}"},"cache_control":{"type":"ephemeral"}}]}, + {"role":"tool","tool_call_id":"toolu_1","content":"group results"} + ] +}`, + expRequestBody: `{"max_tokens":16,"messages":[{"content":[{"text":"search notifications","type":"text"}],"role":"user"},{"content":[{"id":"toolu_1","input":{"query":"notifications"},"name":"search_groups","cache_control":{"type":"ephemeral"},"type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_1","is_error":false,"content":[{"text":"group results","type":"text"}],"type":"tool_result"}],"role":"user"}],"tools":[{"input_schema":{"properties":{"query":{"type":"string"}},"required":["query"],"type":"object"},"name":"search_groups","description":"Search feedback groups"}],"anthropic_version":"bedrock-2023-05-31"}`, + }, + { + name: "tool_messages_bp_on_plain", + requestBody: `{ + "model":"anthropic.claude-cache-matrix", + "max_tokens":16, + "tools":[{ + "type":"function", + "function":{ + "name":"search_groups", + "description":"Search feedback groups", + "parameters":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]} + } + }], + "messages":[ + {"role":"user","content":"search notifications"}, + {"role":"assistant","tool_calls":[{"id":"toolu_1","type":"function","function":{"name":"search_groups","arguments":"{\"query\":\"notifications\"}"}}]}, + {"role":"tool","tool_call_id":"toolu_1","content":"group results"}, + {"role":"user","content":[{"type":"text","text":"summarize","cache_control":{"type":"ephemeral"}}]} + ] +}`, + expRequestBody: `{"max_tokens":16,"messages":[{"content":[{"text":"search notifications","type":"text"}],"role":"user"},{"content":[{"id":"toolu_1","input":{"query":"notifications"},"name":"search_groups","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_1","is_error":false,"content":[{"text":"group results","type":"text"}],"type":"tool_result"}],"role":"user"},{"content":[{"text":"summarize","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"tools":[{"input_schema":{"properties":{"query":{"type":"string"}},"required":["query"],"type":"object"},"name":"search_groups","description":"Search feedback groups"}],"anthropic_version":"bedrock-2023-05-31"}`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Eventually(t, func() bool { + fwd := e2elib.RequireNewHTTPPortForwarder(t, e2elib.EnvoyGatewayNamespace, egSelector, e2elib.EnvoyGatewayDefaultServicePort) + defer fwd.Kill() + + req, err := http.NewRequest(http.MethodPost, fwd.Address()+"/v1/chat/completions", strings.NewReader(tc.requestBody)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set(testupstreamlib.ResponseBodyHeaderKey, base64.StdEncoding.EncodeToString([]byte(fakeResponseBody))) + req.Header.Set(testupstreamlib.ExpectedPathHeaderKey, base64.StdEncoding.EncodeToString([]byte(expPath))) + req.Header.Set(testupstreamlib.ExpectedHostKey, expHost) + req.Header.Set(testupstreamlib.ExpectedTestUpstreamIDKey, upstreamID) + req.Header.Set(testupstreamlib.ExpectedRequestBodyHeaderKey, base64.StdEncoding.EncodeToString([]byte(tc.expRequestBody))) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Logf("error: %v", err) + return false + } + defer func() { _ = resp.Body.Close() }() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Logf("error reading response body: %v", err) + return false + } + if resp.StatusCode != http.StatusOK { + t.Logf("unexpected status code: %d, body: %s", resp.StatusCode, body) + return false + } + return true + }, 20*time.Second, 1*time.Second) + }) + } +} From 8ab2c830eba06fa01e46809ddbbb01c5a0ecb15c Mon Sep 17 00:00:00 2001 From: Prajjwal Chittori Date: Mon, 13 Jul 2026 04:49:56 -0500 Subject: [PATCH 55/59] fix(translator): merge input/cache token usage reported on Anthropic message_delta (#2292) **Description** Anthropic Messages streaming usage extraction reads input and cache token counts from `message_start`, and on `message_delta` it only updates `output_tokens`. Some Anthropic-compatable backends report the final `input_tokens`, `cache_creation_input_tokens` and `cache_read_input_tokens` on the `message_delta` event instead. In that case the input and cache counts get dropped and recorded as zero, which corrupts usage metrics and any downstream cost or quota accounting that depends on them. This runs the `message_delta` usage through the same `ExtractTokenUsageFromExplicitCaching` path already used for `message_start` and merges it with `Override`. The fields are only merged when non-zero, so a standard delta that carries `output_tokens` alone doesnt overwrite the `message_start` baseline with zeros. **Related Issues/PRs (if applicable)** Fixes #2290 **Special notes for reviewers (if applicable)** Added two streaming subtests (cache-creation and cache-read variants) using the values from the issue: `input_tokens` 4522 + cache 4511 gives 9033 input total and 9038 total. They fail on main and pass with this change. The existing streaming and non-streaming usage tests are untouched. --------- Signed-off-by: pjdurden Co-authored-by: Ignasi Barrera --- internal/translator/anthropic_anthropic.go | 37 +++++++++- .../translator/anthropic_anthropic_test.go | 69 +++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/internal/translator/anthropic_anthropic.go b/internal/translator/anthropic_anthropic.go index 345b120aca..7bae252bb7 100644 --- a/internal/translator/anthropic_anthropic.go +++ b/internal/translator/anthropic_anthropic.go @@ -178,11 +178,44 @@ func (a *anthropicToAnthropicTranslator) reflectStreamingEvent(eventUnion *anthr } case eventUnion.MessageDelta != nil: u := eventUnion.MessageDelta.Usage - // message_delta events provide final counts for specific token types - // Update output tokens from message_delta (final count) + // message_delta carries the final counts. Standard Anthropic only reports output_tokens + // here, but some Anthropic-compatible backends report the final input/cache counts on + // message_delta instead of message_start. See https://github.com/envoyproxy/ai-gateway/issues/2290. + // + // output_tokens is always the final value on message_delta, so take it unconditionally. if u.OutputTokens >= 0 { a.streamingTokenUsage.SetOutputTokens(uint32(u.OutputTokens)) //nolint:gosec } + // Merge the input/cache counts per field rather than replacing the whole usage snapshot: + // a delta may report only the fields that apply (the rest arrive as zero), so overwriting + // every field would clobber values already set on message_start. We can only treat a field + // as "present" when it is non-zero, since the upstream usage fields are not pointers. + if u.InputTokens > 0 || u.CacheReadInputTokens > 0 || u.CacheCreationInputTokens > 0 { + // The unified input_tokens is the sum of raw input + cache-read + cache-creation, so + // recover the latest known value of each component and overwrite only the ones present + // on this delta before recomputing the sum. + cacheRead, _ := a.streamingTokenUsage.CachedInputTokens() + cacheCreation, _ := a.streamingTokenUsage.CacheCreationInputTokens() + unifiedInput, _ := a.streamingTokenUsage.InputTokens() + var rawInput uint32 + if unifiedInput >= cacheRead+cacheCreation { + rawInput = unifiedInput - cacheRead - cacheCreation + } + + if u.InputTokens > 0 { + rawInput = uint32(u.InputTokens) //nolint:gosec + } + if u.CacheReadInputTokens > 0 { + cacheRead = uint32(u.CacheReadInputTokens) //nolint:gosec + } + if u.CacheCreationInputTokens > 0 { + cacheCreation = uint32(u.CacheCreationInputTokens) //nolint:gosec + } + + a.streamingTokenUsage.SetCachedInputTokens(cacheRead) + a.streamingTokenUsage.SetCacheCreationInputTokens(cacheCreation) + a.streamingTokenUsage.SetInputTokens(rawInput + cacheRead + cacheCreation) + } } } diff --git a/internal/translator/anthropic_anthropic_test.go b/internal/translator/anthropic_anthropic_test.go index ae0a922b92..ac94be08ec 100644 --- a/internal/translator/anthropic_anthropic_test.go +++ b/internal/translator/anthropic_anthropic_test.go @@ -195,6 +195,75 @@ data: {"type":"message_stop" }` require.Equal(t, "claude-sonnet-4-5-20250929", responseModel) } +func TestAnthropicToAnthropic_ResponseBody_streaming_usageOnMessageDelta(t *testing.T) { + // Some Anthropic-compatible streaming backends report the final input/cache usage only on the + // message_delta event rather than message_start. The translator must merge those fields instead + // of dropping everything but output_tokens. See https://github.com/envoyproxy/ai-gateway/issues/2290. + t.Run("cache creation tokens", func(t *testing.T) { + translator := NewAnthropicToAnthropicTranslator("", "") + require.NotNil(t, translator) + translator.(*anthropicToAnthropicTranslator).stream = true + + const responseBody = `event: message_start +data: {"type":"message_start","message":{"model":"claude-sonnet-4-5-20250929","id":"msg_x","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0}}} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":4522,"output_tokens":5,"cache_creation_input_tokens":4511}} + +event: message_stop +data: {"type":"message_stop"}` + + _, _, tokenUsage, _, err := translator.ResponseBody(nil, strings.NewReader(responseBody), false, nil) + require.NoError(t, err) + // Total input = input_tokens(4522) + cache_creation_input_tokens(4511) = 9033; total = 9033 + 5 = 9038. + require.Equal(t, tokenUsageFrom(9033, 0, 4511, 5, 9038, -1), tokenUsage) + }) + + t.Run("cache read tokens", func(t *testing.T) { + translator := NewAnthropicToAnthropicTranslator("", "") + require.NotNil(t, translator) + translator.(*anthropicToAnthropicTranslator).stream = true + + const responseBody = `event: message_start +data: {"type":"message_start","message":{"model":"claude-sonnet-4-5-20250929","id":"msg_x","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0}}} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":4522,"output_tokens":5,"cache_read_input_tokens":4511}} + +event: message_stop +data: {"type":"message_stop"}` + + _, _, tokenUsage, _, err := translator.ResponseBody(nil, strings.NewReader(responseBody), false, nil) + require.NoError(t, err) + // Total input = input_tokens(4522) + cache_read_input_tokens(4511) = 9033; total = 9033 + 5 = 9038. + require.Equal(t, tokenUsageFrom(9033, 4511, 0, 5, 9038, -1), tokenUsage) + }) + + t.Run("partial fields on message_delta do not clobber message_start", func(t *testing.T) { + // A backend may set input_tokens on message_start and only report the final cache_read on + // message_delta. Merging must be per field: the delta omits input_tokens (reported as 0), + // which must not zero out the value already recorded from message_start. + translator := NewAnthropicToAnthropicTranslator("", "") + require.NotNil(t, translator) + translator.(*anthropicToAnthropicTranslator).stream = true + + const responseBody = `event: message_start +data: {"type":"message_start","message":{"model":"claude-sonnet-4-5-20250929","id":"msg_x","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0}}} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10,"cache_read_input_tokens":500}} + +event: message_stop +data: {"type":"message_stop"}` + + _, _, tokenUsage, _, err := translator.ResponseBody(nil, strings.NewReader(responseBody), false, nil) + require.NoError(t, err) + // message_start raw input(1000) is preserved; delta adds cache_read(500). + // Total input = 1000 + 500 = 1500; total = 1500 + 10 = 1510. + require.Equal(t, tokenUsageFrom(1500, 500, 0, 10, 1510, -1), tokenUsage) + }) +} + func TestAnthropicToAnthropic_ResponseError(t *testing.T) { t.Run("json error", func(t *testing.T) { translator := NewAnthropicToAnthropicTranslator("", "") From 902d91a28407c6045b28749be519c78eb7c5ae4c Mon Sep 17 00:00:00 2001 From: Yang Haoran <97868579@qq.com> Date: Thu, 16 Jul 2026 00:09:41 +0800 Subject: [PATCH 56/59] fix(extensionserver): handle per-backend route clusters (#2348) **Description** This handles both route-level cluster names and per-backend cluster names generated by Envoy Gateway. Per-backend clusters now receive the upstream ext_proc and header mutation filters together with metadata for the specific backend reference, so priority-based fallback can apply the fallback backend's schema translation. The cluster-name parsing is isolated in a helper that validates the route rule and backend indexes. Existing route-level cluster behavior is preserved. **Related Issues/PRs (if applicable)** Fixes #2135 **Testing** - `go test ./internal/extensionserver -run 'TestParseAIGatewayClusterName|TestMaybeModifyClusterPerBackendClusterName|Test_maybeModifyCluster' -count=1` - `go test ./internal/extensionserver -count=1` - `go test ./internal/extensionserver -coverprofile= -count=1` (package 88.3%; parser and endpoint metadata helper 100%) - `go vet ./internal/extensionserver` - `git diff --check` The repository's `go tool -modfile=tools/go.mod` lint command is not compatible with the local Go 1.26 tool invocation, so the extension server lint target was not run. **Special notes for reviewers (if applicable)** The regression tests cover both per-backend LoadAssignment metadata and the EDS-managed cluster-level metadata fallback. This update was prepared with OpenAI Codex assistance and reviewed against the current extension server implementation. --------- Signed-off-by: Yang Haoran <97868579@qq.com> Co-authored-by: Ignasi Barrera --- .../extensionserver/extensionserver_test.go | 95 +++++++++++- .../extensionserver/post_translate_modify.go | 139 +++++++++++++----- 2 files changed, 193 insertions(+), 41 deletions(-) diff --git a/internal/extensionserver/extensionserver_test.go b/internal/extensionserver/extensionserver_test.go index af4115655a..4074762843 100644 --- a/internal/extensionserver/extensionserver_test.go +++ b/internal/extensionserver/extensionserver_test.go @@ -143,7 +143,7 @@ func Test_maybeModifyCluster(t *testing.T) { {c: &clusterv3.Cluster{}, errLog: "non-ai-gateway cluster name"}, {c: &clusterv3.Cluster{ Name: "httproute/ns/name/rule/invalid", - }, errLog: "failed to parse HTTPRoute rule index"}, + }, errLog: "invalid HTTPRoute rule index"}, {c: &clusterv3.Cluster{ Name: "httproute/ns/myroute/rule/99999", }, errLog: `HTTPRoute rule index out of range`}, @@ -417,6 +417,99 @@ func Test_maybeModifyCluster(t *testing.T) { } } +func TestParseAIGatewayClusterName(t *testing.T) { + for _, tc := range []struct { + name string + cluster string + expected aiGatewayClusterName + wantErr bool + }{ + { + name: "route-level cluster", + cluster: "httproute/ns/route/rule/3", + expected: aiGatewayClusterName{ + namespace: "ns", + routeName: "route", + ruleIndex: 3, + backendRefIndex: noBackendRefIndex, + }, + }, + { + name: "per-backend cluster", + cluster: "httproute/ns/route/rule/3/backend/2", + expected: aiGatewayClusterName{ + namespace: "ns", + routeName: "route", + ruleIndex: 3, + backendRefIndex: 2, + }, + }, + {name: "missing backend marker", cluster: "httproute/ns/route/rule/3/not-backend/2", wantErr: true}, + {name: "negative backend index", cluster: "httproute/ns/route/rule/3/backend/-1", wantErr: true}, + {name: "invalid rule index", cluster: "httproute/ns/route/rule/nope", wantErr: true}, + {name: "non route cluster", cluster: "service/ns/route", wantErr: true}, + } { + t.Run(tc.name, func(t *testing.T) { + actual, err := parseAIGatewayClusterName(tc.cluster) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + }) + } +} + +func TestMaybeModifyClusterPerBackendClusterName(t *testing.T) { + newServer := func(t *testing.T) *Server { + t.Helper() + c := newFakeClient() + require.NoError(t, c.Create(t.Context(), &aigv1b1.AIGatewayRoute{ + ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "ns"}, + Spec: aigv1b1.AIGatewayRouteSpec{Rules: []aigv1b1.AIGatewayRouteRule{{ + BackendRefs: []aigv1b1.AIGatewayRouteRuleBackendRef{ + {Name: "primary", Priority: ptr.To[uint32](0)}, + {Name: "fallback", Priority: ptr.To[uint32](1)}, + }, + }}}, + })) + s, err := New(c, logr.Discard(), udsPath, false, nil, nil, "envoy-ai-gateway-ratelimit.envoy-gateway-system", 5, false) + require.NoError(t, err) + return s + } + + assertBackendName := func(t *testing.T, metadata *corev3.Metadata, expected string) { + t.Helper() + require.NotNil(t, metadata) + filterMetadata := metadata.FilterMetadata[internalapi.InternalEndpointMetadataNamespace] + require.NotNil(t, filterMetadata) + require.Equal(t, expected, filterMetadata.Fields[internalapi.InternalMetadataBackendNameKey].GetStringValue()) + } + + t.Run("sets endpoint metadata for the selected backend", func(t *testing.T) { + cluster := &clusterv3.Cluster{ + Name: "httproute/ns/myroute/rule/0/backend/1", + LoadAssignment: &endpointv3.ClusterLoadAssignment{Endpoints: []*endpointv3.LocalityLbEndpoints{{ + LbEndpoints: []*endpointv3.LbEndpoint{{}}, + }}}, + } + require.NoError(t, newServer(t).maybeModifyCluster(t.Context(), cluster)) + require.Equal(t, uint32(1), cluster.LoadAssignment.Endpoints[0].Priority) + assertBackendName(t, cluster.LoadAssignment.Endpoints[0].LbEndpoints[0].Metadata, + internalapi.PerRouteRuleRefBackendName("ns", "fallback", "myroute", 0, 1)) + require.Contains(t, cluster.TypedExtensionProtocolOptions, "envoy.extensions.upstreams.http.v3.HttpProtocolOptions") + }) + + t.Run("sets cluster metadata for EDS-managed endpoints", func(t *testing.T) { + cluster := &clusterv3.Cluster{Name: "httproute/ns/myroute/rule/0/backend/0"} + require.NoError(t, newServer(t).maybeModifyCluster(t.Context(), cluster)) + assertBackendName(t, cluster.Metadata, + internalapi.PerRouteRuleRefBackendName("ns", "primary", "myroute", 0, 0)) + require.Contains(t, cluster.TypedExtensionProtocolOptions, "envoy.extensions.upstreams.http.v3.HttpProtocolOptions") + }) +} + // Helper function to create an InferencePool ExtensionResource. func createInferencePoolExtensionResource(name, namespace string) *egextension.ExtensionResource { unstructuredObj := &unstructured.Unstructured{ diff --git a/internal/extensionserver/post_translate_modify.go b/internal/extensionserver/post_translate_modify.go index 694ff060f0..ca0625be9d 100644 --- a/internal/extensionserver/post_translate_modify.go +++ b/internal/extensionserver/post_translate_modify.go @@ -41,8 +41,50 @@ import ( const ( extProcUDSClusterName = "ai-gateway-extproc-uds" aiGatewayExtProcName = "envoy.filters.http.ext_proc/aigateway" + noBackendRefIndex = -1 ) +type aiGatewayClusterName struct { + namespace string + routeName string + ruleIndex int + backendRefIndex int +} + +// parseAIGatewayClusterName parses the cluster names generated for AIGatewayRoute rules. +// Envoy Gateway uses a route-level cluster name unless it creates a cluster for each backend. +func parseAIGatewayClusterName(name string) (aiGatewayClusterName, error) { + parts := strings.Split(name, "/") + if (len(parts) != 5 && len(parts) != 7) || parts[0] != "httproute" || parts[3] != "rule" { + return aiGatewayClusterName{}, fmt.Errorf("invalid AIGatewayRoute cluster name %q", name) + } + + ruleIndex, err := strconv.Atoi(parts[4]) + if err != nil || ruleIndex < 0 { + return aiGatewayClusterName{}, fmt.Errorf("invalid HTTPRoute rule index %q", parts[4]) + } + + clusterName := aiGatewayClusterName{ + namespace: parts[1], + routeName: parts[2], + ruleIndex: ruleIndex, + backendRefIndex: noBackendRefIndex, + } + if len(parts) == 5 { + return clusterName, nil + } + if parts[5] != "backend" { + return aiGatewayClusterName{}, fmt.Errorf("invalid AIGatewayRoute backend cluster name %q", name) + } + + backendRefIndex, err := strconv.Atoi(parts[6]) + if err != nil || backendRefIndex < 0 { + return aiGatewayClusterName{}, fmt.Errorf("invalid HTTPRoute backend index %q", parts[6]) + } + clusterName.backendRefIndex = backendRefIndex + return clusterName, nil +} + // PostTranslateModify allows an extension to modify the clusters and secrets in the xDS config // after the initial translation is complete. This method is responsible for: // @@ -169,23 +211,14 @@ func (s *Server) PostTranslateModify(ctx context.Context, req *egextension.PostT // The resulting configuration is similar to the envoy.yaml files in tests/data-plane/. // Only clusters with names matching the AIGatewayRoute pattern are modified. func (s *Server) maybeModifyCluster(ctx context.Context, cluster *clusterv3.Cluster) error { - // Parse cluster name to extract AIGatewayRoute information. - // Expected format: "httproute///rule/". - parts := strings.Split(cluster.Name, "/") - if len(parts) != 5 || parts[0] != "httproute" { - // This is not an AIGatewayRoute-generated cluster, skip modification. - s.log.Info("non-ai-gateway cluster name", "cluster_name", cluster.Name) - return nil - } - httpRouteNamespace := parts[1] - httpRouteName := parts[2] - httpRouteRuleIndexStr := parts[4] - httpRouteRuleIndex, err := strconv.Atoi(httpRouteRuleIndexStr) + clusterName, err := parseAIGatewayClusterName(cluster.Name) if err != nil { - s.log.Error(err, "failed to parse HTTPRoute rule index", - "cluster_name", cluster.Name, "rule_index", httpRouteRuleIndexStr) + s.log.Info("non-ai-gateway cluster name", "cluster_name", cluster.Name, "error", err) return nil } + httpRouteNamespace := clusterName.namespace + httpRouteName := clusterName.routeName + httpRouteRuleIndex := clusterName.ruleIndex // Check if this rule has InferencePool backends. pool := getInferencePoolByMetadata(cluster.Metadata) @@ -206,23 +239,43 @@ func (s *Server) maybeModifyCluster(ctx context.Context, cluster *clusterv3.Clus // Get the backend from the HTTPRoute object. if httpRouteRuleIndex >= len(aigwRoute.Spec.Rules) { s.log.Info("HTTPRoute rule index out of range", - "cluster_name", cluster.Name, "rule_index", httpRouteRuleIndexStr) + "cluster_name", cluster.Name, "rule_index", httpRouteRuleIndex) return nil } httpRouteRule := &aigwRoute.Spec.Rules[httpRouteRuleIndex] + if clusterName.backendRefIndex != noBackendRefIndex && clusterName.backendRefIndex >= len(httpRouteRule.BackendRefs) { + s.log.Info("HTTPRoute backend index out of range", + "cluster_name", cluster.Name, "backend_index", clusterName.backendRefIndex) + return nil + } // Only process LoadAssignment for non-InferencePool backends. if pool == nil { - if cluster.LoadAssignment == nil { + switch { + case cluster.LoadAssignment == nil: // When LoadAssignment is nil (e.g. EDS-managed endpoints in standalone mode), // set backend name on cluster-level metadata so the upstream ext_proc filter // can resolve the backend via XDSClusterMetadataBackendNamePath fallback. s.log.Info("LoadAssignment is nil, setting cluster-level metadata", "cluster_name", cluster.Name) if len(httpRouteRule.BackendRefs) > 0 { - backendRef := httpRouteRule.BackendRefs[0] - setClusterMetadataBackendName(cluster, aigwRoute.Namespace, backendRef.Name, aigwRoute.Name, httpRouteRuleIndex, 0) + backendRefIndex := 0 + if clusterName.backendRefIndex != noBackendRefIndex { + backendRefIndex = clusterName.backendRefIndex + } + backendRef := httpRouteRule.BackendRefs[backendRefIndex] + setClusterMetadataBackendName(cluster, aigwRoute.Namespace, backendRef.Name, aigwRoute.Name, httpRouteRuleIndex, backendRefIndex) } - } else { + case clusterName.backendRefIndex != noBackendRefIndex: + backendRef := httpRouteRule.BackendRefs[clusterName.backendRefIndex] + for _, endpoints := range cluster.LoadAssignment.Endpoints { + if backendRef.Priority != nil { + endpoints.Priority = *backendRef.Priority + } + for _, endpoint := range endpoints.LbEndpoints { + setEndpointMetadataBackendName(endpoint, aigwRoute.Namespace, backendRef.Name, aigwRoute.Name, httpRouteRuleIndex, clusterName.backendRefIndex) + } + } + default: // Populate the metadata for each endpoint in the LoadAssignment. var lbEndpointIndex int for i, backendRef := range httpRouteRule.BackendRefs { @@ -238,33 +291,19 @@ func (s *Server) maybeModifyCluster(ctx context.Context, cluster *clusterv3.Clus if backendRef.Priority != nil { endpoints.Priority = *backendRef.Priority } - // We populate the same metadata for all endpoints in the LoadAssignment. - // This is because currently, an extproc cannot retrieve the endpoint set level metadata. for _, endpoint := range endpoints.LbEndpoints { - if endpoint.Metadata == nil { - endpoint.Metadata = &corev3.Metadata{} - } - if endpoint.Metadata.FilterMetadata == nil { - endpoint.Metadata.FilterMetadata = make(map[string]*structpb.Struct) - } - m, ok := endpoint.Metadata.FilterMetadata[internalapi.InternalEndpointMetadataNamespace] - if !ok { - m = &structpb.Struct{} - endpoint.Metadata.FilterMetadata[internalapi.InternalEndpointMetadataNamespace] = m - } - if m.Fields == nil { - m.Fields = make(map[string]*structpb.Value) - } - m.Fields[internalapi.InternalMetadataBackendNameKey] = structpb.NewStringValue( - internalapi.PerRouteRuleRefBackendName(namespace, name, aigwRoute.Name, httpRouteRuleIndex, i), - ) + setEndpointMetadataBackendName(endpoint, namespace, name, aigwRoute.Name, httpRouteRuleIndex, i) } } } } else { // we can only specify one backend in a rule for InferencePool. - backendRef := httpRouteRule.BackendRefs[0] - setClusterMetadataBackendName(cluster, aigwRoute.Namespace, backendRef.Name, aigwRoute.Name, httpRouteRuleIndex, 0) + backendRefIndex := 0 + if clusterName.backendRefIndex != noBackendRefIndex { + backendRefIndex = clusterName.backendRefIndex + } + backendRef := httpRouteRule.BackendRefs[backendRefIndex] + setClusterMetadataBackendName(cluster, aigwRoute.Namespace, backendRef.Name, aigwRoute.Name, httpRouteRuleIndex, backendRefIndex) } if cluster.TypedExtensionProtocolOptions == nil { @@ -843,6 +882,26 @@ func setClusterMetadataBackendName(cluster *clusterv3.Cluster, namespace, name, ) } +func setEndpointMetadataBackendName(endpoint *endpointv3.LbEndpoint, namespace, name, routeName string, routeRuleIndex, refIndex int) { + if endpoint.Metadata == nil { + endpoint.Metadata = &corev3.Metadata{} + } + if endpoint.Metadata.FilterMetadata == nil { + endpoint.Metadata.FilterMetadata = make(map[string]*structpb.Struct) + } + m, ok := endpoint.Metadata.FilterMetadata[internalapi.InternalEndpointMetadataNamespace] + if !ok { + m = &structpb.Struct{} + endpoint.Metadata.FilterMetadata[internalapi.InternalEndpointMetadataNamespace] = m + } + if m.Fields == nil { + m.Fields = make(map[string]*structpb.Value) + } + m.Fields[internalapi.InternalMetadataBackendNameKey] = structpb.NewStringValue( + internalapi.PerRouteRuleRefBackendName(namespace, name, routeName, routeRuleIndex, refIndex), + ) +} + func shouldAIGatewayExtProcBeInserted(filters []*httpconnectionmanagerv3.HttpFilter) bool { for _, f := range filters { if f.Name == aiGatewayExtProcName { From 0e54a5981bf76bb06e91fcf755247cd31b6ca203 Mon Sep 17 00:00:00 2001 From: hustxiayang Date: Thu, 2 Jul 2026 13:15:37 -0400 Subject: [PATCH 57/59] fix: add reasoning tokens into token metrics in stream mode for gemini models (#2259) **Description** The previous implementation on tokens tracking is like ``` if chunk.UsageMetadata.CandidatesTokenCount >= 0 { tokenUsage.SetOutputTokens(uint32(chunk.UsageMetadata.CandidatesTokenCount)) //nolint:gosec } ``` The output tokens should include both the reasoning tokens and the candidates tokens. Thus fix this by reusing the the variable `usage`, just like non-streaming mode. Also updated the tests to include reasoning tokens --------- Signed-off-by: yxia216 Co-authored-by: Ignasi Barrera --- internal/translator/openai_gcpvertexai.go | 20 +++++-------------- .../translator/openai_gcpvertexai_test.go | 12 +++++------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/internal/translator/openai_gcpvertexai.go b/internal/translator/openai_gcpvertexai.go index e4066e1ab2..aa39bfd3df 100644 --- a/internal/translator/openai_gcpvertexai.go +++ b/internal/translator/openai_gcpvertexai.go @@ -249,21 +249,11 @@ func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) handleStreamingResponse( span.RecordResponseChunk(usageChunk) } - if chunk.UsageMetadata.PromptTokenCount >= 0 { - tokenUsage.SetInputTokens(uint32(chunk.UsageMetadata.PromptTokenCount)) //nolint:gosec - } - if chunk.UsageMetadata.CandidatesTokenCount >= 0 { - tokenUsage.SetOutputTokens(uint32(chunk.UsageMetadata.CandidatesTokenCount)) //nolint:gosec - } - if chunk.UsageMetadata.TotalTokenCount >= 0 { - tokenUsage.SetTotalTokens(uint32(chunk.UsageMetadata.TotalTokenCount)) //nolint:gosec - } - if chunk.UsageMetadata.CachedContentTokenCount >= 0 { - tokenUsage.SetCachedInputTokens(uint32(chunk.UsageMetadata.CachedContentTokenCount)) //nolint:gosec - } - if chunk.UsageMetadata.ThoughtsTokenCount >= 0 { - tokenUsage.SetReasoningTokens(uint32(chunk.UsageMetadata.ThoughtsTokenCount)) //nolint:gosec - } + tokenUsage.SetInputTokens(uint32(usage.PromptTokens)) //nolint:gosec + tokenUsage.SetOutputTokens(uint32(usage.CompletionTokens)) //nolint:gosec + tokenUsage.SetTotalTokens(uint32(usage.TotalTokens)) //nolint:gosec + tokenUsage.SetCachedInputTokens(uint32(usage.PromptTokensDetails.CachedTokens)) //nolint:gosec + tokenUsage.SetReasoningTokens(uint32(usage.CompletionTokensDetails.ReasoningTokens)) //nolint:gosec } } diff --git a/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index d81fdca9f3..6d8295d78c 100644 --- a/internal/translator/openai_gcpvertexai_test.go +++ b/internal/translator/openai_gcpvertexai_test.go @@ -1230,7 +1230,7 @@ data: [DONE] }, body: `data: {"candidates":[{"content":{"parts":[{"text":"let me think step by step and reply you.", "thought": true}]}}]} -data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":3,"totalTokenCount":8}}`, +data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":3,"totalTokenCount":18,"thoughtsTokenCount":10}}`, stream: true, endOfStream: true, wantError: false, @@ -1239,11 +1239,11 @@ data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}],"usageMetadata":{ data: {"choices":[{"index":0,"delta":{"content":"Hello","role":"assistant"}}],"object":"chat.completion.chunk"} -data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8,"completion_tokens_details":{},"prompt_tokens_details":{}}} +data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":5,"completion_tokens":13,"total_tokens":18,"completion_tokens_details":{"reasoning_tokens":10},"prompt_tokens_details":{}}} data: [DONE] `), - wantTokenUsage: tokenUsageFrom(5, 0, -1, 3, 8, 0), // Does not support Cache Creation. + wantTokenUsage: tokenUsageFrom(5, 0, -1, 13, 18, 10), // Does not support Cache Creation. }, { name: "stream chunks with thought signature on text part", @@ -1252,7 +1252,7 @@ data: [DONE] }, body: `data: {"candidates":[{"content":{"parts":[{"text":"let me think about this.", "thought": true}]}}]} -data: {"candidates":[{"content":{"parts":[{"text":"The answer is 42.", "thoughtSignature": "dGVzdHNpZ25hdHVyZQ=="}]}}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":8,"totalTokenCount":18}}`, +data: {"candidates":[{"content":{"parts":[{"text":"The answer is 42.", "thoughtSignature": "dGVzdHNpZ25hdHVyZQ=="}]}}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":8,"totalTokenCount":33,"thoughtsTokenCount":15}}`, stream: true, endOfStream: true, wantError: false, @@ -1261,11 +1261,11 @@ data: {"candidates":[{"content":{"parts":[{"text":"The answer is 42.", "thoughtS data: {"choices":[{"index":0,"delta":{"content":"The answer is 42.","role":"assistant","reasoning_content":{"signature":"dGVzdHNpZ25hdHVyZQ=="}}}],"object":"chat.completion.chunk"} -data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":10,"completion_tokens":8,"total_tokens":18,"completion_tokens_details":{},"prompt_tokens_details":{}}} +data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":10,"completion_tokens":23,"total_tokens":33,"completion_tokens_details":{"reasoning_tokens":15},"prompt_tokens_details":{}}} data: [DONE] `), - wantTokenUsage: tokenUsageFrom(10, 0, -1, 8, 18, 0), + wantTokenUsage: tokenUsageFrom(10, 0, -1, 23, 33, 15), }, } From 93b438c31999cbc9feacf47ac7c8dfb9287acf48 Mon Sep 17 00:00:00 2001 From: Adam Nych Date: Thu, 23 Jul 2026 13:06:51 +0200 Subject: [PATCH 58/59] translator: accept thinking_blocks signatures and fall back to skip_thought_signature_validator for Gemini function calls (#2366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** Gemini 3.x requires the thought signature (see [1]) returned with a `functionCall` to be echoed back on that part in subsequent requests; otherwise Vertex rejects the request with `400 INVALID_ARGUMENT` ("Function call is missing a thought_signature in functionCall parts"). The OpenAI → GCPVertexAI translator emits the signature in responses as `message.thinking_blocks[].signature` (#1995) but on the request side only reads it from assistant `content` parts of type `"thinking"` (#1726), and sends nothing when absent. Standard OpenAI-schema clients have no signature field to echo, so every tool round-trip with Gemini 3.x fails. This PR changes `assistantMsgToGeminiParts` to: - also accept a signature echoed via the assistant message's `thinking_blocks[].signature` — the same shape the gateway itself emits, so round-tripping the response message now works. Content-part signatures keep priority. - fall back to Google's documented compatibility value `skip_thought_signature_validator` on the first `functionCall` part when the client echoed no signature at all (matching LiteLLM's behavior). Attached to the first part only, consistent with how Gemini attaches signatures for parallel calls. `ChatCompletionAssistantMessageParam` gains the `thinking_blocks` field to make the request/response contract symmetric. **Related Issues/PRs (if applicable)** Fixes #2365 Related PRs: #1726, #1995 **Special notes for reviewers (if applicable)** - Four pre-existing fixtures in `gemini_helper_test.go` / `TestOpenAIMessagesToGeminiContents` exercised tool calls without a signature and now expect the dummy value — that's the intended new behavior, not weakened assertions. - Verified end-to-end with `aigw run` against real Vertex (`gemini-3.5-flash`, `eu`): the signature-less round-trip returns 200 with this patch and 400 on `main`; `thinking_blocks` echo returns 200 and carries the real signature; streaming unaffected. Vertex accepts the dummy value (documented compatibility path, so no hard dependency on Google keeping undocumented behavior). 1: https://ai.google.dev/gemini-api/docs/thought-signatures --------- Signed-off-by: Adam Nych Co-authored-by: Claude Fable 5 --- internal/apischema/openai/openai.go | 4 + internal/translator/gemini_helper.go | 32 +++- internal/translator/gemini_helper_test.go | 177 +++++++++++++++++++++- 3 files changed, 204 insertions(+), 9 deletions(-) diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go index fad1357668..3623ab8c7f 100644 --- a/internal/apischema/openai/openai.go +++ b/internal/apischema/openai/openai.go @@ -641,6 +641,10 @@ type ChatCompletionAssistantMessageParam struct { Refusal string `json:"refusal,omitempty"` // The tool calls generated by the model, such as function calls. ToolCalls []ChatCompletionMessageToolCallParam `json:"tool_calls,omitempty"` + // ThinkingBlocks holds structured thinking metadata (signatures, redacted content) echoed back + // by clients from a previous response's thinking_blocks. Non-standard extension, see + // https://docs.litellm.ai/docs/reasoning_content for the convention. + ThinkingBlocks []ThinkingBlock `json:"thinking_blocks,omitempty"` } // ChatCompletionMessageToolCallType The type of the tool. Currently, only `function` is supported. diff --git a/internal/translator/gemini_helper.go b/internal/translator/gemini_helper.go index be32bd9a64..dfb9c7036c 100644 --- a/internal/translator/gemini_helper.go +++ b/internal/translator/gemini_helper.go @@ -32,6 +32,11 @@ const ( gcpMethodRawPredict = "rawPredict" ) +// dummyThoughtSignature is Google's documented compatibility escape for clients that cannot echo +// back a thought_signature on function-call parts in multi-turn requests, see +// https://ai.google.dev/gemini-api/docs/thought-signatures. +var dummyThoughtSignature = []byte("skip_thought_signature_validator") + // geminiResponseMode represents the type of response mode for Gemini requests type geminiResponseMode string @@ -274,6 +279,21 @@ func assistantMsgToGeminiParts(msg *openai.ChatCompletionAssistantMessageParam) } } + // Fall back to the first non-empty signature echoed back via thinking_blocks (see + // https://docs.litellm.ai/docs/reasoning_content) when no content-part signature was found. + if thoughtSignature == nil { + for _, block := range msg.ThinkingBlocks { + if block.Signature != "" { + sigBytes, err := base64.StdEncoding.DecodeString(block.Signature) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode thought signature: %w", err) + } + thoughtSignature = sigBytes + break // Only use first signature. + } + } + } + // Handle tool calls in the assistant message. knownToolCalls := make(map[string]string) for i, toolCall := range msg.ToolCalls { @@ -289,8 +309,16 @@ func assistantMsgToGeminiParts(msg *openai.ChatCompletionAssistantMessageParam) funcCallPart := genai.NewPartFromFunctionCall(toolCall.Function.Name, parsedArgs) // According to https://ai.google.dev/gemini-api/docs/thought-signatures, if the model generates parallel function calls in a response, the thought_signature is attached only to the first functionCall part. Subsequent functionCall parts in the same response will not contain a signature. - if i == 0 && thoughtSignature != nil { - funcCallPart.ThoughtSignature = thoughtSignature + if i == 0 { + if thoughtSignature != nil { + funcCallPart.ThoughtSignature = thoughtSignature + } else { + // No signature was echoed back by the client at all (e.g. an OpenAI-schema client + // that doesn't round-trip thinking_blocks). Gemini 3.x rejects function calls in + // multi-turn requests that are missing a thought_signature, so fall back to + // Google's documented compatibility escape. + funcCallPart.ThoughtSignature = dummyThoughtSignature + } } parts = append(parts, funcCallPart) diff --git a/internal/translator/gemini_helper_test.go b/internal/translator/gemini_helper_test.go index 94d05aa3ef..7843e605a8 100644 --- a/internal/translator/gemini_helper_test.go +++ b/internal/translator/gemini_helper_test.go @@ -94,6 +94,7 @@ func TestOpenAIMessagesToGeminiContents(t *testing.T) { "param1": "value1", }, }, + ThoughtSignature: dummyThoughtSignature, }, {Text: "This is a assistant message"}, }, @@ -249,6 +250,7 @@ func TestAssistantMsgToGeminiParts(t *testing.T) { Args: map[string]any{"location": "New York", "unit": "celsius"}, Name: "get_weather", }, + ThoughtSignature: dummyThoughtSignature, }, }, expectedToolCalls: map[string]string{ @@ -282,10 +284,14 @@ func TestAssistantMsgToGeminiParts(t *testing.T) { }, }, expectedParts: []*genai.Part{ - genai.NewPartFromFunctionCall("get_weather", map[string]any{ - "location": "New York", - "unit": "celsius", - }), + func() *genai.Part { + p := genai.NewPartFromFunctionCall("get_weather", map[string]any{ + "location": "New York", + "unit": "celsius", + }) + p.ThoughtSignature = dummyThoughtSignature + return p + }(), genai.NewPartFromFunctionCall("get_time", map[string]any{ "timezone": "EST", }), @@ -428,9 +434,13 @@ func TestAssistantMsgToGeminiParts(t *testing.T) { }, }, expectedParts: []*genai.Part{ - genai.NewPartFromFunctionCall("get_weather", map[string]any{ - "location": "San Francisco", - }), + func() *genai.Part { + p := genai.NewPartFromFunctionCall("get_weather", map[string]any{ + "location": "San Francisco", + }) + p.ThoughtSignature = dummyThoughtSignature + return p + }(), { Text: "I need to call a function to get the weather", Thought: true, @@ -520,6 +530,159 @@ func TestAssistantMsgToGeminiParts(t *testing.T) { "call_time": "get_time", }, }, + { + name: "thinking_blocks signature with tool calls - signature on first tool call", + msg: openai.ChatCompletionAssistantMessageParam{ + Role: openai.ChatMessageRoleAssistant, + ThinkingBlocks: []openai.ThinkingBlock{ + {Type: "thinking", Signature: "dGVzdHNpZ25hdHVyZQ=="}, // "testsignature" in base64 + }, + ToolCalls: []openai.ChatCompletionMessageToolCallParam{ + { + ID: ptr.To("call_weather"), + Function: openai.ChatCompletionMessageToolCallFunctionParam{ + Name: "get_weather", + Arguments: `{"location":"San Francisco"}`, + }, + Type: openai.ChatCompletionMessageToolCallTypeFunction, + }, + { + ID: ptr.To("call_time"), + Function: openai.ChatCompletionMessageToolCallFunctionParam{ + Name: "get_time", + Arguments: `{"timezone":"PST"}`, + }, + Type: openai.ChatCompletionMessageToolCallTypeFunction, + }, + }, + }, + expectedParts: []*genai.Part{ + { + FunctionCall: &genai.FunctionCall{ + Name: "get_weather", + Args: map[string]any{"location": "San Francisco"}, + }, + ThoughtSignature: []byte("testsignature"), + }, + { + FunctionCall: &genai.FunctionCall{ + Name: "get_time", + Args: map[string]any{"timezone": "PST"}, + }, + }, + }, + expectedToolCalls: map[string]string{ + "call_weather": "get_weather", + "call_time": "get_time", + }, + }, + { + name: "tool calls with no signature anywhere fall back to dummy signature on first call only", + msg: openai.ChatCompletionAssistantMessageParam{ + Role: openai.ChatMessageRoleAssistant, + ToolCalls: []openai.ChatCompletionMessageToolCallParam{ + { + ID: ptr.To("call_weather"), + Function: openai.ChatCompletionMessageToolCallFunctionParam{ + Name: "get_weather", + Arguments: `{"location":"San Francisco"}`, + }, + Type: openai.ChatCompletionMessageToolCallTypeFunction, + }, + { + ID: ptr.To("call_time"), + Function: openai.ChatCompletionMessageToolCallFunctionParam{ + Name: "get_time", + Arguments: `{"timezone":"PST"}`, + }, + Type: openai.ChatCompletionMessageToolCallTypeFunction, + }, + }, + }, + expectedParts: []*genai.Part{ + { + FunctionCall: &genai.FunctionCall{ + Name: "get_weather", + Args: map[string]any{"location": "San Francisco"}, + }, + ThoughtSignature: dummyThoughtSignature, + }, + { + FunctionCall: &genai.FunctionCall{ + Name: "get_time", + Args: map[string]any{"timezone": "PST"}, + }, + }, + }, + expectedToolCalls: map[string]string{ + "call_weather": "get_weather", + "call_time": "get_time", + }, + }, + { + name: "content thinking-part signature wins over differing thinking_blocks signature", + msg: openai.ChatCompletionAssistantMessageParam{ + Content: openai.StringOrAssistantRoleContentUnion{ + Value: []openai.ChatCompletionAssistantMessageParamContent{ + { + Type: openai.ChatCompletionAssistantMessageParamContentTypeThinking, + Text: ptr.To("I need to call a function to get the weather"), + Signature: ptr.To("dGVzdHNpZ25hdHVyZQ=="), // "testsignature" in base64 + }, + }, + }, + Role: openai.ChatMessageRoleAssistant, + ThinkingBlocks: []openai.ThinkingBlock{ + {Type: "thinking", Signature: "b3RoZXJzaWduYXR1cmU="}, // "othersignature" in base64 + }, + ToolCalls: []openai.ChatCompletionMessageToolCallParam{ + { + ID: ptr.To("call_weather"), + Function: openai.ChatCompletionMessageToolCallFunctionParam{ + Name: "get_weather", + Arguments: `{"location":"San Francisco"}`, + }, + Type: openai.ChatCompletionMessageToolCallTypeFunction, + }, + }, + }, + expectedParts: []*genai.Part{ + { + FunctionCall: &genai.FunctionCall{ + Name: "get_weather", + Args: map[string]any{"location": "San Francisco"}, + }, + ThoughtSignature: []byte("testsignature"), + }, + { + Text: "I need to call a function to get the weather", + Thought: true, + }, + }, + expectedToolCalls: map[string]string{ + "call_weather": "get_weather", + }, + }, + { + name: "invalid base64 in thinking_blocks signature", + msg: openai.ChatCompletionAssistantMessageParam{ + Role: openai.ChatMessageRoleAssistant, + ThinkingBlocks: []openai.ThinkingBlock{ + {Type: "thinking", Signature: "not-valid-base64!!!"}, + }, + ToolCalls: []openai.ChatCompletionMessageToolCallParam{ + { + ID: ptr.To("call_weather"), + Function: openai.ChatCompletionMessageToolCallFunctionParam{ + Name: "get_weather", + Arguments: `{"location":"San Francisco"}`, + }, + Type: openai.ChatCompletionMessageToolCallTypeFunction, + }, + }, + }, + expectedErrorMsg: "failed to decode thought signature", + }, { name: "thinking content with invalid base64 signature", msg: openai.ChatCompletionAssistantMessageParam{ From 36fea2bcb443b64197645c863c015c191b7d99d4 Mon Sep 17 00:00:00 2001 From: hustxiayang Date: Mon, 20 Jul 2026 15:51:06 -0400 Subject: [PATCH 59/59] fix: fix tool-calls finish reason conversion in streaming mode for newer gemini models like gemini-3.5-flash (#2399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** ***Problem*** When the model returns a tool call, OpenAI expects the finish reason to be `tool_calls` while Gemini returns `STOP`. We convert it via ``` if len(toolCalls) > 0 { return openai.ChatCompletionChoicesFinishReasonToolCalls } ``` This was correct for streaming mode because the finish reason and the tool calls are in the same chunk in the previous Gemini models. However, for newer Gemini models like gemini-3.5-flash, they are split into **two** chunks. The `functionCall` chunk has **no** `finishReason`, and a **trailing** chunk carries `finishReason: STOP` with an empty text part and **no** `functionCall`: ```jsonc // raw GCP chunk 1 — has the tool call, no finishReason {"candidates":[{"content":{"parts":[{"functionCall":{"name":"get_current_weather","args":{"...":"..."}}, "thoughtSignature":"..."}]}}], "usageMetadata":{"trafficType":"ON_DEMAND"}} // raw GCP chunk 2 — terminal STOP, empty text, no functionCall {"candidates":[{"content":{"parts":[{"text":""}]},"finishReason":"STOP"}], "usageMetadata":{"promptTokenCount":107,"candidatesTokenCount":31,"thoughtsTokenCount":148,"...":"..."}} ``` In this case, the conversion would be invalid. Thus, we need to remember whether the model returned a tool call during the streaming mode. --------- Signed-off-by: yxia216 --- internal/translator/openai_gcpvertexai.go | 24 +++++++++ .../translator/openai_gcpvertexai_test.go | 53 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/internal/translator/openai_gcpvertexai.go b/internal/translator/openai_gcpvertexai.go index aa39bfd3df..f3c04bc5d4 100644 --- a/internal/translator/openai_gcpvertexai.go +++ b/internal/translator/openai_gcpvertexai.go @@ -82,6 +82,12 @@ type openAIToGCPVertexAITranslatorV1ChatCompletion struct { bufferedBody []byte // Buffer for incomplete JSON chunks. requestModel internalapi.RequestModel toolCallIndex int64 + // streamedToolCall records whether any tool call has been emitted so far in + // the streaming response. Newer Gemini models (e.g. gemini-3.5-flash, + // gemini-3.1-flash-lite) stream the terminal STOP on a separate chunk that no + // longer carries the functionCall part, so the finish_reason must be derived + // from the whole stream, not just the current chunk. + streamedToolCall bool // Redaction configuration for debug logging debugLogEnabled bool enableRedaction bool @@ -444,7 +450,25 @@ func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) geminiCandidatesToOpenAI } else { choice.Delta = &openai.ChatCompletionResponseChunkChoiceDelta{} } + + // Track whether a tool call has been streamed at any point in the response. + if len(toolCalls) > 0 { + o.streamedToolCall = true + } + choice.FinishReason = geminiFinishReasonToOpenAI(candidate.FinishReason, toolCalls) + // Newer Gemini models (e.g. gemini-3.5-flash, gemini-3.1-flash-lite) stream + // the terminal STOP on a separate chunk whose parts no longer contain the + // functionCall (e.g. an empty text part), so the per-chunk toolCalls slice + // is empty and geminiFinishReasonToOpenAI maps it to "stop". If a tool call + // was streamed in an earlier chunk, the correct OpenAI finish_reason for the + // completion is still "tool_calls". Older Gemini models carried the + // functionCall and STOP in the same chunk, so this only affects the newer + // split-chunk shape. + if choice.FinishReason == openai.ChatCompletionChoicesFinishReasonStop && o.streamedToolCall { + choice.FinishReason = openai.ChatCompletionChoicesFinishReasonToolCalls + } + choices = append(choices, choice) } diff --git a/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index 6d8295d78c..c36cc9630b 100644 --- a/internal/translator/openai_gcpvertexai_test.go +++ b/internal/translator/openai_gcpvertexai_test.go @@ -1944,6 +1944,59 @@ data: {"candidates":[{"content":{"parts":[{"functionCall":{"name":"get_weather", require.Equal(t, uint32(10), outputTokens) } +func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_StreamingToolCallSplitFinishReason(t *testing.T) { + // Newer Gemini models (e.g. gemini-3.5-flash, gemini-3.1-flash-lite) stream + // the functionCall and the terminal STOP in separate chunks: the functionCall + // chunk carries no finishReason, and a trailing chunk carries finishReason=STOP + // with an empty text part (no functionCall). The completion's finish_reason + // must still be "tool_calls", not "stop". (Older Gemini models carried both in + // a single chunk; that case is covered by + // TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_StreamingToolCallWithSignature.) + translator := NewChatCompletionOpenAIToGCPVertexAITranslator("gemini-3.5-flash").(*openAIToGCPVertexAITranslatorV1ChatCompletion) + + gcpStreamingChunk := `data: {"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"name":"get_weather","args":{"location":"Paris"}},"thoughtSignature":"dG9vbGNhbGxzaWduYXR1cmU="}]}}],"usageMetadata":{"trafficType":"ON_DEMAND"}} + +data: {"candidates":[{"content":{"role":"model","parts":[{"text":""}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":15,"candidatesTokenCount":10,"totalTokenCount":25,"thoughtsTokenCount":8}}` + + headerMut, body, tokenUsage, _, err := translator.handleStreamingResponse( + bytes.NewReader([]byte(gcpStreamingChunk)), + false, + nil, + ) + + require.Nil(t, headerMut) + require.NoError(t, err) + require.NotNil(t, body) + + chatCompletionChunks := getChatCompletionResponseChunk(body) + // We expect 3 chunks: tool call (no finish_reason), terminal STOP, and usage. + require.Len(t, chatCompletionChunks, 3) + + // First chunk carries the tool call and no finish_reason yet. + firstChunk := chatCompletionChunks[0] + require.Len(t, firstChunk.Choices, 1) + assert.Equal(t, openai.ChatCompletionChoicesFinishReason(""), firstChunk.Choices[0].FinishReason) + require.Len(t, firstChunk.Choices[0].Delta.ToolCalls, 1) + assert.Equal(t, "get_weather", firstChunk.Choices[0].Delta.ToolCalls[0].Function.Name) + assert.JSONEq(t, `{"location":"Paris"}`, firstChunk.Choices[0].Delta.ToolCalls[0].Function.Arguments) + + // Second chunk carries the terminal STOP but no tool call. The finish_reason + // must be rewritten to "tool_calls" because a tool call was already streamed. + secondChunk := chatCompletionChunks[1] + require.Len(t, secondChunk.Choices, 1) + assert.Equal(t, openai.ChatCompletionChoicesFinishReason("tool_calls"), secondChunk.Choices[0].FinishReason) + assert.Empty(t, secondChunk.Choices[0].Delta.ToolCalls) + + // Third chunk is usage. + thirdChunk := chatCompletionChunks[2] + assert.NotNil(t, thirdChunk.Usage) + + // Completion tokens = candidatesTokenCount(10) + thoughtsTokenCount(8). + outputTokens, ok := tokenUsage.OutputTokens() + require.True(t, ok) + require.Equal(t, uint32(18), outputTokens) +} + func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_StreamingEndOfStream(t *testing.T) { translator := NewChatCompletionOpenAIToGCPVertexAITranslator("gemini-2.0-flash-001").(*openAIToGCPVertexAITranslatorV1ChatCompletion)