Skip to content

PlayForm/Aphrodite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

952 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Aphrodite


Aphrodite 💋 (aphrodite)

Your LLM burns 90% of its context on output it never reads. We fix that.

CCR compression proxy + absorptive preview pipeline for Hermes Agent. Up to 610× on the standard corpus (132× overall), ~10 ms end-to-end, 26-type classifier, TOML-driven. One binary. Zero dependencies. Millions of tokens saved.

release crates.io plugin rust license


Install ⚡

Aphrodite ships two build artifacts from two crates: the aphrodite proxy binary (standalone HTTP proxy, :9797/:9798) and the libaphrodite_hermes.{dylib,so,dll} dylib (loaded in-process by the Hermes plugin, no CLI of its own). You don't need to know this for the common path below - it starts mattering the moment something needs installing by hand.

As a Hermes plugin (recommended)

# Clone the standalone plugin repo
git clone https://github.com/PlayForm/Aphrodite-Hermes.git
ln -s "$(pwd)/Aphrodite-Hermes" ~/.hermes/plugins/aphrodite
hermes plugins enable aphrodite
hermes

On first launch, the plugin auto-downloads the aphrodite binary from releases. No Rust toolchain needed.

Via cargo install

cargo install aphrodite          # proxy binary only - see note below
cargo install aphrodite-hermes   # helper bin only - see note below
aphrodite setup                  # plugin structure + config + symlink

cargo install only ever copies a crate's [[bin]] target into ~/.cargo/bin/ - it does not (and cannot) distribute the libaphrodite.dylib / libaphrodite_hermes.dylib cdylib artifacts these two crates also build. aphrodite setup will fail loudly ("dylib not found") if it can't locate them, and it's honest about that failure - but the dylibs themselves still need to come from somewhere: either a full source checkout (cargo build --release -p aphrodite -p aphrodite-hermes) or the release-download flow the "Hermes plugin" method above already uses (plugins/aphrodite/download.sh). If you only need the proxy binary itself (no Hermes plugin), cargo install aphrodite alone is sufficient.

