- LINUX.DO Indeed a very friendly community. Thank y'all at
LINUX.DOfor giving me inspiration and courage.
- This README.md is partially generated by Codex. I apologize for the potential inconveninece of comprehension due to the limitations of AI-generated text.
- Kiyomizu is still a PoC. Treat it as a sharp tool, not a managed cloud product.
- Localhost is still the cleanest deployment. VPS/LAN exposure is now guarded by default, but you should put TLS and ordinary network controls in front of it.
- I'd be very grateful if you help make Kiyomizu more robust and more usable!
Kiyomizu is a local Kotlin/Ktor proxy for LLM APIs to turn a boring API call to a personified AI companion. It sits between your client and an upstream provider, rewrites requests when useful, preserves streaming, and exposes a small web UI for live configuration.
Kiyomizu manages stable prefix for a higher cache-hit rate, and help your AI companion recall memories that matters.
- Proxies OpenAI-style, Anthropic Messages, and Gemini
generateContentrequests. - Injects Anthropic prompt-cache breakpoints into stable conversation history.
- Strips historical thinking and reasoning blocks to keep cacheable prefixes stable.
- Preserves SSE and text streaming instead of buffering the full response.
- Exposes a browser UI at
/and a JSON config API at/api/config. - Protects
/api/configwith a config password. On public binds, first-run setup from the network is disabled unless explicitly allowed. - Requires proxy authentication on public binds, strips internal auth headers before forwarding, and rejects unsafe outbound URLs by default.
- Includes an optional companion-memory layer backed by SQLite graph memory, local text search, and reflection summaries.
The request flow is:
- Accept a request from your client.
- Detect the request envelope: Chat Completions, Responses, Anthropic Messages, or Gemini.
- Optionally rewrite the payload for cache stability and companion memory injection.
- Forward the request upstream with cleaned headers.
- Stream the upstream response back to the client.
- If companion memory is enabled, asynchronously extract memories from the latest exchange and store them in SQLite.
Main files:
- src/main/kotlin/hifumi/kiyomizu/Main.kt: server bootstrap, routing, proxy loop
- src/main/kotlin/hifumi/kiyomizu/MessagePatcher.kt: request-shaping, cache injection, memory prompt injection
- src/main/kotlin/hifumi/kiyomizu/ProxyService.kt: header cleanup, upstream client, request logging
- src/main/kotlin/hifumi/kiyomizu/MemoryService.kt: graph-memory extraction, recall, deep recall, decay jobs
- src/main/kotlin/hifumi/kiyomizu/DatabaseService.kt: SQLite schema and persistence
- src/main/resources/ui.html: configuration UI
- JDK 17 or newer
- Network access to your upstream LLM provider
- Gradle wrapper included in the repo
./gradlew runDefault address:
http://127.0.0.1:8787
Useful endpoints:
/: configuration UI/ui: same UI alias/health: plain-text health summary/api/config: config read/write API/v1/modelsand related model-list paths: proxied upstream/{...}: catch-all proxy route
The Compose setup builds the fat jar in a container, stores the SQLite database in ./storage, and exposes Kiyomizu on ${KIYOMIZU_PORT:-8787}.
The Dockerfile expects gradle-9.5.1-bin.zip in the repository root. This avoids downloading the Gradle distribution during image build.
KIYOMIZU_CONFIG_PASSWORD='use-a-long-random-config-secret' \
KIYOMIZU_PROXY_PASSWORD='use-a-different-long-random-proxy-secret' \
UPSTREAM_URL='https://api.anthropic.com' \
docker compose up --buildFor public or LAN binds, keep KIYOMIZU_ALLOW_UNAUTHENTICATED_PROXY=0 and put TLS in front of the service.
A common VPS layout: Kiyomizu listens on 127.0.0.1:8787, Nginx terminates HTTPS for https://your.domain.example, and a local upstream (for example CPA at http://127.0.0.1:8317) sits on the same host.
In that setup:
- Set
HOST=127.0.0.1andKIYOMIZU_ALLOW_PRIVATE_UPSTREAMS=1when the upstream is HTTP on loopback. - Set
KIYOMIZU_ALLOW_BROWSER_CORS=1so the browser UI can call the API when opened via your public hostname (loopback bind disables CORS by default). - Set
KIYOMIZU_CORS_ALLOWED_HOSTSto the hostname(s) users type in the browser, comma-separated (host only or fullhttps://…URLs are accepted). Example:KIYOMIZU_CORS_ALLOWED_HOSTS=your.domain.example. Without this, CORS only allowslocalhost/127.0.0.1and cross-origin requests from your public site will fail.
Example Compose file for Linux host networking: docker-compose.host-local.yml (network_mode: host, HOST=127.0.0.1, private upstream and CORS env vars wired).
./gradlew build
./gradlew testKiyomizu reads startup configuration from environment variables and also supports live updates through /api/config.
By default, the companion SQLite database now lives in the OS data directory:
- macOS:
~/Library/Application Support/Kiyomizu/kiyomizu_companion.db - Linux:
~/.local/share/Kiyomizu/kiyomizu_companion.db - Windows:
%APPDATA%/Kiyomizu/kiyomizu_companion.db
You can override that path with KIYOMIZU_DB_FILE or the JVM property -Dkiyomizu.db.file=....
| Variable | Default | Meaning |
|---|---|---|
HOST |
127.0.0.1 |
Bind address |
PORT |
8787 |
Server port |
KIYOMIZU_CONFIG_PASSWORD |
empty | Optional startup password override. Use at least 12 characters. /api/config requires this password via X-Kiyomizu-Config-Password or Basic auth |
KIYOMIZU_PROXY_PASSWORD |
empty | Optional dedicated proxy password. When HOST is not loopback, proxy routes require X-Kiyomizu-Proxy-Password or Proxy-Authorization; if unset, the config password is accepted instead |
KIYOMIZU_ALLOW_UNAUTHENTICATED_PROXY |
0 |
Dangerous escape hatch. Set 1 only behind a trusted reverse proxy or private firewall |
KIYOMIZU_ALLOW_REMOTE_PASSWORD_SETUP |
0 |
Allows first-run password setup on a public bind. Prefer KIYOMIZU_CONFIG_PASSWORD instead |
KIYOMIZU_ALLOW_PRIVATE_UPSTREAMS |
0 |
Allows http, localhost, private IPs, and metadata-like hosts for upstream/memory URLs. Keep this off on VPS unless you truly need a local model router |
KIYOMIZU_ALLOW_BROWSER_CORS |
auto-local | Enables browser CORS when public-bound. By default CORS is local-bind only |
KIYOMIZU_CORS_ALLOWED_HOSTS |
empty | Comma-separated hostnames (or https://host/… URLs) allowed in CORS when the UI is served via a reverse proxy on a public domain while Kiyomizu binds loopback. Always includes localhost and 127.0.0.1 |
KIYOMIZU_MAX_PROXY_REQUEST_BYTES |
26214400 |
Maximum proxied request body size |
KIYOMIZU_MAX_CONFIG_REQUEST_BYTES |
131072 |
Maximum config/password request body size |
KIYOMIZU_VERBOSE_HEALTH |
0 |
Makes /health include config details. Default is only ok |
PRESET |
custom |
custom or anthropic |
UPSTREAM_URL |
empty | Upstream base URL |
OPENROUTER_BASE_URL |
empty | Fallback upstream when UPSTREAM_URL is unset |
CACHE_TTL |
1h |
5m, 1h, or none |
CACHE_MODE |
explicit |
explicit or automatic |
CACHE_STRATEGY |
stable-prefix |
stable-prefix or last |
CACHE_BREAKPOINTS |
4 |
Number of explicit cache markers, 0..4 |
SEND_TOP_LEVEL_CACHE_CONTROL |
0 |
Add top-level cache_control even outside automatic mode |
DYNAMIC_TAIL_MESSAGES |
1 |
Tail messages excluded from the stable cacheable prefix |
MEMORY_SUMMARY_WINDOW_ROUNDS |
2 |
Prior user/assistant rounds fed to the summarization model alongside the current round, so it can resolve pronouns/deixis and catch facts surfacing across consecutive turns. 0 keeps the legacy single-round behaviour. |
MEMORY_SUMMARY_WINDOW_MAX_CHARS |
12000 |
Hard total-character cap on the assembled lookback history; when exceeded the oldest whole round is dropped (current round always kept intact). |
STRIP_THINKING |
1 |
Remove stored reasoning/thinking blocks from message history |
MODEL_FILTER |
anthropic,claude |
Models eligible for Anthropic-specific patching |
ANTHROPIC_BETA |
extended-cache-ttl-2025-04-11 |
Beta header sent on Anthropic Messages requests |
| Variable | Default | Meaning |
|---|---|---|
MEMORY_ENABLED |
0 |
Enables the memory system |
MEMORY_SUMMARY_URL |
https://generativelanguage.googleapis.com |
Summary/state model base URL |
MEMORY_SUMMARY_KEY |
empty | API key for summary/state extraction |
MEMORY_SUMMARY_MODEL |
gemini-2.5-flash |
Summary/state model |
MEMORY_SUMMARY_PROMPT |
built-in prompt | Extraction prompt for graph nodes, edges, and relationship deltas |
MEMORY_DECAY_INTERVAL_HOURS |
24 |
Decay/reflection job interval |
MEMORY_DECAY_RATE |
0.1 |
Strength decay applied to all memories |
MEMORY_THRESHOLD |
0.1 |
Memories below this strength are deleted |
MEMORY_RECOVERY_AMOUNT |
0.3 |
Strength restored when a memory is recalled |
MEMORY_MAX_STRENGTH |
1.0 |
Upper bound for memory strength |
MEMORY_INITIAL_STRENGTH |
0.8 |
Base strength multiplier for new memories |
INTIMACY_DECAY_RATE |
0.5 |
Daily relationship decay when interaction stops |
MEMORY_DECAY_TAU_HOURS |
360 |
Base Ebbinghaus-style decay constant |
MEMORY_SALIENCE_K |
1.0 |
Emotional salience multiplier |
MEMORY_RECALL_MAX_NODES |
6 |
Max normal recall clues injected per request |
MEMORY_DEEP_RECALL_ENABLED |
1 |
Enables explicit deep recall when the user asks to remember |
MEMORY_DEEP_RECALL_MAX_CANDIDATES |
40 |
Search budget for deep recall |
MEMORY_DEEP_RECALL_MAX_CLUES |
10 |
Max deep-recall clues injected into one response |
MEMORY_PERSON_CONTEXT_MAX_CLUES |
2 |
Max person-context clues injected per request |
custom is a dumb pipe by design. Kiyomizu forwards headers and payloads without Anthropic-specific surgery, aside from optional companion-memory injection when enabled.
Use it for OpenAI-compatible gateways, OpenRouter-style endpoints, local model routers, or anything else that should mostly be left alone.
anthropic is opinionated. It:
- rewrites
Authorization: Bearer ...intox-api-key - ensures
anthropic-version: 2023-06-01 - injects explicit cache markers in
explicitmode - can send top-level
cache_controlinautomaticmode - strips reasoning blocks from prior history when configured
If you want Anthropic prompt caching to behave like a disciplined machine instead of a lottery ticket, use this preset.
stable-prefix is the default and the better choice. It treats the last DYNAMIC_TAIL_MESSAGES as volatile and spreads cache breakpoints across the older, stable region.
last is narrower. It places the effective cache boundary at the last stable message.
Thinking-strip behavior matters because reasoning blocks mutate constantly. Leaving them in history is a fine way to sabotage your own cache hits.
The memory layer is not a transcript archive. It is a graph-memory system with local search:
- After a response, Kiyomizu summarizes the latest user/assistant exchange with a separate model.
- That summary returns graph nodes, graph edges, and relationship deltas.
- Nodes are stored in SQLite as searchable text records, derived search terms, and lightweight graph links.
- Normal recall uses local search plus graph expansion. It does not call an embedding model.
- Deep recall runs only when the user explicitly asks Kiyomizu to remember or recall earlier interactions.
- A background maintenance job decays weak memories and can generate short reflection diary entries.
SQLite tables:
relationship_statememory_nodesmemory_edgesmemory_search_termsreflections
The legacy memories table is still kept for database compatibility, but the active memory path no longer depends on embeddings or vector search.
Database file:
kiyomizu_companion.db
Open the UI here:
http://127.0.0.1:8787/
Read config:
curl http://127.0.0.1:8787/api/config \
-H 'X-Kiyomizu-Config-Password: your-password'Update config:
curl -X POST http://127.0.0.1:8787/api/config \
-H 'Content-Type: application/json' \
-H 'X-Kiyomizu-Config-Password: your-password' \
-d '{
"preset": "anthropic",
"upstream": "https://api.anthropic.com",
"cache_mode": "explicit",
"cache_strategy": "stable-prefix",
"cache_breakpoints": 4
}'When password protection is enabled:
curl http://127.0.0.1:8787/api/config \
-H 'X-Kiyomizu-Config-Password: your-password'On first local run without KIYOMIZU_CONFIG_PASSWORD, open the Web UI and set the config password before reading or changing settings. On public binds, set KIYOMIZU_CONFIG_PASSWORD before startup; remote first-run setup is blocked by default.
Secrets are not exposed by the public config payload. The API reports whether memory keys are configured, but not their values.
Run public deployments behind HTTPS. Kiyomizu does not terminate TLS.
When HOST is 0.0.0.0, ::, or any non-loopback address:
- Proxy routes require
X-Kiyomizu-Proxy-PasswordorProxy-Authorization. - The first-run password setup endpoint is disabled unless
KIYOMIZU_ALLOW_REMOTE_PASSWORD_SETUP=1. - Browser CORS is disabled unless
KIYOMIZU_ALLOW_BROWSER_CORS=1. - Outbound URLs must be HTTPS public hosts with no credentials, query string, or fragment. Private networks and metadata services are blocked unless
KIYOMIZU_ALLOW_PRIVATE_UPSTREAMS=1. /healthreturns onlyokunlessKIYOMIZU_VERBOSE_HEALTH=1.
Example:
HOST=0.0.0.0 \
KIYOMIZU_CONFIG_PASSWORD='use-a-long-random-config-secret' \
KIYOMIZU_PROXY_PASSWORD='use-a-different-long-random-proxy-secret' \
UPSTREAM_URL='https://api.anthropic.com' \
./gradlew run- Base URL:
http://127.0.0.1:8787/v1 - API key: your Anthropic key
- Public bind: also send
X-Kiyomizu-Proxy-PasswordorProxy-Authorizationif your client supports it. Otherwise put Kiyomizu behind a reverse proxy that performs authentication before traffic reaches it.
- Base URL:
http://127.0.0.1:8787/v1 - API key: whatever your upstream expects
- Preset:
custom
Clients calling /v1/models can point at Kiyomizu directly; the request is proxied upstream.
Kiyomizu writes structured JSON request summaries to stdout. These logs include whether a request was patched, how many thinking blocks were removed, and where explicit cache breakpoints were inserted.
That is enough to answer the only question that matters: did the proxy help, or did it merely make noise.
- The server allows local/private-network CORS only for local binds by default.
- Streaming text and SSE responses are passed through without full buffering.
- The memory database is created on startup when needed and tightened to owner-only permissions on POSIX filesystems.
- The project uses Kotlin
2.0.21, Ktor2.3.12, and SQLite viaorg.xerial:sqlite-jdbc.