Commit graph

2630 commits

Author SHA1 Message Date
JerrettDavis
6f500dfa9c feat(openclaw): support configurable gateway providers 2026-04-08 21:10:49 -05:00
JerrettDavis
a4fe13d62f docs(openclaw): document codex gateway routing 2026-04-08 21:10:49 -05:00
JerrettDavis
92b7b09f70 fix(openclaw): route codex through headroom proxy 2026-04-08 21:10:49 -05:00
chopratejas
059e165c57 Support both API key and codex login auth for Codex WS proxy
Config now includes both env_key=OPENAI_API_KEY (for API key users)
and requires_openai_auth=true (for codex login / Plus plan users).

Tested and confirmed working with:
- Rust Codex v0.118.0 + OPENAI_API_KEY → wss://api.openai.com
- Rust Codex v0.118.0 + codex login (Plus) → wss://chatgpt.com
2026-04-08 18:40:53 -07:00
chopratejas
92f74db8e0 Add INFO-level logging for WS /v1/responses connect and complete
WS handler only logged at DEBUG level, making it invisible in production
logs. Added INFO logs for connection start and completion with request ID
and tokens_saved, matching the HTTP handler's PERF log pattern.
2026-04-08 18:28:52 -07:00
chopratejas
bd948b4cf2 Auto-inject Codex provider config with supports_websockets=true
headroom wrap codex now writes a [model_providers.headroom] section
into ~/.codex/config.toml with supports_websockets=true. Without this,
Codex routes WebSocket traffic directly to OpenAI, bypassing the proxy.

Safe to call multiple times — replaces existing section if port changes.
Requires OPENAI_API_KEY (ChatGPT OAuth cannot auth through a proxy).
2026-04-08 18:02:12 -07:00
chopratejas
e7008f64d7 Count Strands reasoningContent, image, document, video tokens (#111 follow-up)
reasoningContent: exact counting via count_text() — pure text, no estimation
image: decode with Pillow for (w*h)/750 formula, fallback by byte size
document: ~1500 tokens/page heuristic (3KB/page of PDF)
video: ~1000 tokens/frame heuristic (30KB/frame)

Text content (reasoning, text, toolResult) uses exact tokenization.
Binary content (image, document, video) uses provider formula or
size-based estimates — accurate counting requires content extraction
that only the provider can do.

14 tests covering all Strands content block types.
2026-04-08 17:31:00 -07:00
chopratejas
c92a6b9fa1 Fix /v1/responses/* passthrough for ChatGPT session auth (#71)
ChatGPT session auth tokens are only valid at chatgpt.com, not
api.openai.com. The catch-all route was sending all unknown paths
to api.openai.com, causing 401 for /v1/responses/compact when
Codex sub-agents use codex login auth.

Build URL directly instead of using handle_passthrough because
ChatGPT backend uses /responses/... path (no /v1/ prefix).
2026-04-08 14:05:42 -07:00
chopratejas
532d9dab9d Add passthrough route for /v1/responses/* sub-endpoints (compact, cancel, etc.)
Codex sub-agents call /v1/responses/compact for context compaction.
Without a route, the proxy returned 404/405, causing 401 errors when
the client retried against OpenAI with stale/wrong auth.

Added catch-all route for /v1/responses/{sub_path} that forwards
to the correct upstream (chatgpt.com for session auth, api.openai.com
for API key auth).
2026-04-08 12:41:44 -07:00
Garm
c22135d7b3 fix: use MCP-namespaced tool name in Claude Code instructions
Claude Code prefixes all MCP tool names with `mcp__<server>__`, so
headroom_retrieve is only callable as `mcp__headroom__headroom_retrieve`.
The strings that tell Claude how to call the tool were using the short
name, causing "No such tool available: headroom_retrieve" errors.

Update the 4 runtime-visible strings in mcp_server.py to use the full
namespaced name so Claude calls the tool correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 21:37:14 +02:00
chopratejas
c52b0f1e57 Add sentencepiece to image extra — required by SigLIP tokenizer (#103)
SiglipTokenizer from transformers hard-requires sentencepiece at runtime.
Without it, the image router silently falls back to PRESERVE (no compression).
Users on pip install headroom-ai[image] or [all] were missing this dependency.
2026-04-08 11:33:28 -07:00
chopratejas
d7122d6bab Fix Strands SDK tool pairing, CCR marker format, and startup timeout (#114 #116 #117)
- Increase headroom wrap startup timeout from 15s to 45s (#114)
  ML components (Kompress, Magika, Tree-sitter) need 20-30s on slower machines

- Detect Strands SDK toolUse/toolResult blocks in find_tool_units (#116)
  Strands wraps tools as {"toolUse": {"toolUseId": ...}} not {"type": "tool_use"}
  Without this, tool pairs aren't grouped and dropping one breaks Anthropic validation

- Match CCR marker content format to conversation style (#117)
  Strands expects list-of-blocks content, not plain strings
  Detect format from existing messages and match it
2026-04-08 11:28:23 -07:00
chopratejas
9c51f63c48 Bump to v0.5.21 2026-04-08 10:31:32 -07:00
Tejas Chopra
3e94e53cc9
Merge pull request #112 from JerrettDavis/feat/docker-e2e-wrap-validation
Add Docker wrap e2e workflow
2026-04-07 22:56:23 -07:00
JerrettDavis
addcfcffcd Merge upstream/main into feat/docker-e2e-wrap-validation
Resolve the PR merge conflict by carrying forward the secure TLS fail-closed rtk download behavior, bring in the latest upstream proxy-handler updates, and fix the OpenClaw linked-install fallback so it copies the required hook-shim directory with regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 23:54:05 -05:00
JerrettDavis
710ef8d13a fix(ci): match proxy formatting to Ruff 0.15.9
Format the OpenAI proxy handler and proxy helpers with the same Ruff version used in GitHub Actions so the Python 3.12 lint step stops failing on CI-only formatter output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 23:39:48 -05:00
Tejas Chopra
4baef0e598 chore: fix ruff lint issues (import sorting, trailing whitespace) 2026-04-07 21:33:16 -07:00
JerrettDavis
ca728a4d35 fix(ci): harden wrap e2e validation
Make the Docker wrap e2e harness validate live proxy env wiring for Codex and Aider, start a real OpenClaw gateway in-container, and clear the repo-wide Ruff issues that were keeping the Python 3.12 CI job red.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 23:27:31 -05:00
Tejas Chopra
1006bc3522 fix: skip token inflation guard in cache mode
Cache mode assembles prefix+compressed-delta which can legitimately
have different token counts than the original messages. The inflation
guard only applies to optimize/token modes.
2026-04-07 21:21:21 -07:00
Tejas Chopra
039399a017 fix: eliminate duplicate proxy logging and guard against token inflation
Duplicate logging: wrap.py redirects stderr to proxy.log while
_setup_file_logging also writes there via RotatingFileHandler. Set
propagate=False on the headroom logger and guard against adding
duplicate handlers.

Token inflation: 5.8% of requests had optimized_tokens > original_tokens
due to tokenizer counting mismatches between handler and pipeline. Added
guards in all handlers (anthropic, openai, gemini, batch) to revert to
original messages when optimization inflates tokens.
2026-04-07 20:43:04 -07:00
JerrettDavis
1967859ef6 feat(ci): add docker wrap e2e workflow
Add a Docker-based end-to-end harness that validates Headroom's Codex, Aider, Cursor, and OpenClaw wrap flows without calling real model providers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 22:36:43 -05:00
chopratejas
cfcfc0ffde Fix 4 bugs from code review: audit artifacts, unbound var, noisy log, dead code
- Remove bandit_result.txt, pip_audit_result.txt, reqs.txt, ruff_result.txt
  from repo (security risk: bandit output lists exact vuln locations) + gitignore
- Fix unbound original_tokens in batch handler except block (crash on first
  batch request failure)
- Downgrade temporary cache debug log from INFO to DEBUG (fires on every
  streaming request, polluting production logs)
- Remove duplicate _extract_anthropic_cache_ttl_metrics from AnthropicHandlerMixin
  (StreamingMixin version wins via MRO, duplicate was dead code)
2026-04-07 19:39:00 -07:00
chopratejas
a9aa66e4c6 Fix 19 test failures: missing security attr, nosec inside f-strings, stale test mock
- Initialize self.security = None on HeadroomProxy (enterprise plugin hook)
- Move '# nosec B608' comments outside f-strings in sqlite.py and fts5.py
  (SQLite doesn't understand # as comment, causing OperationalError)
- Add mark_stable_from_messages to _FakeCompressionCache test mocks
- Update token mode freeze assertion to match cache-aware behavior
2026-04-07 18:30:29 -07:00
Tejas Chopra
f65c09a835 fix: initialize parsed_response to prevent UnboundLocalError crashing all streaming responses
parsed_response was only assigned inside the memory_enabled block but
referenced unconditionally by CCR feedback and prefix cache tracker in
the finally block, causing every non-memory streaming request to crash
with UnboundLocalError and abort the connection.
2026-04-07 17:36:44 -07:00
Tejas Chopra
8bb1b1ea14 chore: bump version to 0.5.20 2026-04-07 17:30:14 -07:00
Tejas Chopra
22dad133e2 fix: eliminate prefix cache busts from frozen count underestimation
Root cause: CompressionCache.compute_frozen_count() stopped at the first
tool_result not in its cache, capping frozen_message_count at 2. Tool
results excluded by content_router (Read/Glob) or skipped (ratio too
high) never entered the cache, so every subsequent message was eligible
for recompression — causing 192 cache busts per session.

Four fixes:
1. Add _stable_hashes set to CompressionCache so excluded/skipped
   tool_results don't block the frozen count walk
2. Fix _estimate_message_tokens to count tool_result content and
   tool_use input fields (were counted as 0 tokens in Anthropic format)
3. Fix streaming handler to include assistant response and
   original_messages in prefix tracker updates (parity with non-streaming)
4. TTL-aware batch recompression: defer first-time compressions within
   the 5-min cache TTL window, batching them at the boundary to trade
   many small busts for one
2026-04-07 17:10:20 -07:00
Tejas Chopra
c2dde5f6b8
Merge pull request #110 from rrubayet321/main
Add security audit reports and update dependency management
2026-04-07 15:09:15 -07:00
chopratejas
feb163d8c8 Fix Bedrock/Vertex auth regression: don't forward api_key for env-auth providers (#105)
The 0.5.18 refactor added api_key forwarding from request headers to
LiteLLM kwargs in all 4 handler methods. This breaks Bedrock (AWS SigV4)
and Vertex AI (Google ADC) which authenticate via env vars, not API keys.
Forwarding a dummy key like sk-ant-dummy overrides AWS credentials.

Fix: skip api_key forwarding for bedrock, vertex_ai, vertex_ai_beta,
and sagemaker providers. Applied to all 4 occurrences.
2026-04-07 14:24:19 -07:00
chopratejas
8f18468f3b Fix Strands SDK content block tokenization (#111)
Strands SDK sends content blocks without a 'type' field:
  {"text": "..."} instead of {"type": "text", "text": "..."}
  {"toolUse": {...}} instead of {"type": "tool_use", ...}
  {"toolResult": {...}} instead of {"type": "tool_result", ...}

The tokenizer's _count_content_parts() only matched on type field,
causing Strands blocks to fall through to json.dumps estimation.
Now explicitly handles Strands text, toolUse, and toolResult formats
with proper recursive counting for nested toolResult content.

8 new tests covering Strands text blocks, tool blocks, and mixed formats.
2026-04-07 14:19:15 -07:00
rrubayet321
ca2a5a6cf9 chore: suppress bandit security warnings for URL and SQL operations and add URL scheme validation 2026-04-07 13:13:53 +06:00
rrubayet321
2ae71fe44d chore: add nosec B324 annotations to non-cryptographic MD5 usages and update temporary database path to use system temp directory 2026-04-07 13:07:26 +06:00
rrubayet321
d5301191f3 chore: add dependency list and security/linting audit reports 2026-04-07 13:03:05 +06:00
Tejas Chopra
cfedcef7e2
Merge pull request #107 from JerrettDavis/feat/extended-cache-ttl-metrics
Harden cache metrics and add dashboard TTL coverage
2026-04-06 22:54:09 -07:00
chopratejas
e1ad3e5db2 Route diffs to DiffCompressor via Magika label mapping
Magika detects diffs with score=1.0 (label='diff') but the label
was unmapped, falling through to TEXT. This sent diffs through
Kompress token pruning which destroys diff structure (+/- markers,
hunk headers, indentation).

Fix: add 'diff' to both the Magika ContentType enum and the
ContentRouter type_map so diffs route to DiffCompressor which
preserves all change lines and hunk structure.
2026-04-06 22:52:27 -07:00
JerrettDavis
b7e4468ccf fix: stabilize cache stats typing
Annotate the cache stats containers so the observed TTL mix fields do not confuse mypy in the Linux CI job.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-06 23:35:04 -05:00
JerrettDavis
6f701033a1 test: align anthropic cache stability fixtures
Sync the Anthropic cache stability test double with the prefix tracker contract used by the handler.

Format the benchmark scripts that were failing ruff format --check in CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-06 23:26:51 -05:00
Tejas Chopra
eed9792cef
Merge pull request #108 from JerrettDavis/fix/openclaw-discord-msg-content-filter
fix(openclaw): normalize engine messages for discord
2026-04-06 21:16:29 -07:00
JerrettDavis
2edff08668 fix(openclaw): normalize engine messages for discord 2026-04-06 22:18:37 -05:00
JerrettDavis
78163199c4 Merge upstream/main into feat/extended-cache-ttl-metrics 2026-04-06 21:59:46 -05:00
JerrettDavis
4226372b15 Add dashboard screenshots for cache TTL metrics PR 2026-04-06 21:52:09 -05:00
chopratejas
8af7971fb2 Remove OSS_PR_STRATEGY.md from public repo 2026-04-06 19:46:40 -07:00
Tejas Chopra
44493c92dd
Merge pull request #106 from JerrettDavis/fix/openclaw-local-install-hook
Fix OpenClaw local link installs for headroom plugin
2026-04-06 19:38:38 -07:00
JerrettDavis
746a1737c1 Select Headroom context engine on install 2026-04-06 21:35:58 -05:00
JerrettDavis
6e69c4ef2c Add OpenClaw hook shim for local link installs 2026-04-06 21:28:59 -05:00
JerrettDavis
95f7a0b74e Fix OpenClaw dist plugin installs 2026-04-06 21:21:23 -05:00
JerrettDavis
a6787556be Add observed cache TTL metrics and dashboard coverage 2026-04-06 21:13:30 -05:00
chopratejas
9cca08139f Bump to v0.5.19: cache stats fixes, Codex WS fix, compression-vs-cache tracking
v0.5.19 includes:
- Fix Codex WebSocket HTTP 500: ChatGPT auth routing to chatgpt.com,
  correct beta header, HTTP POST streaming fallback
- Token-level cache hit rate (not binary per-request)
- Compression-vs-cache tracking: tokens_saved vs cache_bust_tokens per session
- Pass uncached_input_tokens for OpenAI and Gemini handlers
- Fix async test failures in test_proxy_scalability
2026-04-06 18:17:13 -07:00
JerrettDavis
8e4d7759de Harden cache validation reporting and TTL analysis 2026-04-06 20:11:21 -05:00
chopratejas
14d00c6c74 Token-level cache hit rate, compression-vs-cache tracking, dashboard SQL, security plan
Cache stats:
- hit_rate is now token-level (cache_read / total_input) not request-level
- Track uncached_input_tokens per provider in metrics
- Preserve request_hit_rate as secondary metric

Compression-vs-cache:
- Detect when compression busts the prefix cache (expected_cached - actual_read)
- Two simple session-level numbers: tokens_saved vs cache_bust_tokens
- Log CACHE-BUST per request, aggregate in /stats and telemetry beacon
- Single new column in proxy_telemetry_v2: cache_bust_tokens

Dashboard infra:
- SQL for dashboard_summary table + pg_cron hourly refresh
- Hourly + daily aggregation from proxy_telemetry_v2
- Upgrade scripts for adding hourly_stats and cache bust columns
2026-04-06 18:10:29 -07:00
chopratejas
f4160f9e47 Fix Codex WebSocket HTTP 500: ChatGPT auth routing, correct beta header, HTTP fallback (#71)
Root causes:
- ChatGPT session auth tokens sent to api.openai.com instead of chatgpt.com
- Fallback beta header was responses-api=v1 instead of responses_websockets=2026-02-06

Fixes:
- Detect ChatGPT-Account-ID header and route WS/HTTP to chatgpt.com/backend-api/codex/responses
- Update beta header fallback to match what Codex actually sends
- Add HTTP POST streaming fallback when upstream WS fails (relay SSE over client WS)
- Unwrap response.create envelope in HTTP fallback for correct POST body
- Initialize body before JSON parse to prevent NameError in fallback path
- Fix async test failures: convert asyncio.get_event_loop() to asyncio.run()
2026-04-06 12:48:46 -07:00