Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/thv-operator/api/v1beta1/virtualmcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
// +kubebuilder:validation:XValidation:rule="!has(self.config) || !has(self.config.rateLimiting) || (has(self.sessionStorage) && self.sessionStorage.provider == 'redis')",message="config.rateLimiting requires sessionStorage with provider 'redis'"
// +kubebuilder:validation:XValidation:rule="!(has(self.config) && has(self.config.rateLimiting) && has(self.config.rateLimiting.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')",message="config.rateLimiting.perUser requires incomingAuth.type oidc"
// +kubebuilder:validation:XValidation:rule="!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools) || self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')",message="per-tool perUser rate limiting requires incomingAuth.type oidc"
// +kubebuilder:validation:XValidation:rule="!(has(self.embeddingServerRef) && has(self.config) && has(self.config.optimizer) && has(self.config.optimizer.embeddingProvider) && self.config.optimizer.embeddingProvider == 'openai')",message="embeddingServerRef provisions a managed TEI server and cannot be combined with optimizer.embeddingProvider 'openai'; openai mode uses embeddingService directly"
//
//nolint:lll // CEL validation rules exceed line length limit
type VirtualMCPServerSpec struct {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

// Package controllers contains integration tests for the VirtualMCPServer controller
package controllers

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

mcpv1beta1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1"
"github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1/v1beta1test"
vmcpconfig "github.com/stacklok/toolhive/pkg/vmcp/config"
)

func newVirtualMCPServerWithOptimizer(name string, optimizer *vmcpconfig.OptimizerConfig,
opts ...v1beta1test.VirtualMCPServerOption) *mcpv1beta1.VirtualMCPServer {
base := []v1beta1test.VirtualMCPServerOption{
v1beta1test.WithVMCPGroupRef("test-group"),
v1beta1test.WithVMCPIncomingAuth(&mcpv1beta1.IncomingAuthConfig{Type: "anonymous"}),
v1beta1test.WithVMCPConfig(vmcpconfig.Config{Group: "test-group", Optimizer: optimizer}),
}
return v1beta1test.NewVirtualMCPServer(name, "default", append(base, opts...)...)
}

