Skip to content
Draft
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 .github/workflows/pr_style_check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ jobs:
refactor
mcp
aigw
dynamic_module
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ build.%: ## Build a binary for the given command under the internal/cmd director
build: ## Build all binaries under cmd/ directory.
@$(foreach COMMAND_NAME,$(COMMANDS),$(MAKE) build.$(COMMAND_NAME);)

# This builds the dynamic module filter for Envoy. This is the shared library that can be loaded by Envoy to run the AI Gateway filter.
.PHONE: build-dm
build-dm: ## Build the dynamic module for Envoy.
CGO_ENABLED=1 go build -tags "envoy_1.36" -buildmode=c-shared -o $(OUTPUT_DIR)/libaigateway.so ./cmd/dynamic_module

# This builds the docker images for the controller, extproc and testupstream for the e2e tests.
.PHONY: build-e2e
build-e2e: ## Build the docker images for the controller, extproc and testupstream for the e2e tests.
Expand Down
127 changes: 127 additions & 0 deletions cmd/dynamic_module/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright Envoy AI Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

package main

import (
"context"
"fmt"
"log/slog"
"os"
"time"

"github.com/envoyproxy/ai-gateway/internal/tracing"
"github.com/prometheus/client_golang/prometheus"
otelprom "go.opentelemetry.io/otel/exporters/prometheus"

"github.com/envoyproxy/ai-gateway/internal/backendauth"
"github.com/envoyproxy/ai-gateway/internal/dynamicmodule"
"github.com/envoyproxy/ai-gateway/internal/dynamicmodule/sdk"
"github.com/envoyproxy/ai-gateway/internal/filterapi"
"github.com/envoyproxy/ai-gateway/internal/internalapi"
"github.com/envoyproxy/ai-gateway/internal/metrics"
)

func main() {} // This must be present to make a shared library.

// Set the envoy.NewHTTPFilter function to create a new http filter.
func init() {
g := &globalState{}
if err := g.initializeEnv(); err != nil {
panic("failed to create env config: " + err.Error())
}

// TODO: use a writer implemented with the Logger ABI of Envoy.
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug, // Adjust log level from environment variable if needed.
}))
if err := filterapi.StartConfigWatcher(context.Background(),
os.Getenv("AI_GATEWAY_DYNAMIC_MODULE_FILTER_CONFIG_PATH"), g, logger, time.Second*5); err != nil {
panic("failed to start filter config watcher: " + err.Error())
}
sdk.NewHTTPFilterConfig = g.newHTTPFilterConfig
}

// globalState implements [filterapi.ConfigReceiver] to load filter configuration.
type globalState struct {
fc *filterapi.RuntimeConfig
env *dynamicmodule.Env
}

// newHTTPFilterConfig creates a new http filter based on the config.
//
// `config` is the configuration string that is specified in the Envoy configuration.
func (g *globalState) newHTTPFilterConfig(name string, _ []byte) sdk.HTTPFilterConfig {
switch name {
case "ai_gateway.router":
return dynamicmodule.NewRouterFilterConfig(g.env, &g.fc)
case "ai_gateway.upstream":
return dynamicmodule.NewUpstreamFilterConfig(g.env)
default:
panic("unknown filter: " + name)
}
}

// LoadConfig implements [filterapi.ConfigReceiver.LoadConfig].
func (g *globalState) LoadConfig(ctx context.Context, config *filterapi.Config) error {
newConfig, err := filterapi.NewRuntimeConfig(ctx, config, backendauth.NewHandler)
if err != nil {
return fmt.Errorf("cannot create runtime filter config: %w", err)
}
g.fc = newConfig // This is racy but we don't care.
return nil
}

