Skip to content
Merged
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,15 +424,15 @@ The default configuration (what `GET /config` returns out of the box):
"min_tokens_to_compress": 250,
"kompress_model": null,
"savings_profile": null,
"headroom_version": "0.28.0"
"headroom_version": "0.30.0"
}
```

`headroom_version` is read-only — the headroom-ai version tsheadroom detected on its workers (omitted until the first worker reports ready). It's informational and reflects which knobs the running headroom can actually honor.

The tunable surface below tracks headroom-ai's library `compress()` API, which is intentionally stable: 0.27.0 and 0.28.0 added no new `CompressConfig` knobs, so the fields here are unchanged since 0.26.0 (which introduced `savings_profile`). Newer headroom releases still improve the compression that runs *inside* `compress()` — you get those automatically with no config change. The extra power those releases added (e.g. `force_kompress`, `--protect-tool-results`) lives in headroom's proxy pipeline, which `compress()` does not expose.
The tunable surface below tracks headroom-ai's library `compress()` API, which is intentionally stable: 0.27.0 through 0.30.0 added no new `CompressConfig` knobs, so the fields here are unchanged since 0.26.0 (which introduced `savings_profile`). The one profile-surface change is that 0.30.0 added the `coding` and `general` presets alongside `agent-90` and `balanced`. Newer headroom releases still improve the compression that runs *inside* `compress()` — you get those automatically with no config change. The extra power those releases added (e.g. `force_kompress`, `--protect-tool-results`) lives in headroom's proxy pipeline, which `compress()` does not expose.

Changes take effect on the next request. Invalid values are rejected with `400` and leave the current configuration untouched (for example, `target_ratio: 5` → `target_ratio must be in (0, 1] or null`; `savings_profile: "fast"` → `savings_profile must be one of agent-90, balanced, or null`). You can also edit the `-config` JSON file directly and restart.
Changes take effect on the next request. Invalid values are rejected with `400` and leave the current configuration untouched (for example, `target_ratio: 5` → `target_ratio must be in (0, 1] or null`; `savings_profile: "fast"` → `savings_profile must be one of agent-90, balanced, coding, general, or null`). You can also edit the `-config` JSON file directly and restart.

Tunable parameters (these mirror Headroom's `CompressConfig`):

Expand All @@ -445,7 +445,7 @@ Tunable parameters (these mirror Headroom's `CompressConfig`):
| `target_ratio` | float \| null | `null` | Keep-ratio for text compression: `null` = model decides (~aggressive), `0.5` = keep 50%. Must be in `(0, 1]`. *(Needs `[ml]`.)* |
| `min_tokens_to_compress` | int | `250` | Skip messages shorter than this. |
| `kompress_model` | string \| null | `null` | Override the Kompress model id; `null` = default. |
| `savings_profile` | string \| null | `null` | Apply a named high-savings preset: `agent-90` (aggressive, tuned for coding agents) or `balanced`. **Overrides the individual knobs above** — if you set both, the profile wins. Requires `headroom-ai >= 0.26.0`; on older versions the field is shown but setting it is rejected with `400`. Note: via the library path it applies only the standard knobs (proxy-only profile settings like forced Kompress don't take effect), so realized savings may be below the profile's headline target. |
| `savings_profile` | string \| null | `null` | Apply a named high-savings preset: `agent-90` (aggressive, tuned for coding agents), `balanced`, or the workload personas `coding` and `general` (added in `headroom-ai` 0.30.0). **Overrides the individual knobs above** — if you set both, the profile wins. Requires `headroom-ai >= 0.26.0`; on older versions the field is shown but setting it is rejected with `400`. Note: via the library path it applies only the standard knobs (proxy-only profile settings like forced Kompress don't take effect), so realized savings may be below the profile's headline target. |

> **Access**: the `/config` endpoint is gated only by your [tailnet policy file](https://tailscale.com/docs/reference/syntax/policy-file): anyone who can reach the device can read and change its configuration. Lock the device down accordingly (see [Security](#security-and-data-handling)).

Expand Down
26 changes: 24 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -45,7 +46,17 @@ type CompressSettings struct {
// compress() path. Validation rejects anything else so an unknown name can never
// reach compress() — where it would raise (it's applied before compress()'s own
// fail-open try) and make the pool recycle the worker on every request.
var savingsProfiles = map[string]bool{"agent-90": true, "balanced": true}
//
// agent-90 and balanced date to 0.26.0; the coding and general workload personas
// were added in 0.30.0. All four apply cleanly via compress() (the library path
// sets only the standard CompressConfig knobs each profile carries). Keep this in
// sync with headroom.agent_savings._PROFILES.
var savingsProfiles = map[string]bool{
"agent-90": true,
"balanced": true,
"coding": true,
"general": true,
}

// defaultSettings mirrors headroom's CompressConfig defaults (compress.py).
func defaultSettings() CompressSettings {
Expand Down Expand Up @@ -75,11 +86,22 @@ func (s CompressSettings) validate() error {
return fmt.Errorf("kompress_model must be a non-empty string or null")
}
if s.SavingsProfile != nil && !savingsProfiles[*s.SavingsProfile] {
return fmt.Errorf("savings_profile must be one of agent-90, balanced, or null")
return fmt.Errorf("savings_profile must be one of %s, or null", strings.Join(sortedProfiles(), ", "))
}
return nil
}

// sortedProfiles returns the accepted savings_profile names in stable order, so
// the validation error text is deterministic and always matches savingsProfiles.
func sortedProfiles() []string {
names := make([]string, 0, len(savingsProfiles))
for name := range savingsProfiles {
names = append(names, name)
}
sort.Strings(names)
return names
}

// textEnabled reports whether a knob that drives the ML text compressor
// (Kompress) is on. It's the single source of truth for whether workers should
// preload that model at startup (passed to them via TSHEADROOM_PRELOAD; see
Expand Down
2 changes: 2 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func TestSettings_Validate(t *testing.T) {
{"empty kompress_model", func(s *CompressSettings) { e := ""; s.KompressModel = &e }, true},
{"savings_profile agent-90", func(s *CompressSettings) { v := "agent-90"; s.SavingsProfile = &v }, false},
{"savings_profile balanced", func(s *CompressSettings) { v := "balanced"; s.SavingsProfile = &v }, false},
{"savings_profile coding", func(s *CompressSettings) { v := "coding"; s.SavingsProfile = &v }, false},
{"savings_profile general", func(s *CompressSettings) { v := "general"; s.SavingsProfile = &v }, false},
{"savings_profile unknown", func(s *CompressSettings) { v := "fast"; s.SavingsProfile = &v }, true},
{"savings_profile empty", func(s *CompressSettings) { v := ""; s.SavingsProfile = &v }, true},
}
Expand Down
Loading