var _ = Describe("CEL Validation for embedding provider on VirtualMCPServer",
Label("k8s", "cel", "validation"), func() {
It("should reject embeddingServerRef combined with embeddingProvider openai", func() {
vmcp := newVirtualMCPServerWithOptimizer("vmcp-ref-openai",
&vmcpconfig.OptimizerConfig{EmbeddingProvider: "openai", EmbeddingModel: "text-embedding-3-small"},
v1beta1test.WithVMCPEmbeddingServerRef("managed-tei"))
err := k8sClient.Create(ctx, vmcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(
"embeddingServerRef provisions a managed TEI server and cannot be combined with optimizer.embeddingProvider 'openai'"))
})

It("should accept embeddingServerRef with the default (tei) provider", func() {
vmcp := newVirtualMCPServerWithOptimizer("vmcp-ref-tei",
&vmcpconfig.OptimizerConfig{EmbeddingProvider: "tei"},
v1beta1test.WithVMCPEmbeddingServerRef("managed-tei"))
err := k8sClient.Create(ctx, vmcp)
Expect(err).NotTo(HaveOccurred())
})

It("should accept embeddingProvider openai without an embeddingServerRef", func() {
vmcp := newVirtualMCPServerWithOptimizer("vmcp-openai-no-ref",
&vmcpconfig.OptimizerConfig{
EmbeddingProvider: "openai",
EmbeddingService: "http://gateway.example:8080",
EmbeddingModel: "text-embedding-3-small",
})
err := k8sClient.Create(ctx, vmcp)
Expect(err).NotTo(HaveOccurred())
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -1792,6 +1792,34 @@ spec:
instead of all backend tools directly. This reduces token usage by allowing
LLMs to discover relevant tools on demand rather than receiving all tool definitions.
properties:
embeddingModel:
description: |-
EmbeddingModel is the model name requested from the embedding service
(e.g. "text-embedding-3-small"). Required when EmbeddingProvider is
"openai". Ignored for the "tei" provider, where the model is fixed by the
running TEI container.

The API key for an OpenAI-compatible service is not configured here: it is
read from the OPENAI_API_KEY environment variable so the secret never
lands in a CRD spec or ConfigMap. An empty key omits the Authorization
header, which supports keyless in-cluster gateways.
type: string
embeddingProvider:
default: tei
description: |-
EmbeddingProvider selects the wire protocol used to talk to the embedding
service. "tei" speaks the HuggingFace Text Embeddings Inference API;
"openai" speaks the OpenAI-compatible /embeddings API, which lets the
optimizer use OpenAI, Azure OpenAI, or another OpenAI-compatible gateway.
Defaults to "tei" when empty.

The "openai" provider reads EmbeddingService directly and cannot be combined
with EmbeddingServerRef, which provisions a managed TEI server; the operator
rejects that combination at admission.
enum:
- tei
- openai
type: string
embeddingService:
description: |-
EmbeddingService is the full base URL of the embedding service endpoint
Expand Down Expand Up @@ -2936,6 +2964,12 @@ spec:
rule: '!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools)
|| self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth)
&& self.incomingAuth.type == ''oidc'')'
- message: embeddingServerRef provisions a managed TEI server and cannot
be combined with optimizer.embeddingProvider 'openai'; openai mode
uses embeddingService directly
rule: '!(has(self.embeddingServerRef) && has(self.config) && has(self.config.optimizer)
&& has(self.config.optimizer.embeddingProvider) && self.config.optimizer.embeddingProvider
== ''openai'')'
status:
description: VirtualMCPServerStatus defines the observed state of VirtualMCPServer
properties:
Expand Down Expand Up @@ -4880,6 +4914,34 @@ spec:
instead of all backend tools directly. This reduces token usage by allowing
LLMs to discover relevant tools on demand rather than receiving all tool definitions.
properties:
embeddingModel:
description: |-
EmbeddingModel is the model name requested from the embedding service
(e.g. "text-embedding-3-small"). Required when EmbeddingProvider is
"openai". Ignored for the "tei" provider, where the model is fixed by the
running TEI container.

The API key for an OpenAI-compatible service is not configured here: it is
read from the OPENAI_API_KEY environment variable so the secret never
lands in a CRD spec or ConfigMap. An empty key omits the Authorization
header, which supports keyless in-cluster gateways.
type: string
embeddingProvider:
default: tei
description: |-
EmbeddingProvider selects the wire protocol used to talk to the embedding
service. "tei" speaks the HuggingFace Text Embeddings Inference API;
"openai" speaks the OpenAI-compatible /embeddings API, which lets the
optimizer use OpenAI, Azure OpenAI, or another OpenAI-compatible gateway.
Defaults to "tei" when empty.

The "openai" provider reads EmbeddingService directly and cannot be combined
with EmbeddingServerRef, which provisions a managed TEI server; the operator
rejects that combination at admission.
enum:
- tei
- openai
type: string
embeddingService:
description: |-
EmbeddingService is the full base URL of the embedding service endpoint
Expand Down Expand Up @@ -6024,6 +6086,12 @@ spec:
rule: '!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools)
|| self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth)
&& self.incomingAuth.type == ''oidc'')'
- message: embeddingServerRef provisions a managed TEI server and cannot
be combined with optimizer.embeddingProvider 'openai'; openai mode
uses embeddingService directly
rule: '!(has(self.embeddingServerRef) && has(self.config) && has(self.config.optimizer)
&& has(self.config.optimizer.embeddingProvider) && self.config.optimizer.embeddingProvider
== ''openai'')'
status:
description: VirtualMCPServerStatus defines the observed state of VirtualMCPServer
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,34 @@ spec:
instead of all backend tools directly. This reduces token usage by allowing
LLMs to discover relevant tools on demand rather than receiving all tool definitions.
properties:
embeddingModel:
description: |-
EmbeddingModel is the model name requested from the embedding service
(e.g. "text-embedding-3-small"). Required when EmbeddingProvider is
"openai". Ignored for the "tei" provider, where the model is fixed by the
running TEI container.