func (g *globalState) initializeEnv() error {
ctx := context.Background()
promRegistry := prometheus.NewRegistry()
promReader, err := otelprom.New(otelprom.WithRegisterer(promRegistry))
if err != nil {
return fmt.Errorf("failed to create prometheus reader: %w", err)
}

meter, _, err := metrics.NewMeterFromEnv(ctx, os.Stdout, promReader)
if err != nil {
return fmt.Errorf("failed to create metrics: %w", err)
}

endpointPrefixes, err := internalapi.ParseEndpointPrefixes(os.Getenv(
"AI_GATEWAY_DYNAMIC_MODULE_FILTER_ENDPOINT_PREFIXES",
))
if err != nil {
return fmt.Errorf("failed to parse endpoint prefixes: %w", err)
}

metricsRequestHeaderAttributes, err := internalapi.ParseRequestHeaderAttributeMapping(os.Getenv(
"AI_GATEWAY_DYNAMIC_MODULE_FILTER_METRICS_REQUEST_HEADER_ATTRIBUTES",
))
if err != nil {
return fmt.Errorf("failed to parse metrics header mapping: %w", err)
}
spanRequestHeaderAttributes, err := internalapi.ParseRequestHeaderAttributeMapping(os.Getenv(
"AI_GATEWAY_DYNAMIC_MODULE_FILTER_TRACING_REQUEST_HEADER_ATTRIBUTES",
))
if err != nil {
return fmt.Errorf("failed to parse tracing header mapping: %w", err)
}

tracing, err := tracing.NewTracingFromEnv(ctx, os.Stdout, spanRequestHeaderAttributes)
if err != nil {
return err
}

g.env = &dynamicmodule.Env{
RootPrefix: os.Getenv("AI_GATEWAY_DYNAMIC_MODULE_ROOT_PREFIX"),
EndpointPrefixes: endpointPrefixes,
ChatCompletionMetricsFactory: metrics.NewMetricsFactory(meter, metricsRequestHeaderAttributes, metrics.GenAIOperationChat),
MessagesMetricsFactory: metrics.NewMetricsFactory(meter, metricsRequestHeaderAttributes, metrics.GenAIOperationMessages),
CompletionMetricsFactory: metrics.NewMetricsFactory(meter, metricsRequestHeaderAttributes, metrics.GenAIOperationCompletion),
EmbeddingsMetricsFactory: metrics.NewMetricsFactory(meter, metricsRequestHeaderAttributes, metrics.GenAIOperationEmbedding),
ImageGenerationMetricsFactory: metrics.NewMetricsFactory(meter, metricsRequestHeaderAttributes, metrics.GenAIOperationImageGeneration),
RerankMetricsFactory: metrics.NewMetricsFactory(meter, metricsRequestHeaderAttributes, metrics.GenAIOperationRerank),
Tracing: tracing,
}
return nil
}
45 changes: 45 additions & 0 deletions internal/dynamicmodule/dynamic_module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright Envoy AI Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

package dynamicmodule

import (
"github.com/envoyproxy/ai-gateway/internal/internalapi"
"github.com/envoyproxy/ai-gateway/internal/metrics"
tracing "github.com/envoyproxy/ai-gateway/internal/tracing/api"
)

// endpoint represents the type of the endpoint that the request is targeting.
type endpoint int

const (
// chatCompletionsEndpoint represents the /v1/chat/completions endpoint.
chatCompletionsEndpoint endpoint = iota
// completionsEndpoint represents the /v1/completions endpoint.
completionsEndpoint
// embeddingsEndpoint represents the /v1/embeddings endpoint.
embeddingsEndpoint
// imagesGenerationsEndpoint represents the /v1/images/generations endpoint.
imagesGenerationsEndpoint
// rerankEndpoint represents the /v2/rerank endpoint of cohere.
rerankEndpoint
// messagesEndpoint represents the /v1/messages endpoint of anthropic.
messagesEndpoint
// modelsEndpoint represents the /v1/models endpoint.
modelsEndpoint
)

// Env holds the environment configuration for the dynamic module that is process-wide.
type Env struct {
RootPrefix string
EndpointPrefixes internalapi.EndpointPrefixes
ChatCompletionMetricsFactory,
MessagesMetricsFactory,
CompletionMetricsFactory,
EmbeddingsMetricsFactory,
ImageGenerationMetricsFactory,
RerankMetricsFactory metrics.Factory
Tracing tracing.Tracing
}
Loading
Loading