Put Mistral behind Kong AI Gateway so that application code never holds a Mistral API
key. By the end of this recipe you will have one /mistral endpoint serving two capabilities,
chat through mistral-small-latest and embeddings through mistral-embed, both reachable only
with a per-application key you can revoke on its own.
Because the AI Model exposes Mistral in OpenAI format, the demo talks to Kong with the stock
OpenAI SDK and never imports a Mistral library.
This provisions an AI Gateway named mistral-ai-with-kong-ai-gateway-recipe, a local Data Plane connected to it, and prints export lines (including AI_GATEWAY_ID) for the rest of the session vars. Paste those into your shell when prompted.
Mistral’s API is OpenAI-compatible, which makes the first integration trivial: change base_url, change the key, ship it. That same key is what gets copied into every service that needs it.
One key, every service. A Mistral API key is a single bearer token with account-wide scope. Once three services hold it, you can’t tell which one caused a spike, and you can’t revoke access for one of them without rotating the key for all three.
No identity at the edge. Usage reporting is per key, not per caller. When a key is shared, per-team attribution has to be reconstructed from application logs that may not exist.
Model names leak into application code.mistral-small-latest, mistral-medium-latest, and codestral-latest differ in price and capability. When those strings are hardcoded across services, moving a workload to a cheaper model becomes a coordinated redeploy.
Chat and embeddings drift apart. A retrieval service needs an embedding model while a chat service needs a generative one. Wired directly, they become two independent integrations, with two copies of the credential and two places to change.
The root issue is coupling. Application code holds the provider credential, names the provider’s models directly, and repeats that wiring per capability, so there’s no single place to enforce identity or change routing.
Kong AI Gateway splits this into entities that map onto the problem. One AI Model Provider holds the Mistral credential. Two AI Models expose it, one for generate and one for embeddings, sharing a single base path and each answering to its own client-facing alias. An AI Auth Strategy with an AI Consumer issues a separate key per application, so revoking one app is a single delete and the Mistral key never leaves Konnect.
sequenceDiagram
participant C as Client app
participant K as Kong AI Gateway
participant M as Mistral API
C->>K: POST /mistral/chat/completions (apikey, model: mistral-chat)
activate K
K->>K: AI Auth Strategy - validate apikey, attach AI Consumer (else 401)
K->>K: AI Model - resolve alias mistral-chat to mistral-small-latest
K->>K: AI Model Provider - inject Authorization header
K->>M: POST /v1/chat/completions (Bearer MISTRAL_API_KEY)
M-->>K: completion and token usage
K->>K: record tokens, latency, cost
K-->>C: OpenAI-format response
deactivate K
Entity
Job in this recipe
AI Model Provider
Stores the Mistral API key once and injects it into every upstream call.
AI Model (chat)
Exposes /mistral/chat/completions in OpenAI format, routed to mistral-small-latest.
AI Model (embeddings)
Exposes /mistral/embeddings in OpenAI format, routed to mistral-embed.
AI Auth Strategy
Requires an apikey header on both endpoints and strips it before the upstream call.
AI Consumer
The application identity a key belongs to, and the unit you revoke.
A chat request through this recipe passes four stages:
Route match. The data plane matches POST /mistral/chat/completions. The /mistral prefix comes from the AI Model’s config.route.paths, and /chat/completions is the endpoint the generate capability exposes. An embeddings request matches /mistral/embeddings on the second AI Model, which shares the same prefix.
Authentication. The AI Auth Strategy reads the apikey header and matches it to an AI Consumer credential. An unknown key terminates with 401 here, before Kong opens a connection to Mistral, so no Mistral quota is spent on rejected traffic.
Model selection. The AI Model reads the model field from the request body. mistral-chat is an alias declared in config.route.model.values; Kong resolves it to the target’s upstream model name and reports the result in the X-Kong-LLM-Model response header.
Upstream call and translation. The AI Model Provider attaches the stored Authorization header, Kong translates the OpenAI-shaped request into Mistral’s contract, and translates the response back. Token counts, latency, and cost are recorded on the way through.
The AI Model Provider is the only entity that knows the Mistral key. Its type: mistral selects Mistral’s API surface and resolves the upstream base URL, so the recipe never configures a hostname. Because the credential lives on the provider rather than on each model, rotating it is a single apply, and every AI Model whose targets reference this provider picks up the new value.
type: mistral: selects the Mistral provider and its default endpoint. See Mistral provider for the supported capabilities.
config.auth.type: basic: header-based upstream auth. Mistral expects a bearer token, so the header value carries the Bearer prefix.
!secret {source: !env 'MISTRAL_TOKEN'}: the credential field is write-only. kongctl sends the value to Konnect and never reads it back, so it can’t surface in a plan or a diff. A plain string in this field is rejected.
No endpoint is configured here.type: mistral resolves Mistral’s hosted API. To route to a self-hosted or Mistral-compatible endpoint instead, set upstream_url on the target’s config block rather than on the provider.
An AI Model is a virtual model: it declares the wire format it speaks, the base path it answers on, the capabilities it exposes, and the upstream targets it routes to. Mistral supports two capabilities through Kong AI Gateway, generate and embeddings, and each needs its own AI Model because the capability determines the endpoint. Both models in this recipe declare the same /mistral base path, so one base_url in the client covers chat and embeddings.
formats: [{type: openai}] is what lets the stock OpenAI SDK work against Mistral unchanged, and config.route.model decouples the client from the upstream model name.
Some providers also offer a native wire format, which passes requests upstream without translation at the cost of locking the AI Model to that provider. Mistral has no native format, so openai is the format this recipe uses. See AI Model for the formats each provider supports.
capabilities: [generate]: exposes /chat/completions under the base path. The embeddings model instead declares capabilities: [embeddings], which exposes /embeddings.
config.route.model: lets clients send mistral-chat instead of mistral-small-latest. Pointing the target at a different Mistral model is then invisible to callers. Each AI Model accepts exactly one alias value.
response_streaming: allow: permits streamed chat responses. The embeddings model sets deny, since embeddings return a single payload.
targets[].config.format: openai: tells Kong which contract to speak to Mistral. This is the 2.0 replacement for the 1.0 mistral_format option.
access.auth_strategies: attaches inbound authentication. Authentication is not a policy in 2.0. The policies field is reserved for guardrails, caching, and rate limiting.
Inbound authentication is separate from the outbound credential on the provider. A key-auth strategy makes Kong look for an apikey header, match it to an AI Consumer’s credential, and reject anything else with 401. Because hide_credentials: true, Kong strips that header before forwarding upstream, so the consumer key never reaches Mistral. Each application gets its own AI Consumer and its own credential, and revoking one application is a delete on that credential rather than a rotation of the Mistral key.
key_in_query: false and key_in_body: false: restrict the credential to the header, keeping keys out of access logs and request bodies.
credentials: nests directly inside the AI Consumer. The api_key field is write-only and rejects a plain string, so even a demo value is wrapped in !secret.
To add a second application, add another ai_gateway_consumers entry with its own credential. Both reference the same auth strategy, and each can be deleted independently.
In production, store credentials in Kong Vaults rather than environment variables. Kong supports HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, and the Konnect Config Store.
This section configures the AI Gateway in two parts. First, adopt the quickstart AI Gateway into a kongctl namespace so the apply command below can manage it. The recipe’s mistral-ai-with-kong-ai-gateway-recipe namespace scopes the gateway so teardown removes only this recipe’s configuration.
Adoption stamps the KONGCTL-namespace label on the AI Gateway.
Export your environment variables:
export CHAT_MODEL='mistral-small-latest' # served as the mistral-chat aliasexport EMBEDDINGS_MODEL='mistral-embed' # served as the mistral-embeddings alias
Copied!
KONNECT_CONTROL_PLANE_NAME, AI_GATEWAY_ID, MISTRAL_TOKEN, and CONSUMER_API_KEY are already exported during the prerequisites, so they do not need to be re-exported here.
Mistral gates its larger models by subscription tier. If you set CHAT_MODEL to a model your key isn’t entitled to, the request reaches Mistral and returns 403 with "type": "tier_not_allowed". That’s an upstream response passed through by Kong, not a configuration error. mistral-small-latest and mistral-embed are available on every tier.
The demo script exercises all four behaviors through the OpenAI SDK: a chat call, the same call streamed, an embeddings call, and a call with an invalid key.
The script passes the key through default_headers because the OpenAI SDK reserves api_key for the Authorization: Bearer header, while this auth strategy reads the apikey header. The SDK still requires api_key to be set to something, so the demo passes the literal string unused.
A chat request through Kong looks like this:
curl -s -X POST "$KONNECT_PROXY_URL/mistral/chat/completions" \ -H "Content-Type: application/json" \ -H "apikey: $CONSUMER_API_KEY" \ --json '{"model":"mistral-chat","messages":[{"role":"user","content":"Reply with exactly: pong"}]}'
The response carries X-Kong-LLM-Model: mistral/mistral-small-latest, the upstream model Kong resolved the mistral-chat alias to.
Save the demo script as demo.py:
"""Mistral chat and embeddings through Kong AI Gateway.Demonstrates that a single Kong endpoint fronts two Mistral capabilities while theMistral API key stays on the AI Model Provider, never in this script: 1. Chat completion via the `mistral-chat` alias, showing the upstream model Kong resolved it to in the X-Kong-LLM-Model response header. 2. The same endpoint streamed, proving response_streaming: allow. 3. Embeddings via the `mistral-embeddings` alias on the same base path. 4. A call with an invalid key, proving the key-auth AI Auth Strategy rejects it before any Mistral quota is spent.Run: export PROXY_URL="$KONNECT_PROXY_URL" export CONSUMER_API_KEY='demo-api-key' python demo.py"""import osimport sysimport timefrom openai import APIStatusError, OpenAIPROXY_URL = os.getenv("PROXY_URL", "http://localhost:8000")API_KEY = os.getenv("CONSUMER_API_KEY", "demo-api-key")# Client-facing aliases declared in each AI Model's config.route.model.values.# Kong resolves these to the upstream Mistral model names.CHAT_MODEL = "mistral-chat"EMBEDDINGS_MODEL = "mistral-embeddings"# 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.environdef _c(code: str, s: str) -> str: return f"\033[{code}m{s}\033[0m" if _USE_COLOR else sdef BOLD(s: str) -> str: return _c("1", s)def DIM(s: str) -> str: return _c("2", s)def GREEN(s: str) -> str: return _c("32", s)def CYAN(s: str) -> str: return _c("36", s)def RED(s: str) -> str: return _c("31", s)def make_client(api_key: str) -> OpenAI: """Kong reads the apikey header; the OpenAI SDK reserves api_key for Authorization.""" return OpenAI( base_url=f"{PROXY_URL}/mistral", api_key="unused", default_headers={"apikey": api_key}, )def section(title: str) -> None: bar = "=" * 70 print(f"\n{bar}\n{BOLD(title)}\n{bar}")def chat(client: OpenAI, prompt: str) -> None: print(f"\n{BOLD('[REQUEST]')} model={CHAT_MODEL!r} prompt={prompt!r}") start_ms = round(time.time() * 1000) try: raw = client.chat.completions.with_raw_response.create( model=CHAT_MODEL, 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() usage = completion.usage 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", ".") print(f"[RESPONSE] {DIM(completion.choices[0].message.content)}") print(f"{GREEN(BOLD('[ROUTED TO]'))} alias={CHAT_MODEL!r} -> upstream model={CYAN(BOLD(upstream_model))}") print(f"[TOKENS] {DIM(f'prompt={usage.prompt_tokens} completion={usage.completion_tokens} total={usage.total_tokens}')}") print(f"[LATENCY] {DIM(f'upstream={upstream_latency}ms proxy={proxy_latency}ms total={elapsed_ms}ms')}")def stream(client: OpenAI, prompt: str) -> None: print(f"\n{BOLD('[REQUEST]')} model={CHAT_MODEL!r} stream=True prompt={prompt!r}") start_ms = round(time.time() * 1000) first_token_ms = None chunks = 0 text = [] for chunk in client.chat.completions.create( model=CHAT_MODEL, messages=[{"role": "user", "content": prompt}], stream=True, ): piece = chunk.choices[0].delta.content if piece: if first_token_ms is None: first_token_ms = round(time.time() * 1000) - start_ms chunks += 1 text.append(piece) elapsed_ms = round(time.time() * 1000) - start_ms print(f"[RESPONSE] {DIM(''.join(text))}") print(f"{GREEN(BOLD('[STREAMED]'))} {chunks} chunks {DIM(f'first token={first_token_ms}ms total={elapsed_ms}ms')}")def embed(client: OpenAI, texts) -> None: print(f"\n{BOLD('[REQUEST]')} model={EMBEDDINGS_MODEL!r} inputs={len(texts)}") start_ms = round(time.time() * 1000) raw = client.embeddings.with_raw_response.create(model=EMBEDDINGS_MODEL, input=texts) elapsed_ms = round(time.time() * 1000) - start_ms result = raw.parse() upstream_model = raw.headers.get("x-kong-llm-model", ".") print(f"[RESPONSE] {DIM(f'{len(result.data)} vectors of {len(result.data[0].embedding)} dimensions')}") print(f"{GREEN(BOLD('[ROUTED TO]'))} alias={EMBEDDINGS_MODEL!r} -> upstream model={CYAN(BOLD(upstream_model))}") print(f"[TOKENS] {DIM(f'prompt={result.usage.prompt_tokens} total={result.usage.total_tokens}')}") print(f"[LATENCY] {DIM(f'total={elapsed_ms}ms')}")def main() -> None: client = make_client(API_KEY) section("1. Chat, in OpenAI format, answered by Mistral") chat(client, "In one sentence, what is an API gateway?") section("2. The same endpoint, streamed") stream(client, "Name three French cities, comma separated.") section("3. Embeddings, same credential, same base path") embed(client, [ "Kong AI Gateway sits between clients and Mistral.", "The AI Model Provider holds the Mistral API key.", ]) bad_key = "not-a-real-key" section("4. Revoked or unknown key. Kong rejects before calling Mistral") print(f"{DIM(f'attempting with apikey={bad_key!r}')}") chat(make_client(bad_key), "In one sentence, what is an API gateway?") section("Done.")if __name__ == "__main__": try: main() except KeyboardInterrupt: sys.exit(130)
======================================================================1. Chat, in OpenAI format, answered by Mistral======================================================================[REQUEST] model='mistral-chat' prompt='In one sentence, what is an API gateway?'[RESPONSE] An **API gateway** is a server that acts as an intermediary between clients and backend services, routing requests, aggregating responses, and enforcing policies like authentication, rate limiting, and load balancing.[ROUTED TO] alias='mistral-chat' -> upstream model=mistral/mistral-small-latest[TOKENS] prompt=25 completion=41 total=66[LATENCY] upstream=881ms proxy=6ms total=1171ms======================================================================2. The same endpoint, streamed======================================================================[REQUEST] model='mistral-chat' stream=True prompt='Name three French cities, comma separated.'[RESPONSE] Paris, Lyon, Marseille[STREAMED] 3 chunks first token=395ms total=424ms======================================================================3. Embeddings, same credential, same base path======================================================================[REQUEST] model='mistral-embeddings' inputs=2[RESPONSE] 2 vectors of 1024 dimensions[ROUTED TO] alias='mistral-embeddings' -> upstream model=mistral/mistral-embed[TOKENS] prompt=27 total=27[LATENCY] total=1167ms======================================================================4. Revoked or unknown key. Kong rejects before calling Mistral======================================================================attempting with apikey='not-a-real-key'[REQUEST] model='mistral-chat' prompt='In one sentence, what is an API gateway?'[BLOCKED] 401 Error code: 401 - {'message': 'Unauthorized'} (6ms)======================================================================Done.======================================================================
The OpenAI SDK talked to Mistral without knowing it. The client set base_url to Kong and sent an OpenAI-shaped request. formats: [{type: openai}] on the AI Model translated it into Mistral’s contract and translated the reply back. No Mistral SDK is installed; the only dependency is openai.
The client sent an alias, not a model name. Every call used model: mistral-chat, which config.route.model resolved to the mistral-small-latest target. X-Kong-LLM-Model reports what actually served the request. Re-pointing that target is a one-line change, invisible to callers.
Streaming worked over the same endpoint. Adding stream=True produced incremental chunks with a first token at 395 ms, with no separate route or model.
Chat and embeddings shared one credential and one provider. The embeddings call went to /mistral/embeddings and returned 1024-dimension vectors from mistral-embed, authenticated by the same AI Consumer key and backed by the same AI Model Provider. Two capabilities, one base path, one credential.
The invalid key never reached Mistral. Kong rejected it in 6 ms, against the 881 ms the real call above took once it reached Mistral. No Mistral quota was spent, and no token was billed.
The Mistral key stayed in Konnect. The script only ever held the AI Consumer credential. The Mistral key lived on the AI Model Provider as a write-only field and was injected server-side.
Open Konnect and select the mistral-ai-with-kong-ai-gateway-recipe AI Gateway. It holds five resources created by this recipe:
One AI Model Provider, mistral-ai-with-kong-ai-gateway-provider, holding the Mistral credential. The key is write-only and is never displayed after creation.
Two AI Models, mistral-ai-with-kong-ai-gateway-chat and mistral-ai-with-kong-ai-gateway-embeddings, each with its own capability, alias rule, and target.
One AI Auth Strategy, mistral-ai-with-kong-ai-gateway-auth, referenced by both AI Models.
One AI Consumer, rag-service, and its credential. Deleting that credential revokes the application immediately, without touching the Mistral key.
Kong AI Gateway records token consumption, request latency, and per-provider cost for every AI Model with no extra configuration, so you can monitor spend and see which AI Models drive the most usage. Platform-wide telemetry lives under the Observability menu.
Add a second tier. Add a third AI Model on the same provider with its own alias and a mistral-medium-latest target, and let callers pick per request. Set input_cost and output_cost on each target so Konnect reports spend per tier. See Model cost management.
Budget each application. Attach an ai-rate-limiting-advanced AI Policy to cap tokens per AI Consumer, so one misbehaving application can’t drain the account. The AI policy counts tokens rather than requests, which is the correct unit for LLM cost control.
Add guardrails. Attach ai-prompt-guard to filter prompts by pattern, or ai-sanitizer to strip PII before it reaches Mistral.
Fail over to another provider. An AI Model can hold targets from different AI Model Providers. Add a second provider and target alongside the Mistral one, then set config.balancer to distribute or fail over between them. See Load balancing.
Swap keys for tokens. Replace the key-auth AI Auth Strategy with openid-connect to accept tokens your identity provider already issues. An AI Model supports one of each simultaneously.
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: