diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 2b747c7b56..48bbf859b4 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 @@ -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..a38b44c69c 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 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.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 }} 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/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/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. diff --git a/api/v1alpha1/quota_policy.go b/api/v1alpha1/quota_policy.go index 76867f9725..ea4db7175c 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 @@ -40,6 +42,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 @@ -59,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"` @@ -90,13 +95,13 @@ 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"` // 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 @@ -133,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/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/cmd/aigw/translate.go b/cmd/aigw/translate.go index 5a9859c812..eb0d20876f 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()), + 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, ) + // 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/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/go.mod b/go.mod index 67bce5632b..236c23a031 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,12 @@ 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/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/service/sts v1.42.1 - github.com/bytedance/sonic v1.15.1 + github.com/anthropics/anthropic-sdk-go v1.46.0 + 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 @@ -29,27 +29,27 @@ 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.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.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 - go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 - go.opentelemetry.io/otel v1.43.0 - go.opentelemetry.io/otel/exporters/prometheus v0.65.0 - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 - go.opentelemetry.io/otel/metric v1.43.0 - go.opentelemetry.io/otel/sdk v1.43.0 - go.opentelemetry.io/otel/sdk/metric v1.43.0 - go.opentelemetry.io/otel/trace v1.43.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.69.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.69.0 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/exporters/prometheus v0.66.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 + go.opentelemetry.io/otel/metric v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/sdk/metric v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 go.opentelemetry.io/proto/otlp v1.10.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.28.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.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 gopkg.in/dnaeon/go-vcr.v4 v4.0.6 @@ -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.16 // 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.35.21 // 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 @@ -167,12 +167,12 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-containerregistry v0.20.6 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -236,23 +236,23 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.6.8 // indirect go.etcd.io/etcd/client/v3 v3.6.8 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.69.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect - go.opentelemetry.io/otel/log v0.19.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.44.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.44.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0 // indirect + go.opentelemetry.io/otel/log v0.20.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.20.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect @@ -260,13 +260,13 @@ require ( golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.51.0 // indirect golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.54.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/grpc/security/advancedtls v1.0.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 2524e427f7..88f0186da2 100644 --- a/go.sum +++ b/go.sum @@ -43,44 +43,44 @@ 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.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= 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.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/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.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/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= @@ -271,8 +271,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= -github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw= +github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE= github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= @@ -281,8 +281,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -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.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= @@ -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= @@ -495,62 +495,64 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= -go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= -go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk= -go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8= +go.opentelemetry.io/contrib/bridges/prometheus v0.69.0 h1:saQoWg5845Q8TojpqeVStS7zGwVZ6bc5W2PJavTPiBM= +go.opentelemetry.io/contrib/bridges/prometheus v0.69.0/go.mod h1:AAaS6xs5AyqMdR3Ir0nSWK+QudL2XM8Vbw5INzUxNc8= +go.opentelemetry.io/contrib/exporters/autoexport v0.69.0 h1:R3jsCoTIzv0BiYNhW0axyswn/6SMJ8xL1OuGxvni1Kw= +go.opentelemetry.io/contrib/exporters/autoexport v0.69.0/go.mod h1:m07gqyr2QhQxKOKb5vqKCCBtLH3uqlNYR7PU/FISXVU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k= -go.opentelemetry.io/contrib/propagators/autoprop v0.68.0/go.mod h1:evWK9nCqCzH8nhclTlpkdUzmxrmJQ2mrWCdKIvyOYec= -go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg= -go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8= -go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A= -go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw= -go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 h1:peiLMz1+aqJE+3L4mOVtR9wlmv+yh/JVYXCBjqmzJJE= -go.opentelemetry.io/contrib/propagators/jaeger v1.43.0/go.mod h1:Agvif+4A8p/3UtZzJ0MCcDEuQwgtrzM71DueU41DCs8= -go.opentelemetry.io/contrib/propagators/ot v1.43.0 h1:Hh1HahlGc81AOE7siqi1tVOlbanY/UxMMWedpb0d5oQ= -go.opentelemetry.io/contrib/propagators/ot v1.43.0/go.mod h1:58MlyS7lghzYvAm5LN9gGmZpCMQEMB5vpZp9SRgOyE4= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE= -go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= -go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= -go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= -go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/contrib/propagators/autoprop v0.69.0 h1:3gzAeb5dgGzwB7hXutgJ07Xsv3v4Wc0llV8AaMc0wiQ= +go.opentelemetry.io/contrib/propagators/autoprop v0.69.0/go.mod h1:SpChkgQWjh6egTT0chEc7VfusZgQMPzLsxRWWrqJdaQ= +go.opentelemetry.io/contrib/propagators/aws v1.44.0 h1:Rtvfd6nTbAF2csjiw41m1DfuqC5TneXs+gB84ZA3gq4= +go.opentelemetry.io/contrib/propagators/aws v1.44.0/go.mod h1:auu0tIyZErQGLLUvOp9DgmhKALIoebR4Fpkt9CT0c0k= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0 h1:1IFH4oFKK8KupzIelCl3u+bkxpGRps1oWRjQI2+TTWs= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0/go.mod h1:JqWFXsc7VDaqIyubFhEd2cPHqsrzqP0Lvn783SUwyro= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 h1:OyzvsAMc/zHt0DRPcfstn0wgfq8ApDkeY0ABMcueweM= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0/go.mod h1:44kghcGX+BNxy9UTiWtd6VDt8Nd4EypGBkH2+v2Dqrc= +go.opentelemetry.io/contrib/propagators/ot v1.44.0 h1:JLTPenzmPtLp5ODPntAA5JhxVu1i3pAFUvXcAORMZAk= +go.opentelemetry.io/contrib/propagators/ot v1.44.0/go.mod h1:8zr0bHgwkoQXucBK39/H4QphmLf1lSen1Z7FPDZD5Uc= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 h1:rydZ9sxbcFdm/oWrVyfLTjHIygMgv0bEeMd+3B/BvoM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0/go.mod h1:earQ25dooT0Hhspq59DZ8YCC50jWfOlFEeWoxy/P444= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0/go.mod h1:MP4eemTiI9zC8fgg+DYynhYDYf3ba72S376TvP+Ye0Q= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/exporters/prometheus v0.66.0 h1:vkrK8PAznv2NKt2r+kdu252ccGzkEqLc2aSXbQIALYQ= +go.opentelemetry.io/otel/exporters/prometheus v0.66.0/go.mod h1:V/UB6D3vMF/UBOL5igAsAYnk1nG/bzYYTzvsB16cy7o= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0 h1:aZfdmtI6QU/DAPD4b7YZ5zuJgewxO1EW9miOZklqleU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0/go.mod h1:isNl10/Om5CBWu9jj8WOb2+tJLbCVXDgqwzCaJMnJ6w= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D/gAmznQTd4dxxMA93aKbcpKqiTCs= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0= +go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs= +go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY= +go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU= +go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg= +go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -582,8 +584,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -598,8 +600,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -622,16 +624,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.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-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/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/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= diff --git a/internal/apischema/gcp/gcp.go b/internal/apischema/gcp/gcp.go index 4218d344a9..24890c217b 100644 --- a/internal/apischema/gcp/gcp.go +++ b/internal/apischema/gcp/gcp.go @@ -148,3 +148,47 @@ type CachedContentRequest struct { // Name is populated on responses; it is the fully-qualified resource name of the cache entry. Name string `json:"name,omitempty"` } + +// 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 4f67873576..3623ab8c7f 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. @@ -639,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. @@ -1662,6 +1668,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 { @@ -1684,15 +1731,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 +1754,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 +1837,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 - - AutoTruncate bool `json:"auto_truncate,omitempty"` + // Auto-truncate input text if it exceeds the model's max length. Defaults to true. + 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/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/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/controller/ai_gateway_route.go b/internal/controller/ai_gateway_route.go index 970b4afe74..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, }}, @@ -367,24 +379,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..d38da246d4 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) { @@ -472,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/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..058e2783b0 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,51 @@ 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 + + candidateNamespaces := make([]string, 1, 2) + candidateNamespaces[0] = gw.Namespace + if c.envoyGatewayNamespace != gw.Namespace { + candidateNamespaces = append(candidateNamespaces, c.envoyGatewayNamespace) } - 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 candidateNamespaces { + 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..7228a0c43f 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,57 @@ 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") +} + +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) +} diff --git a/internal/controller/mcp_route.go b/internal/controller/mcp_route.go index a4908d2d39..94fbeeb96f 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 } @@ -460,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. @@ -549,87 +567,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 +681,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 +692,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 +713,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 +742,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 +764,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..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) { @@ -285,42 +287,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 +355,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 +371,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 +429,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 +492,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) { @@ -447,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)) + } +} 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") } diff --git a/internal/endpointspec/endpointspec_test.go b/internal/endpointspec/endpointspec_test.go index 5f946f6a93..4fa45fa9ec 100644 --- a/internal/endpointspec/endpointspec_test.go +++ b/internal/endpointspec/endpointspec_test.go @@ -148,8 +148,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) @@ -160,6 +166,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/extensionserver/extensionserver_test.go b/internal/extensionserver/extensionserver_test.go index 127c986a5e..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{ @@ -1355,6 +1448,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 +1513,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) } diff --git a/internal/extensionserver/post_translate_modify.go b/internal/extensionserver/post_translate_modify.go index 89689e1b3d..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 { @@ -676,6 +715,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) @@ -839,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 { @@ -850,9 +913,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: @@ -867,6 +941,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 00ea2c718d..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 { @@ -340,6 +382,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{ diff --git a/internal/extproc/processor_impl.go b/internal/extproc/processor_impl.go index dd13a1038c..c9f7c7cf88 100644 --- a/internal/extproc/processor_impl.go +++ b/internal/extproc/processor_impl.go @@ -395,6 +395,11 @@ func (r *routerProcessor[ReqT, RespT, RespChunkT, EndpointSpecT]) ProcessRequest Header: &corev3.HeaderValue{Key: internalapi.ModelNameHeaderKeyDefault, RawValue: []byte(originalModel)}, }) originalPath := r.requestHeaders[":path"] + // 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{ @@ -505,8 +510,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 { @@ -531,6 +547,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 291e81eed8..9dde9fd011 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{} 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) { 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"]) +} diff --git a/internal/internalapi/internalapi.go b/internal/internalapi/internalapi.go index 0336065566..59652de7f3 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/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..9a82bf8118 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 @@ -292,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) } 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) 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) { 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 { 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/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("", "") diff --git a/internal/translator/anthropic_awsbedrock.go b/internal/translator/anthropic_awsbedrock.go index 1266b920dd..27e1d9c945 100644 --- a/internal/translator/anthropic_awsbedrock.go +++ b/internal/translator/anthropic_awsbedrock.go @@ -66,43 +66,41 @@ 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) - 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) { + if isOnlyToolResult(msg) { bedrockMsg := a.convertToolResultMessage(msg) - // Coalesce consecutive tool result messages. - for i+1 < msgLen && hasToolResult(&body.Messages[i+1]) { - nextMsg := &body.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...) 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) } } @@ -162,16 +160,56 @@ func (a *anthropicToAWSBedrockTranslator) RequestBody(_ []byte, body *anthropics return } -func hasToolResult(msg *anthropicschema.MessageParam) bool { +// 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 +} + +// 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) { @@ -203,13 +241,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 { @@ -244,7 +282,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 c382d4e814..69a8ef8bf8 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 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) + // 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) + + // 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{ @@ -1234,3 +1497,290 @@ 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) + }) +} + +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/anthropic_helper.go b/internal/translator/anthropic_helper.go index f454c2433f..759f29f3cf 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() } @@ -425,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) @@ -545,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 } @@ -608,14 +621,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 { @@ -623,13 +643,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 } @@ -821,6 +846,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) ( @@ -864,6 +950,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 +964,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, } @@ -958,8 +1048,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) } @@ -1042,27 +1132,30 @@ func (p *anthropicStreamParser) handleAnthropicStreamEvent(eventType []byte, dat if err := json.Unmarshal(data, &event); err != nil { return nil, fmt.Errorf("unmarshal message_delta: %w", 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) + // 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 } - // 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) + u := event.Usage + // 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 cacheCreation, ok := usage.CacheCreationInputTokens(); ok { - p.tokenUsage.AddInputTokens(cacheCreation) - // Accumulate cache creation tokens - p.tokenUsage.AddCacheCreationInputTokens(cacheCreation) + if u.JSON.OutputTokensDetails.Valid() && u.OutputTokensDetails.JSON.ThinkingTokens.Valid() { + p.tokenUsage.SetReasoningTokens(uint32(u.OutputTokensDetails.ThinkingTokens)) //nolint:gosec } if event.Delta.StopReason != "" { p.stopReason = event.Delta.StopReason @@ -1178,11 +1271,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 +1286,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/anthropic_helper_test.go b/internal/translator/anthropic_helper_test.go index eb39929731..0b54e07622 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. @@ -752,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 @@ -1157,3 +1497,248 @@ 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 +} + +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/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"}, diff --git a/internal/translator/gemini_helper.go b/internal/translator/gemini_helper.go index d3ac0f5627..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,9 +279,27 @@ 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 { + 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 { @@ -286,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{ 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_awsbedrock.go b/internal/translator/openai_awsbedrock.go index d5ddc9d168..486a1f1195 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 @@ -521,7 +524,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) @@ -730,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_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_awsbedrock_test.go b/internal/translator/openai_awsbedrock_test.go index c07479ea62..e10a622106 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,159 @@ 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) + } + } +} + +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") +} 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/internal/translator/openai_gcpvertexai.go b/internal/translator/openai_gcpvertexai.go index e4066e1ab2..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 @@ -249,21 +255,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 } } @@ -454,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_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/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index 6a7ce3a1d2..c36cc9630b 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" @@ -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", "256"}, }, 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", "256"}, }, 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", "256"}, }, 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", "516"}, }, 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", "395"}, }, 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", "394"}, }, 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", "342"}, }, 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", "403"}, }, 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", "407"}, }, 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", "189"}, }, 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) } @@ -1223,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, @@ -1232,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", @@ -1245,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, @@ -1254,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), }, } @@ -1937,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) 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 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/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..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 @@ -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: |- @@ -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. @@ -324,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: |- @@ -354,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 @@ -370,6 +371,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: |- @@ -377,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/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/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 }} 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" . }} 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/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/api/api.mdx b/site/docs/api/api.mdx index 378c4cdad0..43d513f26d 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 @@ -2295,13 +2296,13 @@ 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`" /> diff --git a/site/docs/capabilities/index.md b/site/docs/capabilities/index.md index bd5f9fd52d..9e759f3ec1 100644 --- a/site/docs/capabilities/index.md +++ b/site/docs/capabilities/index.md @@ -37,6 +37,7 @@ Comprehensive traffic handling and routing capabilities: - **[Model Virtualization](./traffic/model-virtualization.md)**: Abstract and virtualize AI models - **[Provider Fallback](./traffic/provider-fallback.md)**: Automatic failover between AI providers +- **[Quota Policy](./traffic/quota-policy.md)**: Token-based quota management for controlling total consumption budgets - **[Usage-based Rate Limiting](./traffic/usage-based-ratelimiting.md)**: Token-aware rate limiting for AI workloads - **[Header and Body Mutations](./traffic/header-body-mutations.md)**: Customize HTTP headers and JSON body fields per backend or route diff --git a/site/docs/capabilities/traffic/index.md b/site/docs/capabilities/traffic/index.md index 5b2a7086f4..a6772a4faf 100644 --- a/site/docs/capabilities/traffic/index.md +++ b/site/docs/capabilities/traffic/index.md @@ -3,4 +3,4 @@ id: traffic title: Traffic Handling --- -This section provides information about traffic routing related capabilities in Envoy AI Gateway. +This section provides information about traffic routing related capabilities in Envoy AI Gateway, including [Quota Policy](./quota-policy.md) for controlling total token consumption budgets across time windows. diff --git a/site/docs/capabilities/traffic/quota-policy.md b/site/docs/capabilities/traffic/quota-policy.md new file mode 100644 index 0000000000..22d64c5183 --- /dev/null +++ b/site/docs/capabilities/traffic/quota-policy.md @@ -0,0 +1,327 @@ +--- +id: quota-policy +title: Quota Policy +sidebar_position: 6 +--- + +# Quota Policy + +`QuotaPolicy` enables token-based quota management for AI inference services in Envoy AI Gateway. +When all related backend's quota are exceeded, requests are rejected with a `429 Too Many Requests` status code. + +:::note QuotaPolicy vs. Rate Limiting +QuotaPolicy manages **total consumption budgets** (for example, 100,000 tokens per hour). This is +distinct from [usage-based rate limiting](./usage-based-ratelimiting.md), which controls **request +velocity** (for example, requests per second). Use QuotaPolicy when you need to cap cumulative token +spend across a time window. +::: + +## Overview + +Key features of QuotaPolicy: + +- **Per-model token quotas** — assign token budgets to individual models served by an `AIServiceBackend`. +- **CEL cost expressions** — weight input, output, cached, and reasoning tokens differently when + computing how much a request burns down a quota. +- **Client-selector bucket rules** — carve out per-tenant or per-header quotas using request attributes. +- **Shadow mode** — evaluate quota rules without enforcing them, for safe rollout. + +## How It Works + +1. A `QuotaPolicy` is attached to one or more `AIServiceBackend` resources via `targetRefs`. +2. For each completed request, the token cost is computed using the configured cost expression + (defaults to `total_tokens`). +3. The cost is charged against the matching quota bucket (the per-model default bucket, or a matching + bucket rule). +4. When all related quota buckets for that model are exceeded, subsequent matching requests receive `429 Too Many Requests`. + +:::tip Prerequisites +Quota enforcement uses the same infrastructure as usage-based rate limiting: + +1. **Redis Deployment**: A Redis instance for storing quota counters. See the [redis.yaml example](https://github.com/envoyproxy/ai-gateway/blob/main/examples/token_ratelimit/redis.yaml) for a simple deployment. +2. **Envoy Gateway Configuration**: Envoy Gateway must be configured at installation time to enable rate limiting and point to your Redis instance. See the [Envoy Gateway Installation Guide](../../getting-started/prerequisites.md#additional-features-rate-limiting-inferencepool-etc). + +See [Usage-based Rate Limiting](./usage-based-ratelimiting.md) for more detail on the rate limit infrastructure that QuotaPolicy builds on. +::: + +## Configuration + +### Per-Model Quotas + +Use `perModelQuotas` to apply a token budget to a specific model served by the targeted backend(s). + +:::warning The model name must match the route +A `perModelQuotas` entry only applies when its `modelName` matches the `modelNameOverride` set on the +`AIGatewayRoute` rule's `backendRefs` for the targeted backend. If they do not match, the quota is +silently **not** applied. +::: + +Given an `AIGatewayRoute` that routes a model to the backend: + +```yaml +apiVersion: aigateway.envoyproxy.io/v1alpha1 +kind: AIGatewayRoute +metadata: + name: my-route +spec: + rules: + - backendRefs: + - name: my-backend + modelNameOverride: my-model # <-- the QuotaPolicy modelName must match this +``` + +attach a `QuotaPolicy` to the backend: + +```yaml +apiVersion: aigateway.envoyproxy.io/v1alpha1 +kind: QuotaPolicy +metadata: + name: my-quota-policy +spec: + targetRefs: + - group: aigateway.envoyproxy.io + kind: AIServiceBackend + name: my-backend + perModelQuotas: + - modelName: "my-model" + quota: + mode: Shared + defaultBucket: + limit: 10000 # Maximum tokens allowed in the window. + duration: "1h" # Sliding window. +``` + +You can attach quotas for multiple models, each with its own budget: + +```yaml +perModelQuotas: + - modelName: gpt-4 + quota: + defaultBucket: + limit: 10000 # Strict limit for the expensive model. + duration: "1h" + - modelName: gpt-3.5-turbo + quota: + defaultBucket: + limit: 100000 # Higher limit for the cost-effective model. + duration: "1h" +``` + +:::note +When multiple `QuotaPolicy` resources define the same model for the same `AIServiceBackend`, the +policy whose namespace/name sorts alphabetically first takes precedence. +::: + +### Custom Cost Expression + +By default, a request's cost is its `total_tokens`. You can override this with a +[CEL](https://github.com/google/cel-spec) expression that weights token types differently. The +following variables are available in a `costExpression`: + +| Variable | Type | Description | +| ----------------------------- | ------ | ------------------------------------------------ | +| `input_tokens` | uint | Prompt / input tokens. | +| `output_tokens` | uint | Completion / output tokens. | +| `total_tokens` | uint | Total tokens (the default cost). | +| `cached_input_tokens` | uint | Input tokens served from the provider's cache. | +| `cache_creation_input_tokens` | uint | Input tokens charged for writing to the cache. | +| `reasoning_tokens` | uint | Reasoning tokens (for reasoning-capable models). | +| `model` | string | The resolved model name. | +| `backend` | string | The serving backend name. | +| `route_name` | string | The route name. | + +```yaml +perModelQuotas: + - modelName: gpt-4 + quota: + # Cached input tokens count as 1/10 of a regular input token; + # output tokens count 6x. + costExpression: "input_tokens + cached_input_tokens / 10u + output_tokens * 6u" + defaultBucket: + limit: 50000 + duration: "1h" +``` + +:::tip +Use a custom cost expression when token types have significantly different costs with your provider — +for example, output tokens are typically more expensive than input tokens. + +The token variables are **unsigned integers**, so numeric literals must carry a `u` suffix (for +example `output_tokens * 6u`) and the expression must evaluate to a non-negative integer. Integer +division truncates (`cached_input_tokens / 10u`); for an exact fractional weight, cast through +floating point — for example `uint(double(cached_input_tokens) * 0.1)`. +::: + +### Bucket Mode + +The `mode` field on a per-model `quota` controls how the `defaultBucket` and matching `bucketRules` +interact when a request matches one or more rules. + +Currently only **`Shared`** mode is supported (it is also the default, so the field can be omitted): + +- The request is charged to **all** matching `bucketRules` **and** the `defaultBucket`. +- The request is allowed only if quota is available in **at least one** matching bucket. + +```yaml +perModelQuotas: + - modelName: gpt-4 + quota: + mode: Shared # Default — may be omitted. + defaultBucket: + limit: 10000 + duration: "1h" +``` + +:::note +An exclusive bucket mode (charging matching rules **or** the default bucket, but not both) is planned +but not yet available. The only accepted value today is `Shared`. +::: + +### Client Selectors with Bucket Rules + +Bucket rules let you carve out dedicated quotas for specific clients identified by request attributes +such as headers. This is useful for multi-tenant deployments where each tenant needs its own budget. +Because the mode is `Shared`, a request that matches a bucket rule is charged against **both** that +rule's bucket **and** the `defaultBucket`. + +```yaml +perModelQuotas: + - modelName: gpt-4 + quota: + defaultBucket: + limit: 10000 # Shared budget across all tenants. + duration: "1h" + bucketRules: + # Premium tenant gets a dedicated, higher per-tenant budget. + - clientSelectors: + - headers: + - name: x-tenant-id + type: Exact + value: premium-tenant + quota: + limit: 50000 + duration: "1h" +``` + +Use `type: Distinct` to create a separate bucket for every unique value of a header — for example, a +per-tenant budget keyed on the tenant ID: + +```yaml +bucketRules: + - clientSelectors: + - headers: + - name: x-tenant-id + type: Distinct # One bucket per unique tenant ID. + quota: + limit: 5000 + duration: "1h" +``` + +Supported header match types are `Exact`, `Distinct`, and `RegularExpression`. + +`clientSelectors` reuse the Envoy Gateway [`RateLimitSelectCondition`](https://gateway.envoyproxy.io/docs/api/extension_types/#ratelimitselectcondition) type, but QuotaPolicy currently applies only the `headers` matcher. The other fields on that type (`sourceCIDR`, `methods`, `path`, `queryParams`) are accepted by the schema but **not yet honored** for quota buckets. + +### Shadow Mode + +Shadow mode lets you test a bucket rule without rejecting traffic. When `shadowMode` is enabled on a +bucket rule, all quota checks are performed (cache lookups, counter updates, telemetry), but the +outcome is never enforced — the request always succeeds even if the quota is exceeded. + +```yaml +bucketRules: + - clientSelectors: + - headers: + - name: x-tenant-id + type: Distinct + quota: + limit: 5000 + duration: "1h" + shadowMode: true # Evaluate but do not enforce. +``` + +:::tip +Use shadow mode when rolling out a new quota rule. Monitor the telemetry to confirm the limit is set +correctly, then enable enforcement by removing `shadowMode` (or setting it to `false`). +::: + +Shadow mode is configured per bucket rule. It cannot be set on the `defaultBucket`. + +## Duration Format + +The `duration` field selects the sliding-window size. It must be exactly one of the following values: + +| Value | Window | +| ------ | ---------- | +| `"1s"` | One second | +| `"1m"` | One minute | +| `"1h"` | One hour | +| `"1d"` | One day | + +The window is fixed-size — arbitrary multiples such as `"30s"` or `"15m"` are **not** valid and will +be rejected by the CRD schema. Choose the `limit` to express your budget within one of these windows. + +## Service-Wide Quota + +:::warning Not yet available +The API exposes a backend-wide `serviceQuota` field — intended to apply a single budget across all +models on a backend — but it is **not yet enforced** (it is currently a known TODO in the API). +Configuring it has no effect on traffic, so use [Per-Model Quotas](#per-model-quotas) to enforce +token quotas today. This section will document `serviceQuota` once enforcement lands. +::: + +## Full Example + +The following `QuotaPolicy` combines per-model quotas, custom cost expressions, bucket rules with +client selectors, and shadow mode. + +```yaml +apiVersion: aigateway.envoyproxy.io/v1alpha1 +kind: QuotaPolicy +metadata: + name: full-quota-policy +spec: + targetRefs: + - group: aigateway.envoyproxy.io + kind: AIServiceBackend + name: my-backend + + perModelQuotas: + - modelName: gpt-4 + quota: + costExpression: "input_tokens + cached_input_tokens / 10u + output_tokens * 6u" + mode: Shared + defaultBucket: + limit: 10000 + duration: "1h" + bucketRules: + # Premium tenant gets a dedicated, higher quota. + - clientSelectors: + - headers: + - name: x-tenant-id + type: Exact + value: premium-tenant + quota: + limit: 50000 + duration: "1h" + # Track per-tenant usage in shadow mode (no enforcement). + - clientSelectors: + - headers: + - name: x-tenant-id + type: Distinct + quota: + limit: 5000 + duration: "1h" + shadowMode: true + + - modelName: gpt-3.5-turbo + quota: + defaultBucket: + limit: 100000 + duration: "1h" +``` + +## References + +- [API Reference](../../api/api.mdx) +- [Usage-Based Rate Limiting](./usage-based-ratelimiting.md) +- [Provider Fallback](./provider-fallback.md) diff --git a/site/docs/capabilities/traffic/usage-based-ratelimiting.md b/site/docs/capabilities/traffic/usage-based-ratelimiting.md index 13cdf155bf..4e9077d986 100644 --- a/site/docs/capabilities/traffic/usage-based-ratelimiting.md +++ b/site/docs/capabilities/traffic/usage-based-ratelimiting.md @@ -9,6 +9,10 @@ import TabItem from '@theme/TabItem'; This guide focuses on AI Gateway's specific capabilities for token-based rate limiting in LLM requests. For general rate limiting concepts and configurations, refer to [Envoy Gateway's Rate Limiting documentation](https://gateway.envoyproxy.io/docs/tasks/traffic/global-rate-limit/). +:::info Quota Policy vs. Rate Limiting +AI Gateway also provides [Quota Policy](./quota-policy.md) for managing **total consumption budgets** (for example, 100,000 tokens per hour). Use QuotaPolicy when you need to cap cumulative token spend, and usage-based rate limiting (this page) when you need to control **request velocity**. +::: + ## Overview AI Gateway leverages Envoy Gateway's Global Rate Limit API to provide token-based rate limiting for LLM requests. Key features include: diff --git a/site/docs/compatibility.md b/site/docs/compatibility.md index ea4cc4f378..4fdfe5221c 100644 --- a/site/docs/compatibility.md +++ b/site/docs/compatibility.md @@ -10,7 +10,9 @@ 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.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 | | others | N/A | N/A | N/A | End of Life | 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', 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" + } } 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. ), }, 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/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." }, { diff --git a/site/static/img/adopters/stacklok.png b/site/static/img/adopters/stacklok.png new file mode 100644 index 0000000000..7d983fe66b Binary files /dev/null and b/site/static/img/adopters/stacklok.png differ 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 | diff --git a/tests/data-plane/testupstream_test.go b/tests/data-plane/testupstream_test.go index c4ef822774..10652ceb1a 100644 --- a/tests/data-plane/testupstream_test.go +++ b/tests/data-plane/testupstream_test.go @@ -329,7 +329,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, }, { @@ -397,7 +397,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", @@ -411,7 +411,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", @@ -590,7 +590,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": ""}} @@ -605,7 +605,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"} @@ -617,7 +617,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] @@ -653,7 +653,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": {}}} @@ -668,7 +668,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"}`, @@ -681,7 +681,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] @@ -949,7 +949,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": ""}} @@ -961,7 +961,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"} @@ -969,7 +969,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": ""}} @@ -981,7 +981,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"} @@ -1091,7 +1091,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"} @@ -1129,7 +1129,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, @@ -1158,7 +1158,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}} 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) + }) + } +} 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", }, } 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=