Repository: envoyproxy/ai-gateway
Pull request #352 (2025-02-17 10:34:01)

extproc: decouple router package from api paths
**Commit Message**

extproc: decouple router package from api paths

This is a follow-up change to decouple the router package from the API paths. Now that we have specialized processors per API path it does not make sense for the router package to have path-related logic.
Now each processor is responsible for instantiating the right body parser for the path they're processing.

**Related Issues/PRs (if applicable)**

Follow-up for https://github.com/envoyproxy/ai-gateway/pull/325 and https://github.com/envoyproxy/ai-gateway/pull/334

**Special notes for reviewers (if applicable)**

N/A


Patch:
diff --git a/internal/extproc/chatcompletion_processor.go b/internal/extproc/chatcompletion_processor.go
index d5ca921..21717a9 100644
--- a/internal/extproc/chatcompletion_processor.go
+++ b/internal/extproc/chatcompletion_processor.go
@@ -9,6 +9,7 @@ import (
 	"bytes"
 	"compress/gzip"
 	"context"
+	"encoding/json"
 	"fmt"
 	"io"
 	"log/slog"
@@ -18,17 +19,21 @@ import (
 	"google.golang.org/protobuf/types/known/structpb"
 
 	"github.com/envoyproxy/ai-gateway/filterapi"
+	"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
 	"github.com/envoyproxy/ai-gateway/internal/extproc/translator"
 	"github.com/envoyproxy/ai-gateway/internal/llmcostcel"
 )
 
 // NewChatCompletionProcessor implements [Processor] for the /chat/completions endpoint.
-func NewChatCompletionProcessor(config *processorConfig, requestHeaders map[string]string, logger *slog.Logger) Processor {
+func NewChatCompletionProcessor(config *processorConfig, requestHeaders map[string]string, logger *slog.Logger) (Processor, error) {
+	if config.schema.Name != filterapi.APISchemaOpenAI {
+		return nil, fmt.Errorf("unsupported API schema: %s", config.schema.Name)
+	}
 	return &chatCompletionProcessor{
 		config:         config,
 		requestHeaders: requestHeaders,
 		logger:         logger,
-	}
+	}, nil
 }
 
 // chatCompletionProcessor handles the processing of the request and response messages for a single stream.
@@ -70,12 +75,11 @@ func (c *chatCompletionProcessor) ProcessRequestHeaders(_ context.Context, _ *co
 
 // ProcessRequestBody implements [Processor.ProcessRequestBody].
 func (c *chatCompletionProcessor) ProcessRequestBody(ctx context.Context, rawBody *extprocv3.HttpBody) (res *extprocv3.ProcessingResponse, err error) {
-	path := c.requestHeaders[":path"]
-	model, body, err := c.config.bodyParser(path, rawBody)
+	model, body, err := parseOpenAIChatCompletionBody(rawBody)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse request body: %w", err)
 	}
-	c.logger.Info("Processing request", "path", path, "model", model)
+	c.logger.Info("Processing request", "path", c.requestHeaders[":path"], "model", model)
 
 	c.requestHeaders[c.config.modelNameHeaderKey] = model
 	b, err := c.config.router.Calculate(c.requestHeaders)
@@ -195,6 +199,14 @@ func (c *chatCompletionProcessor) ProcessResponseBody(_ context.Context, body *e
 	return resp, nil
 }
 
+func parseOpenAIChatCompletionBody(body *extprocv3.HttpBody) (modelName string, rb translator.RequestBody, err error) {
+	var openAIReq openai.ChatCompletionRequest
+	if err := json.Unmarshal(body.Body, &openAIReq); err != nil {
+		return "", nil, fmt.Errorf("failed to unmarshal body: %w", err)
+	}
+	return openAIReq.Model, &openAIReq, nil
+}
+
 func (c *chatCompletionProcessor) maybeBuildDynamicMetadata() (*structpb.Struct, error) {
 	metadata := make(map[string]*structpb.Value, len(c.config.requestCosts))
 	for i := range c.config.requestCosts {
diff --git a/internal/extproc/models_processor.go b/internal/extproc/models_processor.go
index b799fb7..dec35ff 100644
--- a/internal/extproc/models_processor.go
+++ b/internal/extproc/models_processor.go
@@ -34,7 +34,7 @@ type modelsProcessor struct {
 var _ Processor = (*modelsProcessor)(nil)
 
 // NewModelsProcessor creates a new processor that returns the list of declared models
-func NewModelsProcessor(config *processorConfig, _ map[string]string, logger *slog.Logger) Processor {
+func NewModelsProcessor(config *processorConfig, _ map[string]string, logger *slog.Logger) (Processor, error) {
 	models := openai.ModelList{
 		Object: "list",
 		Data:   make([]openai.Model, 0, len(config.declaredModels)),
@@ -47,7 +47,7 @@ func NewModelsProcessor(config *processorConfig, _ map[string]string, logger *sl
 			Created: openai.JSONUNIXTime(time.Now()), // TODO(nacx): does this really matter here?
 		})
 	}
-	return &modelsProcessor{logger: logger, models: models}
+	return &modelsProcessor{logger: logger, models: models}, nil
 }
 
 // ProcessRequestHeaders implements [Processor.ProcessRequestHeaders].
diff --git a/internal/extproc/processor.go b/internal/extproc/processor.go
index b36360d..ee3e67c 100644
--- a/internal/extproc/processor.go
+++ b/internal/extproc/processor.go
@@ -16,14 +16,13 @@ import (
 	"github.com/envoyproxy/ai-gateway/filterapi"
 	"github.com/envoyproxy/ai-gateway/filterapi/x"
 	"github.com/envoyproxy/ai-gateway/internal/extproc/backendauth"
-	"github.com/envoyproxy/ai-gateway/internal/extproc/router"
 )
 
 // processorConfig is the configuration for the processor.
 // This will be created by the server and passed to the processor when it detects a new configuration.
 type processorConfig struct {
 	uuid                                         string
-	bodyParser                                   router.RequestBodyParser
+	schema                                       filterapi.VersionedAPISchema
 	router                                       x.Router
 	modelNameHeaderKey, selectedBackendHeaderKey string
 	backendAuthHandlers                          map[string]backendauth.Handler
@@ -39,7 +38,7 @@ type processorConfigRequestCost struct {
 }
 
 // ProcessorFactory is the factory function used to create new instances of a processor.
-type ProcessorFactory func(*processorConfig, map[string]string, *slog.Logger) Processor
+type ProcessorFactory func(*processorConfig, map[string]string, *slog.Logger) (Processor, error)
 
 // Processor is the interface for the processor.
 // This decouples the processor implementation detail from the server implementation.
diff --git a/internal/extproc/router/request_body.go b/internal/extproc/router/request_body.go
deleted file mode 100644
index 25d05be..0000000
--- a/internal/extproc/router/request_body.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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 router
-
-import (
-	"encoding/json"
-	"fmt"
-
-	extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
-
-	"github.com/envoyproxy/ai-gateway/filterapi"
-	"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
-)
-
-// RequestBodyParser is a function that parses the body of the request.
-type RequestBodyParser func(path string, body *extprocv3.HttpBody) (modelName string, rb RequestBody, err error)
-
-// NewRequestBodyParser creates a new RequestBodyParser based on the schema.
-func NewRequestBodyParser(schema filterapi.VersionedAPISchema) (RequestBodyParser, error) {
-	if schema.Name == filterapi.APISchemaOpenAI {
-		return openAIParseBody, nil
-	}
-	return nil, fmt.Errorf("unsupported API schema: %s", schema)
-}
-
-// RequestBody is the union of all request body types.
-type RequestBody any
-
-// openAIParseBody parses the body of the request for OpenAI.
-func openAIParseBody(path string, body *extprocv3.HttpBody) (modelName string, rb RequestBody, err error) {
-	if path == "/v1/chat/completions" {
-		var openAIReq openai.ChatCompletionRequest
-		if err := json.Unmarshal(body.Body, &openAIReq); err != nil {
-			return "", nil, fmt.Errorf("failed to unmarshal body: %w", err)
-		}
-		return openAIReq.Model, &openAIReq, nil
-	}
-	return "", nil, fmt.Errorf("unsupported path: %s", path)
-}
diff --git a/internal/extproc/router/router.go b/internal/extproc/router/router.go
index d48ec09..d0507cf 100644
--- a/internal/extproc/router/router.go
+++ b/internal/extproc/router/router.go
@@ -15,14 +15,14 @@ import (
 	"github.com/envoyproxy/ai-gateway/filterapi/x"
 )
 
-// router implements [filterapi.Router].
+// router implements [x.Router].
 type router struct {
 	rules []filterapi.RouteRule
 	rng   *rand.Rand
 }
 
-// NewRouter creates a new [filterapi.Router] implementation for the given config.
-func NewRouter(config *filterapi.Config, newCustomFn x.NewCustomRouterFn) (x.Router, error) {
+// New creates a new [x.Router] implementation for the given config.
+func New(config *filterapi.Config, newCustomFn x.NewCustomRouterFn) (x.Router, error) {
 	r := &router{rules: config.Rules, rng: rand.New(rand.NewSource(uint64(time.Now().UnixNano())))} //nolint:gosec
 	if newCustomFn != nil {
 		customRouter := newCustomFn(r, config)
@@ -31,7 +31,7 @@ func NewRouter(config *filterapi.Config, newCustomFn x.NewCustomRouterFn) (x.Rou
 	return r, nil
 }
 
-// Calculate implements [filterapi.Router.Calculate].
+// Calculate implements [x.Router.Calculate].
 func (r *router) Calculate(headers map[string]string) (backend *filterapi.Backend, err error) {
 	var rule *filterapi.RouteRule
 	for i := range r.rules {
diff --git a/internal/extproc/server.go b/internal/extproc/server.go
index 874c4d5..bd53ae7 100644
--- a/internal/extproc/server.go
+++ b/internal/extproc/server.go
@@ -54,11 +54,7 @@ func NewServer(logger *slog.Logger) (*Server, error) {
 
 // LoadConfig updates the configuration of the external processor.
 func (s *Server) LoadConfig(ctx context.Context, config *filterapi.Config) error {
-	bodyParser, err := router.NewRequestBodyParser(config.Schema)
-	if err != nil {
-		return fmt.Errorf("cannot create request body parser: %w", err)
-	}
-	rt, err := router.NewRouter(config, x.NewCustomRouter)
+	rt, err := router.New(config, x.NewCustomRouter)
 	if err != nil {
 		return fmt.Errorf("cannot create router: %w", err)
 	}
@@ -104,8 +100,9 @@ func (s *Server) LoadConfig(ctx context.Context, config *filterapi.Config) error
 	}
 
 	newConfig := &processorConfig{
-		uuid:       config.UUID,
-		bodyParser: bodyParser, router: rt,
+		uuid:                     config.UUID,
+		schema:                   config.Schema,
+		router:                   rt,
 		selectedBackendHeaderKey: config.SelectedBackendHeaderKey,
 		modelNameHeaderKey:       config.ModelNameHeaderKey,
 		backendAuthHandlers:      backendAuthHandlers,
@@ -130,7 +127,7 @@ func (s *Server) processorForPath(requestHeaders map[string]string) (Processor,
 	if !ok {
 		return nil, fmt.Errorf("no processor defined for path: %v", path)
 	}
-	return newProcessor(s.config, requestHeaders, s.logger), nil
+	return newProcessor(s.config, requestHeaders, s.logger)
 }
 
 // Process implements [extprocv3.ExternalProcessorServer].
diff --git a/internal/extproc/translator/openai_awsbedrock.go b/internal/extproc/translator/openai_awsbedrock.go
index 5e562cb..aa5ed7c 100644
--- a/internal/extproc/translator/openai_awsbedrock.go
+++ b/internal/extproc/translator/openai_awsbedrock.go
@@ -23,7 +23,6 @@ import (
 
 	"github.com/envoyproxy/ai-gateway/internal/apischema/awsbedrock"
 	"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
-	"github.com/envoyproxy/ai-gateway/internal/extproc/router"
 )
 
 // NewChatCompletionOpenAIToAWSBedrockTranslator implements [Factory] for OpenAI to AWS Bedrock translation.
@@ -42,7 +41,7 @@ type openAIToAWSBedrockTranslatorV1ChatCompletion struct {
 }
 
 // RequestBody implements [Translator.RequestBody].
-func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) RequestBody(body router.RequestBody) (
+func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) RequestBody(body RequestBody) (
 	headerMutation *extprocv3.HeaderMutation, bodyMutation *extprocv3.BodyMutation, override *extprocv3http.ProcessingMode, err error,
 ) {
 	openAIReq, ok := body.(*openai.ChatCompletionRequest)
diff --git a/internal/extproc/translator/openai_openai.go b/internal/extproc/translator/openai_openai.go
index 3d0486b..62c110b 100644
--- a/internal/extproc/translator/openai_openai.go
+++ b/internal/extproc/translator/openai_openai.go
@@ -16,7 +16,6 @@ import (
 	extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
 
 	"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
-	"github.com/envoyproxy/ai-gateway/internal/extproc/router"
 )
 
 // NewChatCompletionOpenAIToOpenAITranslator implements [Factory] for OpenAI to OpenAI translation.
@@ -32,7 +31,7 @@ type openAIToOpenAITranslatorV1ChatCompletion struct {
 }
 
 // RequestBody implements [Translator.RequestBody].
-func (o *openAIToOpenAITranslatorV1ChatCompletion) RequestBody(body router.RequestBody) (
+func (o *openAIToOpenAITranslatorV1ChatCompletion) RequestBody(body RequestBody) (
 	headerMutation *extprocv3.HeaderMutation, bodyMutation *extprocv3.BodyMutation, override *extprocv3http.ProcessingMode, err error,
 ) {
 	req, ok := body.(*openai.ChatCompletionRequest)
diff --git a/internal/extproc/translator/translator.go b/internal/extproc/translator/translator.go
index 8371e1e..09f872e 100644
--- a/internal/extproc/translator/translator.go
+++ b/internal/extproc/translator/translator.go
@@ -12,8 +12,6 @@ import (
 	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	extprocv3http "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_proc/v3"
 	extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
-
-	"github.com/envoyproxy/ai-gateway/internal/extproc/router"
 )
 
 var (
@@ -32,6 +30,9 @@ func isGoodStatusCode(code int) bool {
 	return code >= 200 && code < 300
 }
 
+// RequestBody is the union of all request body types. TODO: maybe we should just define Translator interface per endpoint.
+type RequestBody any
+
 // Translator translates the request and response messages between the client and the backend API schemas for a specific path.
 // The implementation can embed [defaultTranslator] to avoid implementing all methods.
 //
@@ -43,7 +44,7 @@ type Translator interface {
 	// 	- `body` is the request body already parsed by [router.RequestBodyParser]. The concrete type is specific to the schema and the path.
 	//	- This returns `headerMutation` and `bodyMutation` that can be nil to indicate no mutation.
 	//  - This returns `override` that to change the processing mode. This is used to process streaming requests properly.
-	RequestBody(body router.RequestBody) (
+	RequestBody(body RequestBody) (
 		headerMutation *extprocv3.HeaderMutation,
 		bodyMutation *extprocv3.BodyMutation,
 		override *extprocv3http.ProcessingMode,
diff --git a/netlify.toml b/netlify.toml
index a1196c7..6254355 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -23,6 +23,7 @@ command = "npm run build"
     Strict-Transport-Security = "max-age=31536000; includeSubDomains"
     Content-Security-Policy = """
       default-src 'self';
+      frame-src 'self' https://www.youtube.com https://app.netlify.com;
       script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com;
       style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
       img-src 'self' data: https: blob:;
diff --git a/site/blog/2024-10-18-introducing-envoy-ai-gw.md b/site/blog/2024-10-18-introducing-envoy-ai-gw.md
index dcf9bf5..496d109 100644
--- a/site/blog/2024-10-18-introducing-envoy-ai-gw.md
+++ b/site/blog/2024-10-18-introducing-envoy-ai-gw.md
@@ -1,16 +1,15 @@
 ---
 slug: introducing-envoy-ai-gateway
 title: Introducing Envoy AI Gateway
+description: Open collaboration to bring AI Gateway features to the Envoy community
 authors: [missberg]
 tags: [news]
 ---
 
-Open collaboration to bring AI Gateway features to the Envoy community
+**The industry is embracing Generative AI functionality, and we need to evolve how we handle traffic on an industry-wide scale. Keeping AI traffic handling features exclusive to enterprise licenses is counterproductive to the industry’s needs. This approach limits incentives to a single commercial entity and its customers. Even single-company open-source initiatives do not promote open multi-company collaboration.**
 
 <!-- truncate -->
 
-The industry is embracing Generative AI functionality, and we need to evolve how we handle traffic on an industry-wide scale. Keeping AI traffic handling features exclusive to enterprise licenses is counterproductive to the industry’s needs. This approach limits incentives to a single commercial entity and its customers. Even single-company open-source initiatives do not promote open multi-company collaboration.
-
 A shared challenge like this presents an opportunity for open collaboration to build the necessary features. We believe bringing together different use cases and requirements through open collaboration will lead to better solutions and accelerate innovation. The industry will benefit from diverse expertise and experiences by openly collaborating on software across companies and industries.
 
 That is why Tetrate and Bloomberg have started an open collaboration to bring critical features for this new era of Gen AI integration. Collaborating openly in the Envoy community, bringing AI traffic handling features to Envoy, via Envoy Gateway and Envoy Proxy.
diff --git a/site/blog/2024-11-14-kubecon-end-user-keynote.md b/site/blog/2024-11-14-kubecon-end-user-keynote.md
new file mode 100644
index 0000000..f1dec26
--- /dev/null
+++ b/site/blog/2024-11-14-kubecon-end-user-keynote.md
@@ -0,0 +1,20 @@
+---
+slug: kubecon-end-user-keynote-2024
+title: End User Keynote at KubeCon 2024
+authors: [missberg, alexagriffith]
+tags: [news, presentations]
+---
+
+At KubeCon North America 2024, Alexa Griffith had the opportunity to present the End User Keynote on **Centralizing & Simplifying Enterprise AI Workflows with Envoy AI Gateway**.
+
+<iframe width="560" height="315" src="https://www.youtube.com/embed/do1viOk8nok?si=a1nn7sbummXaYPXl" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
+
+<!-- truncate -->
+
+## About the presentation
+
+As Generative AI reshapes the industry, the demands on AI platforms have rapidly evolved. Organizations now require centralized infrastructure to manage and optimize access to self-trained, open source, and commercial AI models at scale.
+
+In this talk, we introduce the Envoy AI Gateway, a collaborative open source effort led by engineers from [Bloomberg](https://www.bloomberg.com/company/what-we-do/engineering-cto/) and [Tetrate](https://tetrate.io).
+
+Learn how the Envoy AI Gateway, which is built atop Envoy Gateway and Envoy Proxy, provides a unified, scalable solution for model access, usage limiting, and upstream authorization.
diff --git a/site/blog/authors.yml b/site/blog/authors.yml
index 18ca5b3..01a8284 100644
--- a/site/blog/authors.yml
+++ b/site/blog/authors.yml
@@ -6,3 +6,13 @@ missberg:
   page: true
   socials:
     github: missBerg
+    linkedin: https://www.linkedin.com/in/ericahughberg/
+alexagriffith:
+  name: Alexa Griffith
+  title: Senior Software Engineer, Bloomberg
+  url: https://github.com/alexagriffith
+  image_url: https://github.com/alexagriffith.png
+  page: true
+  socials:
+    github: alexagriffith
+    linkedin: https://www.linkedin.com/in/alexagriffith/
diff --git a/site/blog/tags.yml b/site/blog/tags.yml
index 33c0dd0..0856f0f 100644
--- a/site/blog/tags.yml
+++ b/site/blog/tags.yml
@@ -1,4 +1,8 @@
-envoy:
+news:
   label: News
   permalink: /news
   description: Project News
+presentations:
+  label: Presentations
+  permalink: /presentations
+  description: Project Presentations
diff --git a/site/docs/getting-started/basic-usage.md b/site/docs/getting-started/basic-usage.md
index 5dd9f70..4b21664 100644
--- a/site/docs/getting-started/basic-usage.md
+++ b/site/docs/getting-started/basic-usage.md
@@ -136,8 +136,8 @@ For real AI model responses, see the [Connect Providers](./connect-providers) gu
 ### Understanding the Response Format
 
 The basic setup includes a mock backend that demonstrates the API structure but doesn't provide real AI responses. The response format follows the standard chat completion format with:
-- A `completions` array containing responses
-- Each response having a `role` and `content`
+- A `choices` array containing responses
+- Each message having a `role` and `content`
 
 ## Next Steps
 
diff --git a/site/docusaurus.config.ts b/site/docusaurus.config.ts
index 54fd980..759e13b 100644
--- a/site/docusaurus.config.ts
+++ b/site/docusaurus.config.ts
@@ -48,6 +48,7 @@ const config: Config = {
           ],
         },
         blog: {
+          path: 'blog',
           showReadingTime: true,
           feedOptions: {
             type: ['rss', 'atom'],
diff --git a/site/src/components/HomepageFeatures/styles.module.css b/site/src/components/HomepageFeatures/styles.module.css
index fa4f367..683d044 100644
--- a/site/src/components/HomepageFeatures/styles.module.css
+++ b/site/src/components/HomepageFeatures/styles.module.css
@@ -6,6 +6,7 @@
 }
 
 .featureSvg {
-  max-height: 150px;
-  max-width: 150px;
+  max-height: 120px;
+  max-width: 120px;
+  margin-bottom: 1.5rem;
 }
diff --git a/site/src/css/custom.css b/site/src/css/custom.css
index bb31519..c41d31e 100644
--- a/site/src/css/custom.css
+++ b/site/src/css/custom.css
@@ -70,6 +70,67 @@
   .heroImage{
     max-height: 30vh;
   }
+  .buttons{
+    display: flex;
+    justify-content: center;
+    gap: 1.5rem;
+    margin-top: 3rem;
+    .button{
+      background: linear-gradient(
+        135deg,
+        rgba(255, 255, 255, 0.1) 0%,
+        rgba(255, 20, 147, 0.1) 100%
+      );
+      backdrop-filter: blur(10px);
+      color: #fff;
+      border: 1px solid rgba(255, 255, 255, 0.2);
+      border-radius: 25px;
+      padding: 0.8rem 2rem;
+      font-weight: 600;
+      letter-spacing: 0.5px;
+      transition: all 0.3s ease;
+      text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+      box-shadow: 0 4px 15px rgba(108, 24, 153, 0.2);
+      position: relative;
+      overflow: hidden;
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: linear-gradient(
+          90deg,
+          rgba(255, 119, 0, 0.1) 0%,
+          rgba(255, 255, 255, 0.1) 100%
+        );
+        opacity: 0;
+        transition: opacity 0.3s ease;
+      }
+
+      &:hover {
+        background: linear-gradient(
+          90deg,
+          rgba(255, 255, 255, 0.15) 0%,
+          rgba(255, 20, 147, 0.15) 100%
+        );
+        border-color: rgba(255, 255, 255, 0.4);
+        transform: translateY(-2px);
+        box-shadow: 0 6px 20px rgba(108, 24, 153, 0.3);
+
+        &::before {
+          opacity: 1;
+        }
+      }
+
+      &:active {
+        transform: translateY(0);
+        box-shadow: 0 2px 10px rgba(108, 24, 153, 0.2);
+      }
+    }
+  }
 }
 
   @media screen and (max-width: 996px) {
@@ -121,7 +182,6 @@
 .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 {
   color: var(--custom-heading-color);
   font-weight: 700;
-  margin-top: 1em;
   margin-bottom: 1em;
 }
 
diff --git a/site/src/pages/index.tsx b/site/src/pages/index.tsx
index a721f16..ba104c3 100644
--- a/site/src/pages/index.tsx
+++ b/site/src/pages/index.tsx
@@ -15,8 +15,13 @@ function HomepageHeader() {
         <div className="buttons">
           <Link
             className="button button--secondary button--lg"
-            href="https://docs.google.com/document/d/10e1sfsF-3G3Du5nBHGmLjXw5GVMqqCvFDqp_O65B0_w/edit?pli=1&tab=t.0">
-            Join the community
+            href="/docs/getting-started/">
+            Get Started
+          </Link>
+          <Link
+            className="button button--secondary button--lg"
+            href="https://github.com/envoyproxy/ai-gateway">
+            View on GitHub
           </Link>
         </div>
       </div>


Test patch:
diff --git a/internal/extproc/chatcompletion_processor_test.go b/internal/extproc/chatcompletion_processor_test.go
index a0da65f..2843e21 100644
--- a/internal/extproc/chatcompletion_processor_test.go
+++ b/internal/extproc/chatcompletion_processor_test.go
@@ -6,6 +6,7 @@
 package extproc
 
 import (
+	"encoding/json"
 	"errors"
 	"io"
 	"log/slog"
@@ -16,11 +17,24 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/envoyproxy/ai-gateway/filterapi"
-	"github.com/envoyproxy/ai-gateway/internal/extproc/router"
+	"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
 	"github.com/envoyproxy/ai-gateway/internal/extproc/translator"
 	"github.com/envoyproxy/ai-gateway/internal/llmcostcel"
 )
 
+func TestChatCompletion_Scheema(t *testing.T) {
+	t.Run("unsupported", func(t *testing.T) {
+		cfg := &processorConfig{schema: filterapi.VersionedAPISchema{Name: "Foo", Version: "v123"}}
+		_, err := NewChatCompletionProcessor(cfg, nil, nil)
+		require.ErrorContains(t, err, "unsupported API schema: Foo")
+	})
+	t.Run("supported openai", func(t *testing.T) {
+		cfg := &processorConfig{schema: filterapi.VersionedAPISchema{Name: filterapi.APISchemaOpenAI, Version: "v123"}}
+		_, err := NewChatCompletionProcessor(cfg, nil, nil)
+		require.NoError(t, err)
+	})
+}
+
 func TestChatCompletion_SelectTranslator(t *testing.T) {
 	c := &chatCompletionProcessor{}
 	t.Run("unsupported", func(t *testing.T) {
@@ -128,64 +142,71 @@ func TestChatCompletion_ProcessResponseBody(t *testing.T) {
 }
 
 func TestChatCompletion_ProcessRequestBody(t *testing.T) {
+	bodyFromModel := func(t *testing.T, model string) []byte {
+		var openAIReq openai.ChatCompletionRequest
+		openAIReq.Model = model
+		bytes, err := json.Marshal(openAIReq)
+		require.NoError(t, err)
+		return bytes
+	}
 	t.Run("body parser error", func(t *testing.T) {
-		rbp := mockRequestBodyParser{t: t, retErr: errors.New("test error")}
-		p := &chatCompletionProcessor{config: &processorConfig{bodyParser: rbp.impl}}
-		_, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{})
-		require.ErrorContains(t, err, "failed to parse request body: test error")
+		p := &chatCompletionProcessor{}
+		_, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{Body: []byte("nonjson")})
+		require.ErrorContains(t, err, "invalid character 'o' in literal null")
 	})
 	t.Run("router error", func(t *testing.T) {
 		headers := map[string]string{":path": "/foo"}
-		rbp := mockRequestBodyParser{t: t, retModelName: "some-model", expPath: "/foo"}
 		rt := mockRouter{t: t, expHeaders: headers, retErr: errors.New("test error")}
-		p := &chatCompletionProcessor{config: &processorConfig{bodyParser: rbp.impl, router: rt}, requestHeaders: headers, logger: slog.Default()}
-		_, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{})
+		p := &chatCompletionProcessor{config: &processorConfig{router: rt}, requestHeaders: headers, logger: slog.Default()}
+		_, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{Body: bodyFromModel(t, "some-model")})
 		require.ErrorContains(t, err, "failed to calculate route: test error")
 	})
 	t.Run("translator not found", func(t *testing.T) {
 		headers := map[string]string{":path": "/foo"}
-		rbp := mockRequestBodyParser{t: t, retModelName: "some-model", expPath: "/foo"}
 		rt := mockRouter{
 			t: t, expHeaders: headers, retBackendName: "some-backend",
 			retVersionedAPISchema: filterapi.VersionedAPISchema{Name: "some-schema", Version: "v10.0"},
 		}
-		p := &chatCompletionProcessor{config: &processorConfig{
-			bodyParser: rbp.impl, router: rt,
-		}, requestHeaders: headers, logger: slog.Default()}
-		_, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{})
+		p := &chatCompletionProcessor{config: &processorConfig{router: rt}, requestHeaders: headers, logger: slog.Default()}
+		_, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{Body: bodyFromModel(t, "some-model")})
 		require.ErrorContains(t, err, "unsupported API schema: backend={some-schema v10.0}")
 	})
 	t.Run("translator error", func(t *testing.T) {
 		headers := map[string]string{":path": "/foo"}
-		rbp := mockRequestBodyParser{t: t, retModelName: "some-model", expPath: "/foo"}
+		someBody := bodyFromModel(t, "some-model")
 		rt := mockRouter{
 			t: t, expHeaders: headers, retBackendName: "some-backend",
 			retVersionedAPISchema: filterapi.VersionedAPISchema{Name: "some-schema", Version: "v10.0"},
 		}
-		tr := mockTranslator{t: t, retErr: errors.New("test error")}
-		p := &chatCompletionProcessor{config: &processorConfig{
-			bodyParser: rbp.impl, router: rt,
-		}, requestHeaders: headers, logger: slog.Default(), translator: tr}
-		_, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{})
+		var body openai.ChatCompletionRequest
+		require.NoError(t, json.Unmarshal(someBody, &body))
+		tr := mockTranslator{t: t, retErr: errors.New("test error"), expRequestBody: &body}
+		p := &chatCompletionProcessor{
+			config:         &processorConfig{router: rt},
+			requestHeaders: headers, logger: slog.Default(), translator: tr,
+		}
+		_, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{Body: someBody})
 		require.ErrorContains(t, err, "failed to transform request: test error")
 	})
 	t.Run("ok", func(t *testing.T) {
-		someBody := router.RequestBody("foooooooooooooo")
+		someBody := bodyFromModel(t, "some-model")
 		headers := map[string]string{":path": "/foo"}
-		rbp := mockRequestBodyParser{t: t, retModelName: "some-model", expPath: "/foo", retRb: someBody}
 		rt := mockRouter{
 			t: t, expHeaders: headers, retBackendName: "some-backend",
 			retVersionedAPISchema: filterapi.VersionedAPISchema{Name: "some-schema", Version: "v10.0"},
 		}
 		headerMut := &extprocv3.HeaderMutation{}
 		bodyMut := &extprocv3.BodyMutation{}
-		mt := mockTranslator{t: t, expRequestBody: someBody, retHeaderMutation: headerMut, retBodyMutation: bodyMut}
+
+		var expBody openai.ChatCompletionRequest
+		require.NoError(t, json.Unmarshal(someBody, &expBody))
+		mt := mockTranslator{t: t, expRequestBody: &expBody, retHeaderMutation: headerMut, retBodyMutation: bodyMut}
 		p := &chatCompletionProcessor{config: &processorConfig{
-			bodyParser: rbp.impl, router: rt,
+			router:                   rt,
 			selectedBackendHeaderKey: "x-ai-gateway-backend-key",
 			modelNameHeaderKey:       "x-ai-gateway-model-key",
 		}, requestHeaders: headers, logger: slog.Default(), translator: mt}
-		resp, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{})
+		resp, err := p.ProcessRequestBody(t.Context(), &extprocv3.HttpBody{Body: someBody})
 		require.NoError(t, err)
 		require.Equal(t, mt, p.translator)
 		require.NotNil(t, resp)
@@ -202,3 +223,22 @@ func TestChatCompletion_ProcessRequestBody(t *testing.T) {
 		require.Equal(t, "some-backend", string(hdrs[1].Header.RawValue))
 	})
 }
+
+func TestChatCompletion_ParseBody(t *testing.T) {
+	t.Run("ok", func(t *testing.T) {
+		original := openai.ChatCompletionRequest{Model: "llama3.3"}
+		bytes, err := json.Marshal(original)
+		require.NoError(t, err)
+
+		modelName, rb, err := parseOpenAIChatCompletionBody(&extprocv3.HttpBody{Body: bytes})
+		require.NoError(t, err)
+		require.Equal(t, "llama3.3", modelName)
+		require.NotNil(t, rb)
+	})
+	t.Run("error", func(t *testing.T) {
+		modelName, rb, err := parseOpenAIChatCompletionBody(&extprocv3.HttpBody{})
+		require.Error(t, err)
+		require.Equal(t, "", modelName)
+		require.Nil(t, rb)
+	})
+}
diff --git a/internal/extproc/mocks_test.go b/internal/extproc/mocks_test.go
index 0e064c6..482371c 100644
--- a/internal/extproc/mocks_test.go
+++ b/internal/extproc/mocks_test.go
@@ -19,7 +19,6 @@ import (
 
 	"github.com/envoyproxy/ai-gateway/filterapi"
 	"github.com/envoyproxy/ai-gateway/filterapi/x"
-	"github.com/envoyproxy/ai-gateway/internal/extproc/router"
 	"github.com/envoyproxy/ai-gateway/internal/extproc/translator"
 )
 
@@ -70,7 +69,7 @@ func (m mockProcessor) ProcessResponseBody(_ context.Context, body *extprocv3.Ht
 type mockTranslator struct {
 	t                 *testing.T
 	expHeaders        map[string]string
-	expRequestBody    router.RequestBody
+	expRequestBody    translator.RequestBody
 	expResponseBody   *extprocv3.HttpBody
 	retHeaderMutation *extprocv3.HeaderMutation
 	retBodyMutation   *extprocv3.BodyMutation
@@ -80,7 +79,7 @@ type mockTranslator struct {
 }
 
 // RequestBody implements [translator.Translator.RequestBody].
-func (m mockTranslator) RequestBody(body router.RequestBody) (headerMutation *extprocv3.HeaderMutation, bodyMutation *extprocv3.BodyMutation, override *extprocv3http.ProcessingMode, err error) {
+func (m mockTranslator) RequestBody(body translator.RequestBody) (headerMutation *extprocv3.HeaderMutation, bodyMutation *extprocv3.BodyMutation, override *extprocv3http.ProcessingMode, err error) {
 	require.Equal(m.t, m.expRequestBody, body)
 	return m.retHeaderMutation, m.retBodyMutation, m.retOverride, m.retErr
 }
@@ -127,23 +126,6 @@ func (m mockRouter) Calculate(headers map[string]string) (*filterapi.Backend, er
 	return b, m.retErr
 }
 
-// mockRequestBodyParser implements [router.RequestBodyParser] for testing.
-type mockRequestBodyParser struct {
-	t            *testing.T
-	expPath      string
-	expBody      []byte
-	retModelName string
-	retRb        router.RequestBody
-	retErr       error
-}
-
-// impl implements [router.RequestBodyParser].
-func (m *mockRequestBodyParser) impl(path string, body *extprocv3.HttpBody) (modelName string, rb router.RequestBody, err error) {
-	require.Equal(m.t, m.expPath, path)
-	require.Equal(m.t, m.expBody, body.Body)
-	return m.retModelName, m.retRb, m.retErr
-}
-
 // mockExternalProcessingStream implements [extprocv3.ExternalProcessor_ProcessServer] for testing.
 type mockExternalProcessingStream struct {
 	t                 *testing.T
diff --git a/internal/extproc/models_processor_test.go b/internal/extproc/models_processor_test.go
index 240c535..a36fc61 100644
--- a/internal/extproc/models_processor_test.go
+++ b/internal/extproc/models_processor_test.go
@@ -21,7 +21,8 @@ import (
 
 func TestModels_ProcessRequestHeaders(t *testing.T) {
 	cfg := &processorConfig{declaredModels: []string{"openai", "aws-bedrock"}}
-	p := NewModelsProcessor(cfg, nil, slog.Default())
+	p, err := NewModelsProcessor(cfg, nil, slog.Default())
+	require.NoError(t, err)
 	res, err := p.ProcessRequestHeaders(t.Context(), &corev3.HeaderMap{
 		Headers: []*corev3.HeaderValue{{Key: "foo", Value: "bar"}},
 	})
diff --git a/internal/extproc/router/request_body_test.go b/internal/extproc/router/request_body_test.go
deleted file mode 100644
index 00cda58..0000000
--- a/internal/extproc/router/request_body_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-// 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 router
-
-import (
-	"encoding/json"
-	"testing"
-
-	extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
-	"github.com/stretchr/testify/require"
-
-	"github.com/envoyproxy/ai-gateway/filterapi"
-	"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
-)
-
-func TestNewRequestBodyParser(t *testing.T) {
-	t.Run("ok", func(t *testing.T) {
-		res, err := NewRequestBodyParser(filterapi.VersionedAPISchema{Name: filterapi.APISchemaOpenAI})
-		require.NotNil(t, res)
-		require.NoError(t, err)
-	})
-	t.Run("error", func(t *testing.T) {
-		res, err := NewRequestBodyParser(filterapi.VersionedAPISchema{Name: "foo"})
-		require.Nil(t, res)
-		require.Error(t, err)
-	})
-}
-
-func Test_openAIParseBody(t *testing.T) {
-	t.Run("/v1/chat/completions", func(t *testing.T) {
-		t.Run("ok", func(t *testing.T) {
-			original := openai.ChatCompletionRequest{Model: "llama3.3"}
-			bytes, err := json.Marshal(original)
-			require.NoError(t, err)
-
-			modelName, rb, err := openAIParseBody("/v1/chat/completions", &extprocv3.HttpBody{Body: bytes})
-			require.NoError(t, err)
-			require.Equal(t, "llama3.3", modelName)
-			require.NotNil(t, rb)
-		})
-		t.Run("error", func(t *testing.T) {
-			modelName, rb, err := openAIParseBody("/v1/chat/completions", &extprocv3.HttpBody{})
-			require.Error(t, err)
-			require.Equal(t, "", modelName)
-			require.Nil(t, rb)
-		})
-	})
-}
diff --git a/internal/extproc/router/router_test.go b/internal/extproc/router/router_test.go
index 49aa46a..1344058 100644
--- a/internal/extproc/router/router_test.go
+++ b/internal/extproc/router/router_test.go
@@ -23,7 +23,7 @@ func (c *dummyCustomRouter) Calculate(map[string]string) (*filterapi.Backend, er
 }
 
 func TestRouter_NewRouter_Custom(t *testing.T) {
-	r, err := NewRouter(&filterapi.Config{}, func(defaultRouter x.Router, _ *filterapi.Config) x.Router {
+	r, err := New(&filterapi.Config{}, func(defaultRouter x.Router, _ *filterapi.Config) x.Router {
 		require.NotNil(t, defaultRouter)
 		_, ok := defaultRouter.(*router)
 		require.True(t, ok) // Checking if the default router is correctly passed.
@@ -40,7 +40,7 @@ func TestRouter_NewRouter_Custom(t *testing.T) {
 
 func TestRouter_Calculate(t *testing.T) {
 	outSchema := filterapi.VersionedAPISchema{Name: filterapi.APISchemaOpenAI}
-	_r, err := NewRouter(&filterapi.Config{
+	_r, err := New(&filterapi.Config{
 		Rules: []filterapi.RouteRule{
 			{
 				Backends: []filterapi.Backend{
@@ -92,7 +92,7 @@ func TestRouter_Calculate(t *testing.T) {
 }
 
 func TestRouter_selectBackendFromRule(t *testing.T) {
-	_r, err := NewRouter(&filterapi.Config{}, nil)
+	_r, err := New(&filterapi.Config{}, nil)
 	require.NoError(t, err)
 	r, ok := _r.(*router)
 	require.True(t, ok)
diff --git a/internal/extproc/server_test.go b/internal/extproc/server_test.go
index 198ac1e..9b67109 100644
--- a/internal/extproc/server_test.go
+++ b/internal/extproc/server_test.go
@@ -31,20 +31,12 @@ func requireNewServerWithMockProcessor(t *testing.T) (*Server, *mockProcessor) {
 	s.config = &processorConfig{}
 
 	m := newMockProcessor(s.config, s.logger)
-	s.Register("/", func(*processorConfig, map[string]string, *slog.Logger) Processor { return m })
+	s.Register("/", func(*processorConfig, map[string]string, *slog.Logger) (Processor, error) { return m, nil })
 
 	return s, m.(*mockProcessor)
 }
 
 func TestServer_LoadConfig(t *testing.T) {
-	t.Run("invalid input schema", func(t *testing.T) {
-		s, _ := requireNewServerWithMockProcessor(t)
-		err := s.LoadConfig(t.Context(), &filterapi.Config{
-			Schema: filterapi.VersionedAPISchema{Name: "some-invalid-schema"},
-		})
-		require.Error(t, err)
-		require.ErrorContains(t, err, "cannot create request body parser")
-	})
 	t.Run("ok", func(t *testing.T) {
 		config := &filterapi.Config{
 			MetadataNamespace: "ns",
@@ -88,7 +80,7 @@ func TestServer_LoadConfig(t *testing.T) {
 		require.NotNil(t, s.config)
 		require.Equal(t, "ns", s.config.metadataNamespace)
 		require.NotNil(t, s.config.router)
-		require.NotNil(t, s.config.bodyParser)
+		require.Equal(t, s.config.schema, config.Schema)
 		require.Equal(t, "x-ai-eg-selected-backend", s.config.selectedBackendHeaderKey)
 		require.Equal(t, "x-model-name", s.config.modelNameHeaderKey)
 
@@ -266,16 +258,16 @@ func TestServer_ProcessorSelection(t *testing.T) {
 	require.NotNil(t, s)
 
 	s.config = &processorConfig{}
-	s.Register("/one", func(*processorConfig, map[string]string, *slog.Logger) Processor {
+	s.Register("/one", func(*processorConfig, map[string]string, *slog.Logger) (Processor, error) {
 		// Returning nil guarantees that the test will fail if this processor is selected
-		return nil
+		return nil, nil
 	})
-	s.Register("/two", func(*processorConfig, map[string]string, *slog.Logger) Processor {
+	s.Register("/two", func(*processorConfig, map[string]string, *slog.Logger) (Processor, error) {
 		return &mockProcessor{
 			t:                     t,
 			expHeaderMap:          &corev3.HeaderMap{Headers: []*corev3.HeaderValue{{Key: ":path", Value: "/two"}}},
 			retProcessingResponse: &extprocv3.ProcessingResponse{Response: &extprocv3.ProcessingResponse_RequestHeaders{}},
-		}
+		}, nil
 	})
 
 	t.Run("unknown path", func(t *testing.T) {
diff --git a/internal/extproc/translator/openai_awsbedrock_test.go b/internal/extproc/translator/openai_awsbedrock_test.go
index e90c1fd..8b06f1e 100644
--- a/internal/extproc/translator/openai_awsbedrock_test.go
+++ b/internal/extproc/translator/openai_awsbedrock_test.go
@@ -24,7 +24,6 @@ import (
 
 	"github.com/envoyproxy/ai-gateway/internal/apischema/awsbedrock"
 	"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
-	"github.com/envoyproxy/ai-gateway/internal/extproc/router"
 )
 
 func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_RequestBody(t *testing.T) {
@@ -666,7 +665,7 @@ func TestOpenAIToAWSBedrockTranslatorV1ChatCompletion_RequestBody(t *testing.T)
 		t.Run(tt.name, func(t *testing.T) {
 			o := &openAIToAWSBedrockTranslatorV1ChatCompletion{}
 			originalReq := tt.input
-			hm, bm, mode, err := o.RequestBody(router.RequestBody(&originalReq))
+			hm, bm, mode, err := o.RequestBody(RequestBody(&originalReq))
 			var expPath string
 			if tt.input.Stream {
 				expPath = fmt.Sprintf("/model/%s/converse-stream", tt.input.Model)
diff --git a/internal/extproc/translator/openai_openai_test.go b/internal/extproc/translator/openai_openai_test.go
index a54c52c..05d0595 100644
--- a/internal/extproc/translator/openai_openai_test.go
+++ b/internal/extproc/translator/openai_openai_test.go
@@ -20,7 +20,6 @@ import (
 	"k8s.io/utils/ptr"
 
 	"github.com/envoyproxy/ai-gateway/internal/apischema/openai"
-	"github.com/envoyproxy/ai-gateway/internal/extproc/router"
 )
 
 func TestOpenAIToOpenAITranslatorV1ChatCompletionRequestBody(t *testing.T) {
@@ -35,7 +34,7 @@ func TestOpenAIToOpenAITranslatorV1ChatCompletionRequestBody(t *testing.T) {
 				originalReq := &openai.ChatCompletionRequest{Model: "foo-bar-ai", Stream: stream}
 
 				o := &openAIToOpenAITranslatorV1ChatCompletion{}
-				hm, bm, mode, err := o.RequestBody(router.RequestBody(originalReq))
+				hm, bm, mode, err := o.RequestBody(RequestBody(originalReq))
 				require.Nil(t, bm)
 				require.NoError(t, err)
 				require.Equal(t, stream, o.stream)
diff --git a/tests/internal/testupstreamlib/testupstream/main.go b/tests/internal/testupstreamlib/testupstream/main.go
index 326c769..d91538d 100644
--- a/tests/internal/testupstreamlib/testupstream/main.go
+++ b/tests/internal/testupstreamlib/testupstream/main.go
@@ -356,7 +356,7 @@ var chatCompletionFakeResponses = []string{
 func getFakeResponse(path string) ([]byte, error) {
 	switch path {
 	case "/v1/chat/completions":
-		const template = `{"choices":[{"message":{"content":"%s"}}]}`
+		const template = `{"choices":[{"message":{"role":"assistant", "content":"%s"}}]}`
 		msg := fmt.Sprintf(template,
 			//nolint:gosec
 			chatCompletionFakeResponses[rand.New(rand.NewSource(uint64(time.Now().UnixNano()))).