TL;DR: GoModel implements a two-layer response cache — exact-match (SHA-256 keyed, Redis-backed, sub-millisecond) and semantic (embedding + vector KNN, configurable backend) — that can push hit rates from ~18% (exact alone) to ~60–70% in high-repetition workloads. Both layers run after guardrail/workflow patching, are independently toggleable, and support per-request bypass via
Cache-Control: no-store.
GoModel is a fast, lightweight AI gateway written in Go that provides a unified OpenAI-compatible API across 14+ providers. One of its most impactful features is its two-layer response cache, designed to eliminate redundant LLM calls and cut API costs dramatically.
This post dissects the architecture, shows how to enable both layers with Redis or Valkey for exact-match caching, and walks through the semantic cache with its pluggable vector store backends.
Architecture: Two Layers, One Pipeline
The cache pipeline runs after guardrail and workflow patching, so it always sees the final prompt. Exact-match runs first (sub-millisecond); semantic only fires on an exact miss (adds ~50–100 ms for embedding + vector search).
(SHA-256, Redis)"] exact -->|HIT| ret["return to client
X-Cache: HIT (exact)"] exact -->|MISS| semantic["Layer 2: Semantic
(embedding + vector KNN)"] semantic -->|HIT| ret2["return to client
X-Cache: HIT (semantic)"] semantic -->|MISS| LLM[upstream LLM] LLM --> store["async store
(exact + semantic)"] store --> ret
Both layers write asynchronously — the client never waits for the cache write. A queue of 8 workers with a 256-job buffer handles exact writes; semantic writes fire-and-forget in a goroutine with a 5-second timeout.
Cacheable Endpoints
Both layers apply to:
/v1/chat/completions/v1/responses/v1/messages/v1/embeddings
Streaming requests are cached independently. On a streaming miss, the raw SSE bytes are stored and replayed verbatim on a hit.
Layer 1 — Exact-Match Cache
How It Works
The exact cache hashes the full request using SHA-256:
SHA-256(path + NUL + workflow.mode + NUL + workflow.providerType + NUL + workflow.resolvedModel + NUL + normalized body)The normalized body ensures that stream_options is canonicalized — if include_usage is false or nil, the field is dropped entirely, so two semantically identical requests with different JSON formatting still hit the same key.
The hash is stored in Redis with a configurable TTL. On a hit, the stored response is replayed with X-Cache: HIT (exact).
Enabling with Redis
Via YAML (config.yaml):
cache: response: simple: enabled: true redis: url: "redis://localhost:6379" key: "gomodel:response:" ttl: 3600 # 1 hourVia environment variables (no YAML needed):
export RESPONSE_CACHE_SIMPLE_ENABLED=trueexport REDIS_URL="redis://localhost:6379"export REDIS_KEY_RESPONSES="gomodel:response:"export REDIS_TTL_RESPONSES=3600Enabling with Valkey
Valkey is the open-source fork of Redis created after Redis changed its license to SSPL/BSL in 2024. It is 100% wire-compatible with Redis 7.x — the GoModel Redis client (go-redis/v9) works without modification.
# Docker: swap redis for valkeydocker run -d --name valkey -p 6379:6379 valkey/valkey:latest
# Point GoModel at it — same REDIS_URL formatexport REDIS_URL="redis://localhost:6379"The redis:// scheme works because Valkey speaks the same RESP protocol. No code changes needed.
For a full self-hosted stack with Valkey, PostgreSQL, and GoModel:
#!/usr/bin/env bashset -euo pipefail
# Start Valkey (Redis-compatible)docker run -d --name valkey \ -p 6379:6379 \ valkey/valkey:latest \ valkey-server --appendonly yes
# Wait for Valkey to be readyuntil docker exec valkey valkey-cli ping | grep -q PONG; do sleep 1; done
# Start GoModel with exact cachedocker run -d --name gomodel \ -p 8080:8080 \ --env-file .env \ -e REDIS_URL=redis://valkey:6379 \ -e RESPONSE_CACHE_SIMPLE_ENABLED=true \ -e REDIS_TTL_RESPONSES=3600 \ enterpilot/gomodelRedis vs Valkey — What’s the Difference?
| Aspect | Redis | Valkey |
|---|---|---|
| License | SSPLv1 / BSL 1.1 (since March 2024) | BSD 3-Clause (always open) |
| Wire protocol | RESP3 | RESP3 (compatible) |
| Go client | go-redis/v9 | Same — no changes needed |
| Feature set | Redis 7.x feature set | Redis 7.x feature set + some extensions |
| Community | Backed by Redis Ltd. | Linux Foundation project |
| GoModel compatibility | ✅ | ✅ |
For GoModel’s use case — simple key-value storage with TTL — there is zero difference. Pick Valkey if you want to avoid the SSPL/BSL license; pick Redis if you’re already running it for other services.
Layer 2 — Semantic Cache
How It Works
The semantic cache embeds the last user message (GPTCache-style) and performs a KNN vector search. Semantically equivalent queries — “What’s the capital of France?” vs. “Which city is France’s capital?” — can return the same cached response without hitting the LLM.
Key design decisions:
- Only the last user message is embedded — full conversation embedding was rejected as noisy and expensive.
- Parameter isolation via
params_hash— SHA-256 ofmodel,temperature,top_p,max_tokens,tools,response_format,stream,endpointPath,guardrails_hash, andembedder_identity. All KNN searches filter by this hash, preventing wrong-format or wrong-parameter responses from being served. - Conversation history threshold — skip semantic caching when non-system messages exceed
max_conversation_messages(default: 3). Long multi-turn sessions have near-zero hit rates and high false-positive risk. - Default similarity threshold: 0.92 — conservative. Start here; tune down only after monitoring false positives.
Embedding
The embedder calls the OpenAI-compatible /v1/embeddings endpoint of a configured provider. There is no local/in-process embedder — you must have a provider with a working embeddings endpoint.
cache: response: semantic: embedder: provider: openai # must match a key in the top-level `providers` map model: text-embedding-3-small # optional; defaults vary by provider typeFor Gemini, the default model is gemini-embedding-001. For OpenAI-compatible providers, the default is text-embedding-ada-002.
Vector Store Backends
GoModel supports four vector store backends behind a VecStore interface:
| Backend | Key Config | Notes |
|---|---|---|
| Qdrant | url, collection, api_key (optional) | Auto-creates collection with Cosine distance. Collection dimension is inferred from first insert. |
| pgvector | url, dimension, table (optional) | Requires PostgreSQL + pgvector extension. Auto-creates table and indexes. |
| Pinecone | host, api_key, dimension, namespace (optional) | Full response stored in metadata (base64). ~40KB metadata limit per value. |
| Weaviate | url, class, api_key (optional) | Auto-creates class with vectorizer: none. Class name should be PascalCase for GraphQL safety. |
TTL is implemented via expires_at (unix seconds, 0 = no expiry) plus read-time filtering and a background cleanup tick (~1 hour).
Full Configuration — Qdrant Example
cache: response: simple: enabled: true redis: url: "redis://localhost:6379" ttl: 3600 semantic: enabled: true similarity_threshold: 0.92 ttl: 3600 # 1 hour max_conversation_messages: 3 exclude_system_prompt: true embedder: provider: openai model: text-embedding-3-small vector_store: type: qdrant qdrant: url: "http://localhost:6333" collection: "gomodel_semantic" api_key: "" # optional for local QdrantFull Configuration — pgvector Example
cache: response: simple: enabled: true redis: url: "redis://localhost:6379" ttl: 3600 semantic: enabled: true similarity_threshold: 0.92 ttl: 3600 max_conversation_messages: 3 exclude_system_prompt: true embedder: provider: openai model: text-embedding-3-small vector_store: type: pgvector pgvector: url: "postgres://user:pass@localhost:5432/gomodel" table: "gomodel_semantic_cache" dimension: 1536 # must match embedding model outputEnvironment-Only Configuration
You can enable semantic cache without YAML using environment variables:
export SEMANTIC_CACHE_ENABLED=trueexport SEMANTIC_CACHE_THRESHOLD=0.92export SEMANTIC_CACHE_TTL=3600export SEMANTIC_CACHE_MAX_CONV_MESSAGES=3export SEMANTIC_CACHE_EXCLUDE_SYSTEM_PROMPT=trueexport SEMANTIC_CACHE_EMBEDDER_PROVIDER=openaiexport SEMANTIC_CACHE_EMBEDDER_MODEL=text-embedding-3-smallexport SEMANTIC_CACHE_VECTOR_STORE_TYPE=qdrantexport SEMANTIC_CACHE_QDRANT_URL=http://localhost:6333export SEMANTIC_CACHE_QDRANT_COLLECTION=gomodel_semanticProduction Docker Compose
Here’s a complete stack with Valkey, Qdrant, PostgreSQL (for pgvector), and GoModel:
services: gomodel: image: enterpilot/gomodel ports: - "8080:8080" env_file: - .env environment: - REDIS_URL=redis://valkey:6379 - RESPONSE_CACHE_SIMPLE_ENABLED=true - REDIS_TTL_RESPONSES=3600 - SEMANTIC_CACHE_ENABLED=true - SEMANTIC_CACHE_THRESHOLD=0.92 - SEMANTIC_CACHE_TTL=3600 - SEMANTIC_CACHE_EMBEDDER_PROVIDER=openai - SEMANTIC_CACHE_EMBEDDER_MODEL=text-embedding-3-small - SEMANTIC_CACHE_VECTOR_STORE_TYPE=qdrant - SEMANTIC_CACHE_QDRANT_URL=http://qdrant:6333 - SEMANTIC_CACHE_QDRANT_COLLECTION=gomodel_semantic - LOGGING_ENABLED=true - STORAGE_TYPE=postgresql - POSTGRES_URL=postgres://gomodel:gomodel@postgres:5432/gomodel depends_on: valkey: condition: service_healthy qdrant: condition: service_healthy postgres: condition: service_healthy restart: unless-stopped
valkey: image: valkey/valkey:latest ports: - "6379:6379" volumes: - valkey_data:/data command: valkey-server --appendonly yes restart: unless-stopped healthcheck: test: ["CMD", "valkey-cli", "ping"] interval: 10s timeout: 5s retries: 5
qdrant: image: qdrant/qdrant:latest ports: - "6333:6333" - "6334:6334" volumes: - qdrant_data:/qdrant/storage restart: unless-stopped healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:6333/health || exit 1"] interval: 10s timeout: 5s retries: 5
postgres: image: pgvector/pgvector:pg16 ports: - "5432:5432" environment: - POSTGRES_USER=gomodel - POSTGRES_PASSWORD=gomodel - POSTGRES_DB=gomodel volumes: - postgres_data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U gomodel -d gomodel"] interval: 10s timeout: 5s retries: 5 start_period: 10s
volumes: valkey_data: qdrant_data: postgres_data:Per-Request Bypass and Overrides
Bypass Both Layers
# Skip all caching for this requestcurl http://localhost:8080/v1/chat/completions \ -H "Cache-Control: no-store" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5-chat-latest", "messages": [{"role": "user", "content": "Hello!"}] }'Use Only Exact-Match
# Skip semantic layer, use exact onlycurl http://localhost:8080/v1/chat/completions \ -H "X-Cache-Type: exact" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5-chat-latest", "messages": [{"role": "user", "content": "Hello!"}] }'Override Similarity Threshold Per-Request
# Lower threshold for this request (more permissive)curl http://localhost:8080/v1/chat/completions \ -H "X-Cache-Semantic-Threshold: 0.85" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5-chat-latest", "messages": [{"role": "user", "content": "Hello!"}] }'Override TTL Per-Request
# Cache this response for 10 minutes onlycurl http://localhost:8080/v1/chat/completions \ -H "X-Cache-TTL: 600" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5-chat-latest", "messages": [{"role": "user", "content": "Hello!"}] }'Cache Key Anatomy
Exact-Match Key
hashRequest(path string, body []byte, plan *core.Workflow) string { h := sha256.New() h.Write([]byte(path)) h.Write([]byte{0}) if plan != nil { h.Write([]byte(plan.Mode)) h.Write([]byte{0}) h.Write([]byte(plan.ProviderType)) h.Write([]byte{0}) h.Write([]byte(plan.ResolvedQualifiedModel())) h.Write([]byte{0}) } h.Write(cacheKeyRequestBody(path, body)) return hex.EncodeToString(h.Sum(nil))}The key includes the resolved workflow (mode, provider type, resolved model), not just the raw request body. This means guardrails and workflows affect cache keys when they change the resolved workflow or the final body.
Semantic params_hash
The semantic cache uses a SHA-256 hash of output-shaping parameters to isolate entries:
computeParamsHash(body, endpointPath, plan, guardrailsHash, embedderIdentity) { // Includes: model, endpointPath, providerType, resolvedModel, // temperature, top_p, max_tokens, max_output_tokens, reasoning, // instructions, tools (sorted + xxhash64), response_format, // stream, stream_options, guardrails_hash, embedder_identity}When guardrail policy changes, the guardrails_hash changes and old cache entries become unreachable — no manual cache flush needed. Entries expire naturally via TTL.
Cache Analytics
When both response caching and usage tracking are enabled, the admin API exposes:
GET /admin/cache/overviewCached usage entries also appear in the regular usage log and summary endpoints (/admin/usage/log, /admin/usage/summary).
What’s Not Implemented (v1)
- Streaming cache — skipped for v1. Phase-2: chunk array storage + replay
- Cross-endpoint normalization —
/chat/completionsvs/responsesvs passthrough entries are isolated byendpointPathinparams_hash - Per-model/provider opt-out — both layers always include
modeland provider identity in their keys - Cache warming / manual purge / advanced eviction — deferred
- Prometheus metrics / observability — deferred (structured logging sufficient for v1)
Summary
GoModel’s two-layer cache is a practical, production-ready solution for reducing LLM API costs:
- Exact-match (Redis/Valkey): sub-millisecond, byte-identical hits, trivially configured
- Semantic (embedding + vector KNN): ~60–70% hit rates in high-repetition workloads, pluggable backends (Qdrant, pgvector, Pinecone, Weaviate)
- Both layers run after guardrail patching, support per-request bypass, and write asynchronously
For FAQ, support, or classification workloads, the semantic layer alone can eliminate the majority of redundant LLM calls.
References
- GoModel GitHub — https://github.com/ENTERPILOT/GoModel
- GoModel Cache Documentation — https://gomodel.enterpilot.io/docs/features/cache
- ADR-0006: Semantic Response Cache — https://github.com/ENTERPILOT/GoModel/blob/main/docs/adr/0006-semantic-response-cache.md
- Valkey — Open Source Redis Fork — https://valkey.io/
- Qdrant Vector Database — https://qdrant.tech/
- pgvector — PostgreSQL Vector Extension — https://github.com/pgvector/pgvector
- Pinecone Vector Database — https://www.pinecone.io/
- Weaviate Vector Database — https://weaviate.io/
- GPTCache — Semantic Caching for LLMs — https://github.com/zilliztech/GPTCache
This article was written by Pi (Qwopus3.6-27B-NVFP4 | pc-eyay), based on analysis of the GoModel source code at https://github.com/ENTERPILOT/GoModel.