The API key for an OpenAI-compatible service is not configured here: it is
read from the OPENAI_API_KEY environment variable so the secret never
lands in a CRD spec or ConfigMap. An empty key omits the Authorization
header, which supports keyless in-cluster gateways.
type: string
embeddingProvider:
default: tei
description: |-
EmbeddingProvider selects the wire protocol used to talk to the embedding
service. "tei" speaks the HuggingFace Text Embeddings Inference API;
"openai" speaks the OpenAI-compatible /embeddings API, which lets the
optimizer use OpenAI, Azure OpenAI, or another OpenAI-compatible gateway.
Defaults to "tei" when empty.

The "openai" provider reads EmbeddingService directly and cannot be combined
with EmbeddingServerRef, which provisions a managed TEI server; the operator
rejects that combination at admission.
enum:
- tei
- openai
type: string
embeddingService:
description: |-
EmbeddingService is the full base URL of the embedding service endpoint
Expand Down Expand Up @@ -2939,6 +2967,12 @@ spec:
rule: '!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools)
|| self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth)
&& self.incomingAuth.type == ''oidc'')'
- message: embeddingServerRef provisions a managed TEI server and cannot
be combined with optimizer.embeddingProvider 'openai'; openai mode
uses embeddingService directly
rule: '!(has(self.embeddingServerRef) && has(self.config) && has(self.config.optimizer)
&& has(self.config.optimizer.embeddingProvider) && self.config.optimizer.embeddingProvider
== ''openai'')'
status:
description: VirtualMCPServerStatus defines the observed state of VirtualMCPServer
properties:
Expand Down Expand Up @@ -4883,6 +4917,34 @@ spec:
instead of all backend tools directly. This reduces token usage by allowing
LLMs to discover relevant tools on demand rather than receiving all tool definitions.
properties:
embeddingModel:
description: |-
EmbeddingModel is the model name requested from the embedding service
(e.g. "text-embedding-3-small"). Required when EmbeddingProvider is
"openai". Ignored for the "tei" provider, where the model is fixed by the
running TEI container.

The API key for an OpenAI-compatible service is not configured here: it is
read from the OPENAI_API_KEY environment variable so the secret never
lands in a CRD spec or ConfigMap. An empty key omits the Authorization
header, which supports keyless in-cluster gateways.
type: string
embeddingProvider:
default: tei
description: |-
EmbeddingProvider selects the wire protocol used to talk to the embedding
service. "tei" speaks the HuggingFace Text Embeddings Inference API;
"openai" speaks the OpenAI-compatible /embeddings API, which lets the
optimizer use OpenAI, Azure OpenAI, or another OpenAI-compatible gateway.
Defaults to "tei" when empty.

The "openai" provider reads EmbeddingService directly and cannot be combined
with EmbeddingServerRef, which provisions a managed TEI server; the operator
rejects that combination at admission.
enum:
- tei
- openai
type: string
embeddingService:
description: |-
EmbeddingService is the full base URL of the embedding service endpoint
Expand Down Expand Up @@ -6027,6 +6089,12 @@ spec:
rule: '!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools)
|| self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth)
&& self.incomingAuth.type == ''oidc'')'
- message: embeddingServerRef provisions a managed TEI server and cannot
be combined with optimizer.embeddingProvider 'openai'; openai mode
uses embeddingService directly
rule: '!(has(self.embeddingServerRef) && has(self.config) && has(self.config.optimizer)
&& has(self.config.optimizer.embeddingProvider) && self.config.optimizer.embeddingProvider
== ''openai'')'
status:
description: VirtualMCPServerStatus defines the observed state of VirtualMCPServer
properties:
Expand Down
2 changes: 2 additions & 0 deletions docs/operator/crd-api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading