Allow `headroom learn` to use locally installed coding agent CLIs
(claude, gemini, codex) as LLM backends, so subscription users
without raw API keys can run failure analysis.
Priority: --model flag > API key > HEADROOM_LEARN_CLI env var > auto-detect
- Pass prompts via stdin to avoid ARG_MAX limits
- Handle TimeoutExpired, truncate stderr, enrich JSONDecodeError
- Add 31 new tests (48 total), all passing
- Update docs/learn.md with CLI backend documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rebase the anonymous telemetry transparency warning work onto the latest main branch and format the new telemetry warning tests so the Python 3.12 lint lane passes cleanly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add is_telemetry_warn_enabled() feature flag (HEADROOM_TELEMETRY_WARN=off
to suppress at build/pack time) and format_telemetry_notice() helper to beacon.py
- Export new helpers from headroom.telemetry.__init__
- Proxy CLI startup banner now shows Telemetry: ENABLED/DISABLED with opt-out
instructions when enabled
- Log telemetry status at proxy server startup() so it appears in log stream
- wrap CLI _launch_tool() and the bespoke claude wrap command both call
_print_telemetry_notice() so users see the notice before the tool launches
- /stats endpoint now includes anon_telemetry_shipping boolean flag
- Dashboard header shows amber Anon Telemetry indicator chip when
anon_telemetry_shipping is true (theme-matching, with tooltip showing opt-out)
- 30 new tests covering all paths; all 82 tests pass
Agent-Logs-Url: https://github.com/JerrettDavis/headroom/sessions/880a7bb3-3ad9-49f4-a0b6-3ffdde233e48
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Refactor headroom learn into a plugin architecture where each coding
agent (Claude Code, Codex, Gemini CLI) is a self-contained plugin
with scanner, writer, and detection logic. External plugins can
register via the headroom.learn_plugin entry point.
- Add LearnPlugin ABC (base.py) and plugin registry (registry.py)
- Move scanners from monolithic scanner.py into plugins/ directory
- Extract shared error classification and tool name map (_shared.py)
- Add GeminiScanner for Google Gemini CLI session parsing
- CLI uses dynamic agent detection via registry (no hardcoded choices)
- All existing imports preserved via backwards-compat re-exports
- Wire agent_type through wrap → proxy → TrafficLearner
- Flush learned patterns to correct .md file at proxy shutdown
- Fix shutdown queue drain bug (patterns were lost on exit)
- 97 tests pass (84 existing + 13 new registry/plugin tests)
Update the OpenAI Codex routing test double to match the response shape
expected by the handler so the success path is exercised instead of
falling into the generic failure branch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire the proxy's operational metrics facade into the new observability
layer, expand built-in Prometheus export, surface OTEL and Langfuse status
in /stats, and document the split between anonymous telemetry, OTEL metrics,
and Langfuse traces.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a shared observability layer for OTEL metrics and Langfuse tracing.
Instrument the shared compression pipeline once so SDK and proxy paths can
reuse the same metrics and spans without scattering telemetry concerns.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Transcode technique now runs RapidOCR to extract text from images.
Falls back to full_low if OCR confidence < 70% or no text detected.
Token counting is now done AFTER compression by measuring the actual
output — no hardcoded estimates. OCR text counted by char length,
resized images re-estimated from new dimensions.
RapidOCR added to headroom-ai[image] extra (~15MB ONNX models, ~180ms CPU).
4 new OCR tests (extraction, blank image, confidence threshold, full pipeline).
29 total image compression tests passing.
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.
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>
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>
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
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.
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.
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>
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()