Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,43 @@ See [docs/components.md](docs/components.md) and [docs/reference/presets.md](doc
| `FORCE_MODEL` | — | overwrite the request `model` (eval-containers `EVAL_MODEL`) |

Routes: `POST /openai/v1/chat/completions`, `POST /anthropic/v1/messages`, `GET /healthz`,
`GET /stats` (savings rollups), `GET /expand?id=` (recover an offloaded original). Per-request: header
`GET /stats` (savings rollups), `GET /expand?id=` (recover an offloaded original), and — with
`--dashboard` — `GET /dashboard/` plus `/api/*`. Per-request: header
`x-context-guru-session` sets the session key; `x-context-guru-bypass: true` skips the pipeline.

## Dashboard

`--dashboard` adds a persistent observability UI at `/dashboard/` plus a JSON/SSE API at
`/api/*`. It exists to answer the question the product exists to answer — **what value is
context-guru providing?** — and to make the answer falsifiable.

```sh
context-guru-proxy --preset codesmart --dashboard
# open http://localhost:4000/dashboard/
```

[![The context-guru dashboard](docs/img/dashboard/01-overview.jpg)](docs/dashboard.md)

- **Four labelled savings denominators**, because a single "savings %" is a lie of
omission: of what we tried to compact · of new provider-billed input · of the whole
request (diluted) · unique-of-whole. Each one states what it divides by, and reports
**n/a** rather than a number it cannot compute.
- **Baseline vs actual cumulative cost**, with the saved area shaded, plus an honest
savings **waterfall** that will show a negative net if we spent more than we saved.
- **The cost of our own safety mechanisms beside their benefit** — cache-frozen tokens,
restorations, reverts, and context-guru's own latency and LLM spend.
- **Per-component economics**: unique vs gross savings, `overcount_ratio`, own latency, and
a verdict — so a component that burns wall time for nothing is obvious without a doc.
- **Sessions, requests, and the before/after Git-style diff** of exactly what was removed.
- **Benchmark ingestion** straight from `summary.json` + `rows-*.json`, with cost-vs-reward
per arm and per-task drill-down.

Embedded via `go:embed` — no CDN, no npm, no build step, so it works air-gapped. Capture is
off the hot path (**~175 ns** per request, drops rather than blocks) and redaction happens
before anything reaches disk. `/stats` is unchanged.

Full guide: **[docs/dashboard.md](docs/dashboard.md)**.

## The pipeline

Every component operates on tool-output messages. **Reformat** = lossless repack; **Offload** = drop
Expand Down Expand Up @@ -212,8 +246,9 @@ Details in [docs/integrations.md](docs/integrations.md).

## Docs

- [docs/design.md](docs/design.md) — architecture: component model, fail-open pipeline, store, session, expand loop, metrics, operating modes.
- [docs/design.md](docs/design.md) — architecture: component model, fail-open pipeline, store, session, expand loop, metrics, operating modes, the dashboard's capture/store layer.
- [docs/how-to/operating-modes.md](docs/how-to/operating-modes.md) — sync vs observe: when to use each, and how to read observe's projections.
- [docs/dashboard.md](docs/dashboard.md) — the persistent observability dashboard: metrics semantics, the diff view, storage, access gating, API.
- [docs/components.md](docs/components.md) — every registered component: how it works, live before→after, lossiness, config, best use.
- [docs/integrations.md](docs/integrations.md) — proxy gateway vs AuthBridge plugin, with request paths.
- [docs/setup.md](docs/setup.md) — setup + a concrete SWE-bench run through the eval-containers gateway.
Expand Down
90 changes: 77 additions & 13 deletions apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,33 @@ type slot struct {
lossless bool // wholeMessage: does bifrost round-trip this message without dropping fields
}

// Trace is the per-request record of what BodyFull actually did: the resolved
// session, the pipeline's own run report (per-component accounting), the
// before/after text of every rewritten message, and the cache-awareness facts
// that decided which messages were even eligible. It is the dashboard's capture
// input — the same material CONTEXT_GURU_DUMP writes to a file, handed to a
// caller instead. Purely observational: nothing on it affects the rewrite.
type Trace struct {
Session string
Bypassed bool
CacheAware bool
MaxCachedIdx int
// Messages is the normalized message count this request carried.
Messages int
// AttemptedTokens is the token count of the messages age/supersession
// offloaders were ALLOWED to touch (the uncached tail when cache-aware, the
// whole request otherwise). It is the honest denominator for
// "saved / attempted-to-compress"; TokensBefore−AttemptedTokens is the
// compaction our own cache-safety mechanism deliberately gave up.
AttemptedTokens int
// FrozenTokens is TokensBefore−AttemptedTokens: the cost of cache safety.
FrozenTokens int
// Run is the pipeline's aggregate report (nil when the pipeline never ran).
Run *components.RunReport
// Changes lists each rewritten message's before/after text (clipped).
Changes []Change
}

// Body runs the pipeline with no LLM clients available (deterministic components
// only). See BodyWithModel to supply model clients for LLM-based components.
func Body(ctx context.Context, pipe *components.Pipeline, st store.Store, provider bschemas.ModelProvider, body []byte, explicitSession string, bypass bool) ([]byte, bool) {
Expand Down Expand Up @@ -121,10 +148,16 @@ func BodyFull(ctx context.Context, pipe *components.Pipeline, st store.Store, pr
}

// BodyOpts is the full entry point: everything BodyFull takes plus the operating mode
// (#31) and the per-session boundary tracker. Hosts that support modes call this;
// BodyFull is the positional shim every other caller keeps using.
// (#31), the per-session boundary tracker, and the observational Trace the dashboard's
// capture path reads. Hosts that support modes call this; BodyFull is the positional
// shim every other caller keeps using.
//
// The rewrite is byte-identical whether or not anyone reads the trace: every trace
// field is filled from a value the rewrite already computed, and nothing branches on it.
func BodyOpts(ctx context.Context, pipe *components.Pipeline, st store.Store, o Opts) (res Result) {
body, provider, bypass := o.Body, o.Provider, o.Bypass
tr := &res.Trace
tr.Bypassed = bypass
// Top-level fail-open backstop: the per-component recover in pipeline.runOne only
// covers component code. A panic anywhere else on the rewrite path (normalize, the
// sjson splice, rebuildCountChanged, a marshal) must NOT 500 the client — forward
Expand All @@ -143,7 +176,11 @@ func BodyOpts(ctx context.Context, pipe *components.Pipeline, st store.Store, o
models := o.Models
msgsRaw := gjson.GetBytes(body, "messages")
if !msgsRaw.Exists() || !msgsRaw.IsArray() {
return Result{Body: body}
// Assign rather than return a fresh Result: res already carries the trace fields
// set above, and a bypassed request that also lacks a messages array must still
// report itself as bypassed rather than as "no messages".
res.Body = body
return res
}

// Volatile-tail split, before anything else touches the body. This is a
Expand All @@ -166,7 +203,8 @@ func BodyOpts(ctx context.Context, pipe *components.Pipeline, st store.Store, o

norm, slots := normalize(provider, msgsRaw.Array())
if len(norm) == 0 {
return Result{Body: body, Changed: systemSplit} // keep the split even with nothing to compact
res.Body, res.Changed = body, systemSplit // keep the split even with nothing to compact
return res
}

if debugTraffic {
Expand All @@ -175,7 +213,6 @@ func BodyOpts(ctx context.Context, pipe *components.Pipeline, st store.Store, o
chat := &bschemas.BifrostChatRequest{Provider: provider, Input: norm}
sys, firstUser := systemAndFirstUser(norm)
sessionID := session.Resolve(o.Session, sys, firstUser)
res.Session = sessionID
cacheAware := resolveCacheAware(o.CacheMode, provider, body)
maxCachedIdx := -1
if cacheAware && !bypass {
Expand Down Expand Up @@ -211,6 +248,11 @@ func BodyOpts(ctx context.Context, pipe *components.Pipeline, st store.Store, o
ExistingBreakpoints: wireBreakpoints(body),
Mode: mode,
}
tr.Session, tr.CacheAware, tr.MaxCachedIdx, tr.Messages = sessionID, cacheAware, maxCachedIdx, len(norm)
// The eligible (attempted) denominator: what age/supersession offloaders were
// allowed to touch. Everything before MaxCachedIdx is frozen for cache safety —
// the cost of that mechanism, reported next to its benefit.
tr.AttemptedTokens = attemptedTokens(norm, c)

// Canonical form of each normalized message BEFORE the pipeline, so a
// count-changing component (summarize) can be mapped back to the body.
Expand All @@ -220,7 +262,13 @@ func BodyOpts(ctx context.Context, pipe *components.Pipeline, st store.Store, o
}

rr := pipe.Run(chat, c)
res.Run = rr
tr.Run = rr
if rr != nil {
tr.FrozenTokens = rr.TokensBefore - tr.AttemptedTokens
if tr.FrozenTokens < 0 {
tr.FrozenTokens = 0
}
}

// A component changed the message count (summarize restructures the transcript
// to [msg0, <summary>, last-K]). Rebuild the messages array preserving each
Expand All @@ -240,7 +288,7 @@ func BodyOpts(ctx context.Context, pipe *components.Pipeline, st store.Store, o
// The tail split already rewrote `body`, so the result must be forwarded even
// if no component changes a message.
changed := systemSplit
var changes []change
var changes []Change
// Per-message count of changes this writeback threw away, attributed back to the
// components that made them once the loop is done.
discarded := map[int]int{}
Expand Down Expand Up @@ -297,6 +345,7 @@ func BodyOpts(ctx context.Context, pipe *components.Pipeline, st store.Store, o
}
}
pipe.RecordDiscards(rr, discarded)
tr.Changes = changes
if changed && dumpPath != "" {
dumpChanges(c.Session, changes)
}
Expand Down Expand Up @@ -399,18 +448,33 @@ func putLen(st store.Store, session string, n int) {
st.Put("cg:len:"+session, []byte(strconv.Itoa(n)))
}

// change is one rewritten message, captured for the CONTEXT_GURU_DUMP trace so a
// human can see exactly what context-guru did to the wire.
type change struct {
// attemptedTokens sums the tokens of the messages an age/supersession offloader
// was allowed to touch this turn (Ctx.TailOnly). With cache-awareness off it is
// the whole request; with it on it is the uncached tail, and the difference is
// what cache safety cost us in foregone compaction.
func attemptedTokens(norm []bschemas.ChatMessage, c *components.Ctx) int {
n := 0
for i := range norm {
if c.TailOnly(i) {
n += schema.TextTokens(schema.MessageText(norm[i]))
}
}
return n
}

// Change is one rewritten message, captured for the CONTEXT_GURU_DUMP trace and
// for the dashboard's before/after diff view, so a human can see exactly what
// context-guru did to the wire.
type Change struct {
Path string `json:"path"`
BeforeTokens int `json:"before_tokens"`
AfterTokens int `json:"after_tokens"`
Before string `json:"before"`
After string `json:"after"`
}

func mkChange(path, before, after string) change {
return change{
func mkChange(path, before, after string) Change {
return Change{
Path: path, BeforeTokens: schema.TextTokens(before), AfterTokens: schema.TextTokens(after),
Before: clip(before, 4000), After: clip(after, 4000),
}
Expand All @@ -430,7 +494,7 @@ func clip(s string, n int) string {
var dumpPath = os.Getenv("CONTEXT_GURU_DUMP")

// dumpChanges appends one JSON line describing this request's rewrites.
func dumpChanges(session string, changes []change) {
func dumpChanges(session string, changes []Change) {
f, err := os.OpenFile(dumpPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return
Expand Down
14 changes: 8 additions & 6 deletions apply/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@ type Opts struct {
}

// Result is BodyOpts' output.
//
// The embedded Trace carries everything observational: the resolved Session, the
// pipeline's Run report (which observe mode reads as its ONLY output, since the body
// is thrown away), the cache-awareness facts, and each rewritten message's
// before/after text for the dashboard. It is embedded rather than duplicated so
// there is exactly one Session and one Run in the codebase — two copies of the same
// value is how one of them goes stale.
type Result struct {
// Body is the body to forward. Always valid: on any trouble it is the input.
Body []byte
// Changed is false when Body is the untouched input.
Changed bool
// Session is the resolved session id (the caller usually cannot compute it: it falls
// back to a content hash of system + first user message).
Session string
// Run is the pipeline's report for this request, nil when the pipeline did not run.
// Observe mode needs it: the run is the ONLY output, since the body is thrown away.
Run *components.RunReport
Trace
}
Loading
Loading