Windows: plugins/aphrodite/download.ps1 is a native PowerShell equivalent of download.sh - no Git Bash/WSL needed. See Windows install for the fast path plus a fully manual walkthrough. Full per-platform guides (including which of Aphrodite's two build artifacts you actually need) live under docs/install/.

cargo install + one-shot bootstrap

cargo install aphrodite aphrodite-hermes
aphrodite setup --api-key sk-... --api-url https://api.deepseek.com --model deepseek-v4-pro

aphrodite setup provisions ~/.hermes/aphrodite/ end to end - binaries, dylibs, aphrodite.toml, plugin.yaml, and hermes plugins enable aphrodite - in one command. Its plugin-symlink step is currently a no-op on Windows (Unix-only code path); see docs/install/README.md if you hit that.

From source (monorepo)

git clone https://github.com/PlayForm/Aphrodite.git
cd Aphrodite
git submodule update --init --recursive  # required - vendored deps live in submodules
cargo build --release -p aphrodite -p aphrodite-hermes
# Binary: target/release/aphrodite
# Dylibs: target/release/libaphrodite.dylib, target/release/libaphrodite_hermes.dylib

What changes after install

~/.hermes/
├── plugins/
│   └── aphrodite/          ← symlink to Aphrodite-Hermes
├── aphrodite/
│   ├── aphrodite           ← auto-downloaded binary (~12 MB)
│   └── ccr.db              ← SQLite CCR store (created on first run)
└── profiles/<name>/
    └── plugins/
        └── aphrodite → ~/.hermes/plugins/aphrodite

The plugin registers 13 tools (aphrodite_*), 5 hooks, and a context engine - all routed through the Rust dylib. Two proxies launch automatically on :9797 (cache) and :9798 (token).


The Problem

Every time your agent reads a file, runs a build, searches code, or opens a browser - the raw output floods its context window. Thousands of tokens of compilation logs. Gigantic accessibility trees. Verbose JSON blobs. Your agent spends its precious context budget reading noise instead of reasoning.

Aphrodite intercepts output before it reaches the LLM and replaces it with a compact, structured preview. The agent sees 15 tokens of metadata instead of 500 tokens of raw text - and retrieves the full content only when it actually needs it.


How It Works ⚙️

 ANY OUTPUT ──────► Aphrodite ──────► Agent (preview, not raw)
                       │
                       ├─ build logs     → [build:1E 1W 142L | error[E0432]: ...]
                       ├─ terminal       → [terminal:14L exit code: 0]
                       ├─ file read      → [code:3fns|2structs fn main() 414L]
                       ├─ grep/ripgrep   → [grep:4 hits in 3 files | src/x.rs:12 …]
                       ├─ git status     → [git:2M 1A 1D 3?? | src/x.rs +N more]
                       ├─ git log        → [gitlog:2 commits | abc123 … → def456 …]
                       ├─ test output    → [test:220 pass 0 fail 1 ignored | 0.31s]
                       ├─ dir listing    → [ls:3 files 2 dirs | .rs×2 .md×1]
                       ├─ diff           → [diff:2F +7/-3 12L | src/main.rs Cargo.toml]
                       ├─ JSON blobs     → [json:5items 3L]
                       └─ plain text     → [text:3L 50B | first line hint …]

    Agent decides:
    • Preview is enough → skip retrieval, keep reasoning
    • Needs detail     → aphrodite_retrieve(hash) → full content

    Context engine (automatic):
    • Session hits 45% context → middle turns auto-compressed to CCR
    • Agent never hits context window ceiling

Four layers, all fast (classification 40-123 ns; whole compress step sub-millisecond, dwarfed by the HTTP round-trip):

  1. Classify - 26-type regex classifier identifies content (40-123 ns)
  2. Preview - enriched, type-aware previews (git/test/grep/log/build/diff/ code/ls, etc.) produced by default on both the proxy and hook paths
  3. Store - BLAKE3 → SQLite/in-memory → <<<CCR:hash|type|size>>> marker
  4. Decide - Agent reads preview, retrieves only when needed

The enriched preview shapes are emitted automatically (no flag) - see docs/proxy/compression.md for the full content-type → preview catalog.


Architecture 🏗️

crates/aphrodite/          ← Core compression engine (binary + cdylib)
  ├── proxy.rs             ← HTTP proxy: classify → compress → store → preview
  ├── hooks.rs             ← transform_tool_result, transform_terminal_output
  ├── resolve.rs           ← CCR marker resolution (nested, recursive)
  ├── stage2.rs            ← Semantic reduction (JSON, build, diff, code)
  ├── struct_extract.rs    ← Code structure extraction (Rust, Python, Go, JS/TS)
  ├── state.rs             ← Session state, inline store, LRU
  ├── catalog.rs, session.rs, marker.rs, prefetch.rs, config_loader.rs
  └── lib.rs               ← 25 C ABI functions for dylib loading

crates/aphrodite-hermes/   ← Hermes-specific integration (cdylib)
  ├── tools.rs             ← 13 tool dispatch handlers
  ├── schemas.rs           ← JSON Schema definitions
  └── skills.rs            ← Bundled Hermes skills

plugins/aphrodite/         ← Thin Python loader (421 lines)
  └── __init__.py          ← loads dylib, registers hooks/tools/engine via C ABI
Mode Port Backend Threshold Best for
Cache :9797 In-memory >8 KB Speed, transient sessions
Token :9798 SQLite >1 KB Durability, tool relay

All compression logic lives in the Rust dylib. Python is a thin FFI loader. Hot-reload: rebuild the dylib → mtime change detected → next call picks up new code automatically. The two Aphrodite crates carry unit tests (plus the full vendored Headroom suite) - run cargo test --workspace to see the current count and confirm they pass.


What You Save 💰

Content type Without Aphrodite With Aphrodite Savings
Git diff (42L) ~350 tok ~15 tok 23×
Build output (142L) ~1,400 tok ~10 tok 140×
Traceback ~45 tok ~12 tok 3.8×
Terminal output ~200 tok ~10 tok 20×
Table (50 rows) ~650 tok ~8 tok 81×
JSON blob (30 keys) ~400 tok ~10 tok 40×
Web search (10 results) ~800 tok ~15 tok 53×
Browser snapshot (342 el) ~5,000 tok ~12 tok 416×

Median: 23× fewer tokens on tool output. In a session with 50+ tool calls, that's 15,000-50,000 tokens saved - enough for an entire extra reasoning turn.

Real example: a 6-turn dev session (builds, tests, docs, git) compressed 216 KB of raw tool output into 12 markers - ~54,000 tokens → ~240 tokens (225× reduction), leaving 99.6% of the context window free for reasoning.


Tools 🔧

Tool Description
aphrodite_retrieve Resolve <<<CCR:hash|type>>> markers
aphrodite_compress Compress content via CCR with type hint
aphrodite_stats Proxy health, engine status, inline store size
aphrodite_rebuild Rebuild binary, kill proxies, restart
aphrodite_files Tracked file references, grouped by tool
aphrodite_diff Conversation turn history with summaries
aphrodite_search Search CCR store by keyword or type
aphrodite_directive List/swap/add/remove/reset behavioral directives
aphrodite_test Smoke test suite: quick (1 check), full (3 checks)
aphrodite_catalog Full CCR catalog with hashes, types, sizes, previews
aphrodite_reclassify Retroactive metadata enrichment for unclassified CCR
aphrodite_prefetch Read + compress files on demand; markers returned inline
aphrodite_prefetch_status Live prefetch schedule: loading, ready, errors

Under the Hood 🧩

./plugins/aphrodite/ is a separate repo - it lives at PlayForm/Aphrodite-Hermes. This monorepo tracks it as a git submodule.

plugins/aphrodite/          ← Standalone Hermes plugin (git submodule)
  __init__.py               ← Python loader (ctypes FFI, thin shim)
  plugin.yaml               ← 13 tools, 5 hooks, context engine
  download.sh               ← Binary auto-downloader (macOS/Linux/Git Bash/WSL)
  download.ps1              ← Binary auto-downloader (native Windows PowerShell)
  binaries/                 ← Platform-native dylib + proxy binary
  README.md                 ← Standalone install instructions

crates/aphrodite/           ← Core engine (binary + cdylib)
  src/
    lib.rs                  ← 25 C ABI functions (session, hooks, catalog, …)
    proxy.rs                ← HTTP proxy server (:9797/:9798)
    hooks.rs                ← transform_tool_result, terminal, pre/post LLM
    session.rs              ← Turn lifecycle, conversation index, git cache
    state.rs                ← AphroditeState, inline store, LRU, markers
    marker.rs               ← CCR marker generation + parse (<<<CCR:…>>>)
    catalog.rs              ← Full/compact/TOC catalog display
    resolve.rs              ← Recursive CCR marker expansion (5 levels)
    stage2.rs               ← Semantic reduction for JSON, build, diff, code
    struct_extract.rs       ← Code structure maps (Rust, Python, Go, JS/TS)
    config_loader.rs        ← TOML + env var config loading
    prefetch.rs             ← On-demand file read + compress

crates/aphrodite-hermes/    ← Hermes bridge
  src/
    lib.rs                  ← Universal dispatch (5 hooks → Rust functions)
    tools.rs                ← 13 tool handler implementations
    schemas.rs              ← JSON Schema for all tools
    skills.rs               ← Bundled skill registration for Hermes

vendor/headroom/            ← Headroom fork (git submodule)
  crates/headroom-core/     ← Content transforms, tokenizer, smart crusher

The two Aphrodite crates carry unit tests (the vendored Headroom fork carries its own suite) - run cargo test --workspace for the current count and pass/fail status. CC0-1.0 - public domain.


Developer Workflow 🛠️

# Terminal 1: cargo watch (rebuilds dylib on change)
# Build BOTH packages, not just `aphrodite` - `aphrodite-hermes` (the crate
# that actually produces libaphrodite_hermes.dylib) depends ON `aphrodite`,
# not the other way around, so `cargo build -p aphrodite` alone never
# touches the dylib at all, no matter what changed.
APHRODITE_NO_AUTO_LAUNCH=1 cargo watch -x 'build -p aphrodite -p aphrodite-hermes'

# Terminal 2: Hermes (loads hot-reloaded dylib)
hermes --profile dev-aphrodite
What changes What happens
Any .rs file cargo watch rebuilds → dylib mtime changes
Next hook call Plugin detects new mtime → reloads dylib
Any .py file /quit + hermes restart (Hermes caches Python imports)
Proxy binary aphrodite_rebuild tool → kill + copy + restart

Git hooks

git config core.hooksPath .githooks (set automatically by pnpm install via package.json's prepare script, which also runs an initial submodule sync - see below) enables .githooks/post-checkout and .githooks/post-merge. Both run .githooks/lib/sync-submodules.sh, which floats each of the three vendored submodules (plugins/aphrodite, vendor/headroom, vendor/rtk) to its configured tracking branch tip (branch = Current in .gitmodules) after every checkout/merge/pull, and auto-commits the resulting pin bump in this repo's index (scoped to just the submodule paths - never sweeps up any other staged work). The net effect: local checkouts never sit behind Current, and a fresh clone's plain git submodule update --init --recursive (no --remote needed) lands close to Current's tip too, since the pin is kept continuously in sync rather than only moving via an explicit release action.

Safety: a submodule with uncommitted changes, or with local commits not yet pushed to its tracking branch, is skipped (reported on stderr) rather than force-reset - the sync only ever performs a fast-forward-equivalent move. An in-progress rebase/merge/cherry-pick in this repo also skips the sync outright. The remote to fetch from is resolved by matching .gitmodules' own configured url against each submodule's local remotes, rather than assuming a remote named origin - vendor/rtk's origin happens to be the upstream rtk-ai/rtk repo (no Current branch there); the fork this repo actually tracks is under a remote named Source.

Maintain/scripts/release/auto-release.sh's own SYNC_SUBMODULES=1 opt-in float-at-release-time behavior is unaffected by this and still available for an explicit, reviewed pin bump as part of a release.


Quick Start 🚀

# Build
cargo build --release -p aphrodite

# Run (both proxies start automatically)
aphrodite

# Verify
curl http://127.0.0.1:9798/health
# -> {"status":"healthy","ccr":true,"mode":"token","version":"1.3.4","fill_pct":90.0}

# Dev loop with auto-reload (also rebuilds aphrodite-hermes's dylib, not just
# the proxy binary - see "Developer Workflow" above)
RUST_LOG=aphrodite=info cargo watch -x 'build -p aphrodite -p aphrodite-hermes' -x 'run -p aphrodite'

Configuration - everything in one file

# aphrodite.toml - no recompile needed
[compression]
tool_threshold_token = 512   # token proxy threshold (bytes)
tool_threshold_cache = 4096  # cache proxy threshold (bytes)
inline_threshold = 2048      # inline-vs-durable CCR storage cutoff (bytes)
code_multiplier = 3.0        # multiply threshold for code_* content types

These four [compression] fields are live on the Rust proxy: each is overridable via an APHRODITE_* env var, and editing + saving the file (or POST /reload) applies the new value immediately, no restart. Other [compression]/[previews]/[prompts] keys exist in the schema but either only affect the separate Hermes-plugin dylib session or aren't consumed by anything yet - see docs/config/aphrodite-toml.md for the full, accurate breakdown of what's wired where.

Two more knobs worth knowing about:

  • Directives - [directives] active = ["focus"] seeds short behavioral instructions (directives/*.md) injected into the LLM's context every turn, swappable mid-conversation via the aphrodite_directive tool. See docs/plugin/directives.md.
  • Management auth - set APHRODITE_MGMT_TOKEN to require Authorization: Bearer <token> on the proxy's management routes (/stats, /retrieve, /ccr/*, /reload, ...); /health and /metrics stay open. Unset = open loopback access (back-compat), with a startup warning. See docs/config/env-vars.md.

Performance ⚡

Standard corpus (bench_01, 20 samples, 106,256 B, measured 2026-07-14). Ratio scales with input size and drops with entropy - tiny/high-entropy inputs compress little, large low-entropy prose compresses a lot:

Sample (size) Ratio
tiny_text (120 B) 3.00×
json_tool (675 B) 16.88×
rust_code (2,673 B) 66.83×
linter_output (4,014 B) 100.35×
log_output (4,943 B) 123.58×
code_go (5,712 B) 142.80×
unicode_cjk (6,720 B) 168.00×
build_error (6,949 B) 173.72×
code_js (7,212 B) 180.30×
search_results (10,080 B) 252.00×
large_prose (11,400 B) 285.00×
huge_prose (24,400 B) 610.00×
Overall (106,256 → 800 B) 132.82×

Range: 3.00× (120 B) to 610.00× (24 KB prose); overall 132.82×. Cache and token modes measured identical ratios. 20/20 compressed, 20/20 retrieve round-trips OK (0 misses).

Metric Value
End-to-end latency (cache) 9-40 ms (HTTP round-trip)
End-to-end latency (token) 8-10 ms typical
Classification latency 40-123 ns

Latency figures are end-to-end - they include the HTTP round-trip, not just the compress call. The compress step itself is sub-millisecond; the corpus harness does not isolate it, so we quote the measured wall-clock numbers rather than a compress-only figure.

Benchmarks (cargo run --release -p aphrodite --example bench_0N_*, see crates/aphrodite/examples/) and the in-process smoke suite (aphrodite_test, 3 checks in full mode) are reproducible commands - run them directly to reproduce the numbers above.

Real-world savings (Hermes Agent, deepseek-v4-pro)

Measured via .bench/e2e/hermes_z_ab.sh - 4 standardized coding prompts, A/B testing with aphrodite ON vs OFF, focus directive active:

Prompt OFF (tokens) ON (tokens) Saved API calls OFF/ON
Read + summarize 444,236 178,636 60% 13 → 7
Cargo build output 28,913 28,903 0% 2 → 2
Search codebase 68,719 30,500 56% 4 → 2
Read two docs 34,075 34,049 0% 2 → 2
Total 575,943 272,088 53% 21 → 13

Cost: $0.18 (OFF) → $0.15 (ON) - 18% cheaper, 38% fewer API calls.


Relationship to Headroom

Aphrodite embeds Headroom - our custom fork tracked as a git submodule at vendor/headroom/. Headroom provides the content transforms (classifier, smart crusher, tokenizer); Aphrodite adds the preview pipeline, CCR storage, Hermes integration, and dual-proxy architecture.

Full comparison: Aphrodite vs Headroom


Contributing 🤝

Want to… Start here
Report a bug Open an issue
Suggest a feature Start a discussion
Submit a PR Fork & open a PR
Ask a question Discussions Q&A

No contribution is too small. First-time contributor? Especially welcome.


Built with ❤️ by PlayForm.

About

Aphrodite 💋 Chat Completions proxy wrapping headroom-core with CCR + tool relay.

Resources

License

Code of conduct

Stars

23 stars

Watchers

1 watching

Forks

Contributors