Route chat requests across OpenAI and AWS Bedrock based on how complex each prompt actually
is, without the client choosing a tier or a provider. By the end of this recipe, you’ll
have a single /chat endpoint that classifies every incoming prompt with a small, cheap
model, then dispatches the request to a fast OpenAI model for simple prompts or to Claude on
AWS Bedrock for prompts that need deeper reasoning.
Model-Based Routing
Overview
Prerequisites
Kong Konnect
This tutorial uses Kong Konnect. The AI Gateway quickstart script provisions a recipe-scoped AI Gateway and local data plane.
- Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.
-
Export your token. You reuse the same token later for kongctl commands:
export KONNECT_TOKEN='YOUR_KONNECT_PAT' -
Set the recipe-scoped AI Gateway name and run the quickstart script:
export KONNECT_CONTROL_PLANE_NAME='model-based-routing-recipe' curl -Ls https://get.konghq.com/ai | bash -s -- -k $KONNECT_TOKENThis provisions an AI Gateway named
model-based-routing-recipe, a local data plane connected to it, and printsexportlines for the rest of the session vars, includingAI_GATEWAY_ID. Paste those into your shell when prompted.
kongctl
This tutorial uses kongctl to manage AI Gateway configuration.
- Install kongctl from the kongctl documentation.
-
Verify it’s installed:
kongctl version
AI Credentials
This recipe routes every request across two providers, so you need credentials for both. OpenAI serves the fast tier and the prompt-complexity classifier; AWS Bedrock serves the smart tier.
- OpenAI:
- Create an OpenAI account.
- Get an API key.
-
Export the API key as an environment variable:
export OPENAI_TOKEN='Bearer sk-YOUR-KEY'
- AWS Bedrock:
- Ensure you have an AWS account with Bedrock model access enabled for a Claude model.
-
Export your AWS credentials:
export AWS_ACCESS_KEY_ID='your-access-key' export AWS_SECRET_ACCESS_KEY='your-secret-key' export AWS_REGION='us-east-1'
Python 3.11+
The demo script requires Python 3.11 or later. Set up an isolated environment:
python3 -m venv .venv
source .venv/bin/activate
pip install 'openai>=1.0.0'The problem
Teams running production LLM traffic on a single model face a tradeoff they can’t route around at the application layer:
- A single capable model overpays for easy requests. A frontier model priced for deep reasoning charges the same per-token rate for “hello” as it does for a multi-step analysis task. Industry cost breakdowns of production LLM traffic commonly show the majority of requests are simple completions that a much cheaper model could have handled equally well.
- A single cheap model falls short on hard requests. A small, fast model that keeps costs down on simple traffic produces worse answers, or fails outright, on requests that need multi-step reasoning, code generation, or long-context analysis.
- Hardcoded, keyword-based routing breaks down fast. Rules like “if the prompt contains the word ‘code’, use the bigger model” don’t generalize. Real user prompts don’t follow templates, and the rule set that tries to keep up becomes an unmaintainable pile of string-matching heuristics that still classify most prompts incorrectly.
- Client-chosen tiers push a platform decision onto every caller. If application code decides which model tier to call, every team integrating with the platform needs to implement and maintain that same judgment call, and nothing stops a caller from always requesting the expensive tier out of caution.
The root issue is that model selection needs to be a per-request decision made close to the request, using the actual content of the prompt, not a static assignment baked into application code or a client-side guess.
The solution
Kong AI Gateway inserts a classification step ahead of the actual chat request. A
small, inexpensive model reads each incoming prompt and returns a tier recommendation, and
Kong Gateway uses that recommendation to dispatch the request to the right upstream
model, either a fast OpenAI model for simple prompts or Claude on AWS Bedrock for prompts
that need deeper reasoning. The client always calls the same /chat endpoint with a standard
chat completion body. It never chooses a provider, never chooses a model, and never sees the
classification step happen.
sequenceDiagram
participant C as Client
participant Chat as chat AI Model
participant Sel as selector AI Model<br/>(OpenAI o3-mini)
participant Fast as fast-tier AI Model<br/>(OpenAI)
participant Smart as smart-tier AI Model<br/>(Bedrock Claude)
C->>Chat: POST /chat (apikey, prompt)
activate Chat
Chat->>Chat: key-auth Auth Strategy: validate apikey (else 401)
Chat->>Sel: Datakit Policy calls /model-selection with the prompt
activate Sel
Sel-->>Chat: Tier recommendation ("fast" or "smart")
deactivate Sel
alt Fast tier
Chat->>Fast: Datakit calls /model-based-routing-fast
activate Fast
Fast-->>Chat: Completion
deactivate Fast
else Smart tier
Chat->>Smart: Datakit calls /model-based-routing-smart
activate Smart
Smart-->>Chat: Completion
deactivate Smart
end
Chat-->>C: Completion (+ X-Kong-LLM-Model)
deactivate Chat|
Component |
Responsibility |
|---|---|
| Client application |
Sends a standard chat completion request to /chat with an apikey header. No routing logic required.
|
| Key Auth AI Auth Strategy |
Validates the apikey header against a registered AI Consumer credential before any classification or upstream call happens.
|
| Datakit Policy (orchestrator) | Extracts the prompt, calls the selector AI Model, reads back the tier recommendation, and calls whichever tier AI Model matches. |
| selector AI Model | Classifies prompt complexity using a small OpenAI model and an AI Prompt Decorator Policy that constrains the answer to a single word. |
| fast-tier AI Model | Serves simple prompts using a fast, inexpensive OpenAI model. |
| smart-tier AI Model | Serves complex prompts using Claude on AWS Bedrock. |
| AI Model Providers | Hold the OpenAI and AWS Bedrock credentials and inject them into every upstream call. |
How it works
A request to /chat passes through four stages: authentication, classification, dispatch,
and response relay.
- A client sends a chat completion request to
/chatwith anapikeyheader. The request body’smodelfield carries a placeholder value; Kong ignores it. - The Key Auth AI Auth Strategy validates the
apikeyheader against a registered AI Consumer credential. An invalid or missing key gets a401before any classification or upstream call happens. - The Datakit Policy attached to the
/chatAI Model extracts the prompt and calls the selector AI Model at/model-selection, forwarding the sameapikeyheader. - The selector AI Model’s AI Prompt Decorator Policy prepends classification instructions to
the prompt, then its AI Proxy target sends the decorated prompt to a small OpenAI model,
which returns exactly one word:
fastorsmart. - Back in the Datakit workflow, a parsed tier decision determines which internal AI Model to call next: the fast tier (OpenAI) or the smart tier (AWS Bedrock, Claude).
- Datakit calls the chosen tier’s AI Model directly, then returns that Model’s response to
the client as the final answer to
/chat, including theX-Kong-LLM-Modelheader showing which upstream model actually served the request.
Key Auth: API key authentication and AI Consumer mapping
Authentication in Kong AI Gateway 2.0 is an AI Auth Strategy, not an AI Policy. Every AI
Model in this recipe (the public /chat endpoint and the three internal Models) shares the
same Key Auth AI Auth Strategy and the same AI Consumer credential, so a single API key
authenticates both the client’s request and every internal call the Datakit workflow makes on
its behalf.
Configuration details
ai_gateway_auth_strategies:
- ref: model-based-routing-auth
type: key-auth
display_name: "model-based-routing key auth"
config:
key_names:
- apikey
hide_credentials: true
ai_gateway_consumers:
- ref: model-based-routing-consumer
name: model-based-routing-consumer
display_name: "model-based-routing demo consumer"
type: api-key
credentials:
- ref: model-based-routing-credential
name: model-based-routing-credential
display_name: "model-based-routing demo API key"
type: api-key
api_key: !secret {source: !env CONSUMER_API_KEY}key_names: [apikey]. The header Kong checks for the API key. The Datakit Policy reads
this same header from the client’s request and forwards it on every internal call, so one
credential authenticates the whole request chain.
hide_credentials: true. Strips the API key before any request leaves Kong, so neither
provider ever sees the AI Consumer’s key.
api_key is write-only. An AI Consumer credential’s api_key field must be a !secret
reference. Kong rejects a plain string here, even for a demo value like this recipe’s.
AI Prompt Decorator: classify prompt complexity
The AI Prompt Decorator Policy prepends a hidden
system message to the selector AI Model, instructing the classifier model to analyze the
prompt and return only fast or smart. The client never sees this instruction. It only
sees its own prompt and, eventually, an answer.
Configuration details
ai_gateway_policies:
- ref: model-based-routing-decorator
type: ai-prompt-decorator
display_name: "model-based-routing classifier prompt"
config:
llm_format: openai
prompts:
prepend:
- role: system
content: >
You are a model router. Analyze the user's prompt and recommend
the most appropriate model tier.
Return ONLY ONE of these values:
- "fast" for simple tasks (greetings, basic questions, straightforward requests)
- "smart" for complex tasks (reasoning, analysis, coding, creative writing)
Respond with just the single word "fast" or "smart", nothing else.prompts.prepend. The AI Prompt Decorator Policy inserts the system message before the
user’s prompt in the message array that Kong sends to the classifier model. The system message
constrains the classifier’s output to a single word, which the Datakit workflow parses directly.
Adjust the classification rules in the system message to fit your workload. For example,
treat translation as fast or multi-step planning as smart, depending on which upstream tier
handles your traffic best.
AI Model and AI Model Provider: the selector, fast tier, and smart tier
Three of this recipe’s four AI Models exist to serve the dynamic-routing mechanism rather than to be called directly: the selector classifies prompts using a small OpenAI model, the fast tier serves simple prompts using a larger OpenAI model, and the smart tier serves complex prompts using Claude on AWS Bedrock. Each AI Model references an AI Model Provider that holds that provider’s credentials.
Configuration details
ai_gateway_model_providers:
- ref: model-based-routing-openai-provider
type: openai
display_name: "model-based-routing (OpenAI)"
config:
auth:
type: basic
headers:
- name: Authorization
value: !secret {source: !env OPENAI_TOKEN}
- ref: model-based-routing-bedrock-provider
type: bedrock
display_name: "model-based-routing (AWS Bedrock)"
config:
auth:
type: aws
access_key_id: !env AWS_ACCESS_KEY_ID
secret_access_key: !secret {source: !env AWS_SECRET_ACCESS_KEY}
ai_gateway_models:
- ref: model-based-routing-selector
type: model
display_name: "model-based-routing (selector)"
formats:
- type: openai
capabilities:
- generate
access:
auth_strategies:
- model-based-routing-auth
policies:
- model-based-routing-decorator
config:
max_request_body_size: 5242880
response_streaming: deny
route:
paths:
- /model-selection
targets:
- name: !env OPENAI_SELECTOR_SLM
provider: model-based-routing-openai-provider
config:
type: openaiconfig.auth (on the AI Model Provider). Kong holds provider credentials here and injects
them into every upstream request routed through that AI Model Provider. AWS Bedrock authenticates with
access_key_id/secret_access_key, and the region lives on the target, not the AI Model Provider,
because one AWS account’s credentials can serve targets in different regions.
response_streaming: deny on every AI Model in this recipe. The Datakit Policy needs a
complete response body from the selector call to parse the tier decision, and needs a
complete response body from whichever tier it calls to relay back to the client. Streaming
responses from any of the four Models in this recipe wouldn’t reach the orchestrator step
that depends on them, so this recipe doesn’t support token-by-token streaming on /chat.
targets[].name. The upstream model identifier. The selector uses a small, cheap model
since the classifier only needs to return one word, not generate a full answer.
The fast tier and smart tier AI Models (model-based-routing-fast,
model-based-routing-smart) follow the same shape, targeting a larger OpenAI model and a
Claude model on Bedrock respectively. Clients don’t call either Model directly. Both exist so
the Datakit Policy on /chat can call them internally and relay their response.
Datakit: dynamic tier orchestration
The Datakit Policy attached to the public /chat AI Model runs the whole
dynamic-routing workflow as a sequence of declarative nodes: extract the prompt, call the
selector, parse the tier, call the matching tier’s AI Model, and return that response to the
client.
Configuration details
ai_gateway_policies:
- ref: model-based-routing-orchestrator
type: datakit
display_name: "model-based-routing tier orchestration"
config:
nodes:
- name: EXTRACT_PROMPT
type: jq
input: request.body
output: prompt_data
jq: '{messages: .messages}'
- name: EXTRACT_AUTH
type: jq
input: request.headers
output: auth_header
jq: '{apikey: (.apikey // .Apikey // .APIKey)}'
- name: CALL_MODEL_SELECTOR
type: call
url: http://localhost:8000/model-selection
method: POST
inputs:
body: EXTRACT_PROMPT.prompt_data
headers: EXTRACT_AUTH.auth_header
- name: EXTRACT_TIER
type: jq
input: CALL_MODEL_SELECTOR.body
output: tier
jq: '.choices[0].message.content | gsub("^\\s+|\\s+$"; "")'
- name: IS_SMART
type: jq
input: EXTRACT_TIER.tier
output: value
jq: '. == "smart"'
- name: ROUTE_DECISION
type: branch
input: IS_SMART.value
then:
- CALL_SMART
- RESPOND_SMART
else:
- CALL_FAST
- RESPOND_FAST
- name: CALL_SMART
type: call
url: http://localhost:8000/model-based-routing-smart
method: POST
inputs:
body: EXTRACT_PROMPT.prompt_data
headers: EXTRACT_AUTH.auth_header
- name: RESPOND_SMART
type: exit
inputs:
body: CALL_SMART.body
headers: CALL_SMART.headers
- name: CALL_FAST
type: call
url: http://localhost:8000/model-based-routing-fast
method: POST
inputs:
body: EXTRACT_PROMPT.prompt_data
headers: EXTRACT_AUTH.auth_header
- name: RESPOND_FAST
type: exit
inputs:
body: CALL_FAST.body
headers: CALL_FAST.headersEXTRACT_PROMPT / EXTRACT_AUTH. Pull the message array and the apikey header out of
the client’s original request, so both can be forwarded to every internal call this workflow
makes.
CALL_MODEL_SELECTOR. Calls the selector AI Model’s /model-selection path the same way
an external client would, reusing the client’s own API key.
EXTRACT_TIER / IS_SMART. Parse the classifier’s single-word answer into a tier string,
then into a boolean the branch node can act on.
ROUTE_DECISION. A branch node that calls the smart tier’s AI Model when the classifier
says smart, or the fast tier’s AI Model otherwise.
RESPOND_SMART / RESPOND_FAST. An exit node returns a response built from an earlier
node directly to the client, bypassing this AI Model’s own target. Each branch reaches its
matching exit node with the body and headers from whichever tier’s AI Model actually served
the request, including X-Kong-LLM-Model.
This exact node graph (the branch and exit node fields in the selector configuration) has not yet been
verified against a live AI Gateway. Confirm it with a real deployment before relying on it in
production, and see the Datakit Policy reference for the full field
set on each node type.
In production, store credentials in AI Vaults using
{vault://backend/key}references rather than environment variables. AI Vaults support HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, and the Konnect Config Store.
Apply the Kong configuration
The following configuration creates two AI Model Providers (OpenAI and AWS Bedrock), a
Key Auth AI Auth Strategy with a demo AI Consumer, an AI Prompt Decorator Policy for
classification, a Datakit Policy for orchestration, and four AI Models: the public /chat
endpoint plus the selector, fast-tier, and smart-tier Models it calls internally. A kongctl
namespace scopes every resource, so you can tear the configuration down cleanly without
affecting other configurations on the same AI Gateway.
First, adopt the quickstart AI Gateway into a kongctl namespace so the following apply command can manage it.
kongctl adopt ai-gateway "${KONNECT_CONTROL_PLANE_NAME}" \
--namespace "${KONNECT_CONTROL_PLANE_NAME}" \
--pat "${KONNECT_TOKEN}"Adoption stamps the KONGCTL-namespace label on the AI Gateway.
Export the demo AI Consumer’s API key and the model selections for each tier. The AI Consumer
credential’s api_key field is write-only and must come from an env var, even for this fixed
demo value:
export CONSUMER_API_KEY='demo-api-key'
export OPENAI_SELECTOR_SLM='o3-mini' # classifier model
export OPENAI_FAST_MODEL='gpt-4o-mini' # fast tier
export BEDROCK_SMART_MODEL='anthropic.claude-3-5-sonnet-20241022-v2:0' # smart tierApply the Kong configuration:
cat <<'EOF' > kong-recipe.yaml
_defaults:
kongctl:
namespace: model-based-routing-recipe
ai_gateway_model_providers:
- ref: model-based-routing-openai-provider
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-openai-provider
display_name: "model-based-routing (OpenAI)"
type: openai
config:
auth:
type: basic
headers:
- name: Authorization
value: !secret {source: !env OPENAI_TOKEN}
- ref: model-based-routing-bedrock-provider
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-bedrock-provider
display_name: "model-based-routing (AWS Bedrock)"
type: bedrock
config:
auth:
type: aws
access_key_id: !env AWS_ACCESS_KEY_ID
secret_access_key: !secret {source: !env AWS_SECRET_ACCESS_KEY}
ai_gateway_auth_strategies:
- ref: model-based-routing-auth
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-auth
display_name: "model-based-routing key auth"
type: key-auth
config:
key_names:
- apikey
hide_credentials: true
ai_gateway_consumers:
- ref: model-based-routing-consumer
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-consumer
display_name: "model-based-routing demo consumer"
type: api-key
credentials:
- ref: model-based-routing-credential
name: model-based-routing-credential
display_name: "model-based-routing demo API key"
type: api-key
api_key: !secret {source: !env CONSUMER_API_KEY}
ai_gateway_policies:
- ref: model-based-routing-decorator
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-decorator
display_name: "model-based-routing classifier prompt"
type: ai-prompt-decorator
config:
llm_format: openai
prompts:
prepend:
- role: system
content: >
You are a model router. Analyze the user's prompt and recommend
the most appropriate model tier.
Return ONLY ONE of these values:
- "fast" for simple tasks (greetings, basic questions, straightforward requests)
- "smart" for complex tasks (reasoning, analysis, coding, creative writing)
Respond with just the single word "fast" or "smart", nothing else.
- ref: model-based-routing-orchestrator
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-orchestrator
display_name: "model-based-routing tier orchestration"
type: datakit
config:
nodes:
- name: EXTRACT_PROMPT
type: jq
input: request.body
output: prompt_data
jq: '{messages: .messages}'
- name: EXTRACT_AUTH
type: jq
input: request.headers
output: auth_header
jq: '{apikey: (.apikey // .Apikey // .APIKey)}'
- name: CALL_MODEL_SELECTOR
type: call
url: http://localhost:8000/model-selection
method: POST
inputs:
body: EXTRACT_PROMPT.prompt_data
headers: EXTRACT_AUTH.auth_header
- name: EXTRACT_TIER
type: jq
input: CALL_MODEL_SELECTOR.body
output: tier
jq: '.choices[0].message.content | gsub("^\\s+|\\s+$"; "")'
- name: IS_SMART
type: jq
input: EXTRACT_TIER.tier
output: value
jq: '. == "smart"'
- name: ROUTE_DECISION
type: branch
input: IS_SMART.value
then:
- CALL_SMART
- RESPOND_SMART
else:
- CALL_FAST
- RESPOND_FAST
- name: CALL_SMART
type: call
url: http://localhost:8000/model-based-routing-smart
method: POST
inputs:
body: EXTRACT_PROMPT.prompt_data
headers: EXTRACT_AUTH.auth_header
- name: RESPOND_SMART
type: exit
inputs:
body: CALL_SMART.body
headers: CALL_SMART.headers
- name: CALL_FAST
type: call
url: http://localhost:8000/model-based-routing-fast
method: POST
inputs:
body: EXTRACT_PROMPT.prompt_data
headers: EXTRACT_AUTH.auth_header
- name: RESPOND_FAST
type: exit
inputs:
body: CALL_FAST.body
headers: CALL_FAST.headers
ai_gateway_models:
- ref: model-based-routing-selector
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-selector
display_name: "model-based-routing (selector)"
type: model
formats:
- type: openai
capabilities:
- generate
access:
auth_strategies:
- model-based-routing-auth
policies:
- model-based-routing-decorator
config:
max_request_body_size: 5242880
response_streaming: deny
logging:
payloads: false
route:
paths:
- /model-selection
protocols:
- http
- https
methods:
- POST
- OPTIONS
strip_path: true
targets:
- name: !env OPENAI_SELECTOR_SLM
provider: model-based-routing-openai-provider
config:
type: openai
- ref: model-based-routing-fast
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-fast
display_name: "model-based-routing (fast tier)"
type: model
formats:
- type: openai
capabilities:
- generate
access:
auth_strategies:
- model-based-routing-auth
config:
max_request_body_size: 10485760
response_streaming: deny
logging:
payloads: false
route:
paths:
- /model-based-routing-fast
protocols:
- http
- https
methods:
- POST
- OPTIONS
strip_path: true
targets:
- name: !env OPENAI_FAST_MODEL
provider: model-based-routing-openai-provider
config:
type: openai
- ref: model-based-routing-smart
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-smart
display_name: "model-based-routing (smart tier)"
type: model
formats:
- type: openai
capabilities:
- generate
access:
auth_strategies:
- model-based-routing-auth
config:
max_request_body_size: 10485760
response_streaming: deny
logging:
payloads: false
route:
paths:
- /model-based-routing-smart
protocols:
- http
- https
methods:
- POST
- OPTIONS
strip_path: true
targets:
- name: !env BEDROCK_SMART_MODEL
provider: model-based-routing-bedrock-provider
config:
region: !env AWS_REGION
type: bedrock
- ref: model-based-routing-chat
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: model-based-routing-chat
display_name: "model-based-routing (chat)"
type: model
formats:
- type: openai
capabilities:
- generate
access:
auth_strategies:
- model-based-routing-auth
policies:
- model-based-routing-orchestrator
config:
max_request_body_size: 10485760
response_streaming: deny
logging:
payloads: false
route:
paths:
- /chat
protocols:
- http
- https
methods:
- POST
- OPTIONS
strip_path: true
targets:
- name: !env OPENAI_FAST_MODEL
provider: model-based-routing-openai-provider
config:
type: openai
EOF
kongctl apply -f kong-recipe.yaml -o text --auto-approve --pat "${KONNECT_TOKEN}"
rm -f kong-recipe.yamlTry it out
The demo script sends a simple prompt and a complex prompt to /chat, printing the
X-Kong-LLM-Model header on each response so you can confirm which upstream model actually
answered. A final call presents an invalid API key and shows Kong rejecting it with 401
before any classification or upstream call.
The demo passes the API key via
default_headersbecause the OpenAI SDK reservesapi_keyfor theAuthorization: Bearerheader. To let clients pass the key throughapi_keydirectly, attach a Pre-Function Policy that copies the Bearer token to theapikeyheader server-side.
Create the demo script:
cat <<'EOF' > demo.py
"""Model-based routing demo. See README for context.
Sends a simple prompt and a complex prompt to Kong's /chat endpoint. Kong classifies each
prompt's complexity internally (via an OpenAI o3-mini call) and dynamically routes the
request to either the OpenAI fast tier or the AWS Bedrock (Claude) smart tier. The demo
prints the X-Kong-LLM-Model header on each response so you can confirm which upstream model
actually served the request. A final call with an invalid API key shows Kong rejecting the
request with 401 before any classification or upstream call happens.
Run with:
python demo.py
Requires the Kong configuration from the README to be applied and PROXY_URL (defaults to
http://localhost:8000) reachable.
"""
import os
import sys
import time
from openai import APIStatusError, OpenAI
PROXY_URL = os.getenv("PROXY_URL", "http://localhost:8000")
API_KEY = "demo-api-key"
SIMPLE_PROMPT = "Hi there! What's 2 + 2?"
COMPLEX_PROMPT = (
"Perform in depth analysis of the ancient Roman world during the time "
"of Pliny the Elder in two paragraphs."
)
# ANSI color codes. Disabled when stdout isn't a TTY or NO_COLOR is set.
_USE_COLOR = sys.stdout.isatty() and "NO_COLOR" not in os.environ
def _c(code: str, s: str) -> str:
return f"\033[{code}m{s}\033[0m" if _USE_COLOR else s
BOLD = lambda s: _c("1", s)
DIM = lambda s: _c("2", s)
GREEN = lambda s: _c("32", s)
CYAN = lambda s: _c("36", s)
RED = lambda s: _c("31", s)
def make_client(api_key: str) -> OpenAI:
"""Construct an OpenAI client that sends the given API key in the apikey header."""
return OpenAI(
base_url=f"{PROXY_URL}/chat",
api_key="unused", # required by the SDK; Kong reads the apikey header instead
default_headers={"apikey": api_key},
)
def call(client: OpenAI, label: str, prompt: str) -> None:
"""Send one chat request and print which upstream model Kong routed it to."""
print(f"\n{BOLD('[REQUEST]')} {label}: {prompt!r}")
start_ms = round(time.time() * 1000)
try:
# The body's "model" field is a placeholder. Kong's internal classifier
# overwrites it with "fast" or "smart" before the request reaches the
# upstream provider, so the value sent here never reaches OpenAI or Bedrock.
raw = client.chat.completions.with_raw_response.create(
model="fast",
messages=[{"role": "user", "content": prompt}],
)
except APIStatusError as e:
elapsed_ms = round(time.time() * 1000) - start_ms
print(f"{RED(BOLD('[BLOCKED]'))} {RED(BOLD(str(e.status_code)))} {e.message} ({elapsed_ms}ms)")
return
elapsed_ms = round(time.time() * 1000) - start_ms
completion = raw.parse()
upstream_model = raw.headers.get("x-kong-llm-model", ".")
upstream_latency = raw.headers.get("x-kong-upstream-latency", ".")
proxy_latency = raw.headers.get("x-kong-proxy-latency", ".")
answer = completion.choices[0].message.content
print(f"[RESPONSE] {DIM(answer)}")
print(f"{GREEN(BOLD('[ROUTED TO]'))} upstream model={CYAN(BOLD(upstream_model))}")
print(f"[LATENCY] {DIM(f'upstream={upstream_latency}ms proxy={proxy_latency}ms total={elapsed_ms}ms')}")
def section(title: str) -> None:
bar = "=" * 70
print(f"\n{bar}\n{BOLD(title)}\n{bar}")
def main() -> None:
section("1. Simple prompt. Kong's classifier should recommend the fast tier")
client = make_client(API_KEY)
call(client, "simple", SIMPLE_PROMPT)
section("2. Complex prompt. Kong's classifier should recommend the smart tier")
call(client, "complex", COMPLEX_PROMPT)
section("3. Invalid API key. Kong rejects before any classification or upstream call")
bad_client = make_client("not-a-real-key")
call(bad_client, "simple", SIMPLE_PROMPT)
section("Done.")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit(130)
EOFRun it:
python demo.pyExample output:
======================================================================
1. Simple prompt. Kong's classifier should recommend the fast tier
======================================================================
[REQUEST] simple: "Hi there! What's 2 + 2?"
[RESPONSE] 2 + 2 equals 4.
[ROUTED TO] upstream model=openai/gpt-4o-mini
[LATENCY] upstream=612ms proxy=780ms total=1401ms
======================================================================
2. Complex prompt. Kong's classifier should recommend the smart tier
======================================================================
[REQUEST] complex: "Perform in depth analysis of the ancient Roman world during the time of Pliny the Elder in two paragraphs."
[RESPONSE] Pliny the Elder lived during the early Roman Empire, a period marked by...
[ROUTED TO] upstream model=bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0
[LATENCY] upstream=2114ms proxy=845ms total=2984ms
======================================================================
3. Invalid API key. Kong rejects before any classification or upstream call
======================================================================
[REQUEST] simple: "Hi there! What's 2 + 2?"
[BLOCKED] 401 Error code: 401 - {'message': 'Unauthorized'} (28ms)
======================================================================
Done.
======================================================================Example request body sent to /chat (identical for the simple and complex calls, only the
prompt text changes):
{
"model": "fast",
"messages": [
{ "role": "user", "content": "Hi there! What's 2 + 2?" }
]
}What happened
- Both prompts hit the same endpoint. Neither call told Kong which tier or provider to
use. The
modelfield in the request body is a placeholder the classification step overwrites internally. - Kong classified each prompt before proxying it. Kong classified the simple greeting as
fastand routed it to the OpenAI fast-tier Model. Kong classified the Roman history analysis assmartand routed it to the Bedrock smart-tier Model. TheX-Kong-LLM-Modelheader on each response shows the actual upstream model that answered. - The proxy latency includes the classification round trip. Because the orchestrator
AI Policy calls the selector AI Model internally before calling the chosen tier,
proxylatency in this recipe includes both the classification call and the orchestration overhead, not just Kong’s own request handling. - Kong enforced auth before doing any of that work. The third request used a key no AI Consumer holds. The Key Auth AI Auth Strategy rejected it in about 30 ms, well before the classification step or any provider call would have started, so the request consumed no provider quota.
Explore in Konnect
Open Konnect and find the AI Gateway named
model-based-routing-recipe. The recipe created two AI Model Providers (OpenAI and AWS
Bedrock), four AI Models (model-based-routing-chat, model-based-routing-selector,
model-based-routing-fast, model-based-routing-smart), a Key Auth AI Auth Strategy, an AI
Prompt Decorator Policy, a Datakit Policy, and an AI Consumer with a demo API key credential, all
scoped to this recipe by the kongctl namespace applied above.
For platform-wide traffic analysis across every AI Gateway, head to the Observability L1 menu in Konnect.
Variations and next steps
Add a third tier. Extend the classifier’s system message to return a third value (for
example, "batch" for non-interactive workloads) and add a matching internal AI Model and
branch path in the orchestrator AI Policy.
Tune the classification rules. The AI Prompt Decorator’s system message defines what
counts as simple versus complex. Adjust it to match your own traffic patterns, for example
treating short code snippets as fast and long-form creative writing as smart.
Add per-consumer rate limits. With the Key Auth AI Auth Strategy mapping requests to AI Consumers,
attach the AI Rate Limiting Advanced Policy
to the /chat AI Model to apply token quotas per AI Consumer. See the
LLM cost optimization recipe for a worked example of
cost-based tiered rate limiting.
Add tracing across the classification hop. Attach the OpenTelemetry Policy to see the classification call and the chosen tier’s call as separate spans in your observability backend. See the Voice AI observability recipe for a worked example.
Cleanup
The recipe’s kongctl namespace scoped all resources, so this teardown removes only this recipe’s configuration. Tear down the local data plane and delete the AI Gateway from Konnect:
export KONNECT_CONTROL_PLANE_NAME='model-based-routing-recipe' && curl -Ls https://get.konghq.com/ai | bash -s -- -d -k $KONNECT_TOKEN