0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
1047 lines
38 KiB
Python
1047 lines
38 KiB
Python
"""Main HeadroomClient implementation for Headroom SDK."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Iterator
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from .cache import (
|
|
BaseCacheOptimizer,
|
|
CacheConfig,
|
|
CacheOptimizerRegistry,
|
|
OptimizationContext,
|
|
SemanticCacheLayer,
|
|
)
|
|
from .config import (
|
|
HeadroomConfig,
|
|
HeadroomMode,
|
|
RequestMetrics,
|
|
SimulationResult,
|
|
)
|
|
from .parser import parse_messages
|
|
from .pipeline import PipelineExtensionManager, PipelineStage, summarize_routing_markers
|
|
from .providers.base import Provider
|
|
from .providers.registry import call_client_transport
|
|
from .storage import create_storage
|
|
from .tokenizer import Tokenizer
|
|
from .transforms import CacheAligner, TransformPipeline
|
|
from .utils import (
|
|
compute_messages_hash,
|
|
compute_prefix_hash,
|
|
estimate_cost,
|
|
format_cost,
|
|
generate_request_id,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ChatCompletions:
|
|
"""Wrapper for chat.completions API (OpenAI-style)."""
|
|
|
|
def __init__(self, client: HeadroomClient):
|
|
self._client = client
|
|
|
|
def create(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
stream: bool = False,
|
|
# Headroom-specific parameters
|
|
headroom_mode: str | None = None,
|
|
headroom_cache_prefix_tokens: int | None = None,
|
|
headroom_output_buffer_tokens: int | None = None,
|
|
headroom_keep_turns: int | None = None,
|
|
headroom_tool_profiles: dict[str, dict[str, Any]] | None = None,
|
|
# Pass through all other kwargs
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
"""
|
|
Create a chat completion with optional Headroom optimization.
|
|
|
|
Args:
|
|
model: Model name.
|
|
messages: List of messages.
|
|
stream: Whether to stream the response.
|
|
headroom_mode: Override default mode ("audit" | "optimize").
|
|
headroom_cache_prefix_tokens: Target cache-aligned prefix size.
|
|
headroom_output_buffer_tokens: Reserve tokens for output.
|
|
headroom_keep_turns: Never drop last N turns.
|
|
headroom_tool_profiles: Per-tool compression config.
|
|
**kwargs: Additional arguments passed to underlying client.
|
|
|
|
Returns:
|
|
Chat completion response (or stream iterator).
|
|
"""
|
|
return self._client._create(
|
|
model=model,
|
|
messages=messages,
|
|
stream=stream,
|
|
headroom_mode=headroom_mode,
|
|
headroom_cache_prefix_tokens=headroom_cache_prefix_tokens,
|
|
headroom_output_buffer_tokens=headroom_output_buffer_tokens,
|
|
headroom_keep_turns=headroom_keep_turns,
|
|
headroom_tool_profiles=headroom_tool_profiles,
|
|
api_style="openai",
|
|
**kwargs,
|
|
)
|
|
|
|
def simulate(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
headroom_mode: str = "optimize",
|
|
headroom_output_buffer_tokens: int | None = None,
|
|
headroom_tool_profiles: dict[str, dict[str, Any]] | None = None,
|
|
**kwargs: Any,
|
|
) -> SimulationResult:
|
|
"""
|
|
Simulate optimization without calling the API.
|
|
|
|
Args:
|
|
model: Model name.
|
|
messages: List of messages.
|
|
headroom_mode: Mode to simulate.
|
|
headroom_output_buffer_tokens: Output buffer to use.
|
|
headroom_tool_profiles: Tool profiles to use.
|
|
**kwargs: Additional arguments (ignored).
|
|
|
|
Returns:
|
|
SimulationResult with projected changes.
|
|
"""
|
|
return self._client._simulate(
|
|
model=model,
|
|
messages=messages,
|
|
headroom_mode=headroom_mode,
|
|
headroom_output_buffer_tokens=headroom_output_buffer_tokens,
|
|
headroom_tool_profiles=headroom_tool_profiles,
|
|
)
|
|
|
|
|
|
class Messages:
|
|
"""Wrapper for messages API (Anthropic-style)."""
|
|
|
|
def __init__(self, client: HeadroomClient):
|
|
self._client = client
|
|
|
|
def create(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
max_tokens: int = 1024,
|
|
# Headroom-specific parameters
|
|
headroom_mode: str | None = None,
|
|
headroom_cache_prefix_tokens: int | None = None,
|
|
headroom_output_buffer_tokens: int | None = None,
|
|
headroom_keep_turns: int | None = None,
|
|
headroom_tool_profiles: dict[str, dict[str, Any]] | None = None,
|
|
# Pass through all other kwargs
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
"""
|
|
Create a message with optional Headroom optimization.
|
|
|
|
Args:
|
|
model: Model name.
|
|
messages: List of messages.
|
|
max_tokens: Maximum tokens in response.
|
|
headroom_mode: Override default mode ("audit" | "optimize").
|
|
headroom_cache_prefix_tokens: Target cache-aligned prefix size.
|
|
headroom_output_buffer_tokens: Reserve tokens for output.
|
|
headroom_keep_turns: Never drop last N turns.
|
|
headroom_tool_profiles: Per-tool compression config.
|
|
**kwargs: Additional arguments passed to underlying client.
|
|
|
|
Returns:
|
|
Message response.
|
|
"""
|
|
return self._client._create(
|
|
model=model,
|
|
messages=messages,
|
|
stream=False,
|
|
headroom_mode=headroom_mode,
|
|
headroom_cache_prefix_tokens=headroom_cache_prefix_tokens,
|
|
headroom_output_buffer_tokens=headroom_output_buffer_tokens,
|
|
headroom_keep_turns=headroom_keep_turns,
|
|
headroom_tool_profiles=headroom_tool_profiles,
|
|
api_style="anthropic",
|
|
max_tokens=max_tokens,
|
|
**kwargs,
|
|
)
|
|
|
|
def stream(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
max_tokens: int = 1024,
|
|
# Headroom-specific parameters
|
|
headroom_mode: str | None = None,
|
|
headroom_cache_prefix_tokens: int | None = None,
|
|
headroom_output_buffer_tokens: int | None = None,
|
|
headroom_keep_turns: int | None = None,
|
|
headroom_tool_profiles: dict[str, dict[str, Any]] | None = None,
|
|
# Pass through all other kwargs
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
"""
|
|
Stream a message with optional Headroom optimization.
|
|
|
|
Args:
|
|
model: Model name.
|
|
messages: List of messages.
|
|
max_tokens: Maximum tokens in response.
|
|
headroom_mode: Override default mode ("audit" | "optimize").
|
|
headroom_cache_prefix_tokens: Target cache-aligned prefix size.
|
|
headroom_output_buffer_tokens: Reserve tokens for output.
|
|
headroom_keep_turns: Never drop last N turns.
|
|
headroom_tool_profiles: Per-tool compression config.
|
|
**kwargs: Additional arguments passed to underlying client.
|
|
|
|
Returns:
|
|
Stream context manager.
|
|
"""
|
|
return self._client._create(
|
|
model=model,
|
|
messages=messages,
|
|
stream=True,
|
|
headroom_mode=headroom_mode,
|
|
headroom_cache_prefix_tokens=headroom_cache_prefix_tokens,
|
|
headroom_output_buffer_tokens=headroom_output_buffer_tokens,
|
|
headroom_keep_turns=headroom_keep_turns,
|
|
headroom_tool_profiles=headroom_tool_profiles,
|
|
api_style="anthropic",
|
|
max_tokens=max_tokens,
|
|
**kwargs,
|
|
)
|
|
|
|
def simulate(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
headroom_mode: str = "optimize",
|
|
headroom_output_buffer_tokens: int | None = None,
|
|
headroom_tool_profiles: dict[str, dict[str, Any]] | None = None,
|
|
**kwargs: Any,
|
|
) -> SimulationResult:
|
|
"""
|
|
Simulate optimization without calling the API.
|
|
|
|
Args:
|
|
model: Model name.
|
|
messages: List of messages.
|
|
headroom_mode: Mode to simulate.
|
|
headroom_output_buffer_tokens: Output buffer to use.
|
|
headroom_tool_profiles: Tool profiles to use.
|
|
**kwargs: Additional arguments (ignored).
|
|
|
|
Returns:
|
|
SimulationResult with projected changes.
|
|
"""
|
|
return self._client._simulate(
|
|
model=model,
|
|
messages=messages,
|
|
headroom_mode=headroom_mode,
|
|
headroom_output_buffer_tokens=headroom_output_buffer_tokens,
|
|
headroom_tool_profiles=headroom_tool_profiles,
|
|
)
|
|
|
|
|
|
class HeadroomClient:
|
|
"""
|
|
Context Budget Controller wrapper for LLM API clients.
|
|
|
|
Provides automatic context optimization, waste detection, and
|
|
cache alignment while maintaining API compatibility.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
original_client: Any,
|
|
provider: Provider,
|
|
store_url: str | None = None,
|
|
default_mode: str = "audit",
|
|
model_context_limits: dict[str, int] | None = None,
|
|
cache_optimizer: BaseCacheOptimizer | None = None,
|
|
enable_cache_optimizer: bool = True,
|
|
enable_semantic_cache: bool = False,
|
|
config: HeadroomConfig | None = None,
|
|
):
|
|
"""
|
|
Initialize HeadroomClient.
|
|
|
|
Args:
|
|
original_client: The underlying LLM client (OpenAI-compatible).
|
|
provider: Provider instance for model-specific behavior.
|
|
store_url: Storage URL (sqlite:// or jsonl://). Defaults to temp dir.
|
|
default_mode: Default mode ("audit" | "optimize").
|
|
model_context_limits: Override context limits for models.
|
|
cache_optimizer: Optional custom cache optimizer. If None and
|
|
enable_cache_optimizer=True, auto-detects from provider.
|
|
enable_cache_optimizer: Enable provider-specific cache optimization.
|
|
enable_semantic_cache: Enable query-level semantic caching.
|
|
config: Optional HeadroomConfig for full control over all settings.
|
|
When provided, takes precedence over individual settings like
|
|
store_url, default_mode, etc.
|
|
"""
|
|
self._original = original_client
|
|
self._provider = provider
|
|
|
|
# Set default store_url to temp directory for better DevEx
|
|
if store_url is None:
|
|
import os
|
|
import tempfile
|
|
|
|
db_path = os.path.join(tempfile.gettempdir(), "headroom.db")
|
|
store_url = f"sqlite:///{db_path}"
|
|
|
|
self._store_url = store_url
|
|
self._default_mode = HeadroomMode(default_mode)
|
|
|
|
# Use provided config or build from individual parameters
|
|
if config is not None:
|
|
self._config = config
|
|
# Override store_url and mode if explicitly provided in config
|
|
if config.store_url:
|
|
self._store_url = config.store_url
|
|
else:
|
|
self._config.store_url = store_url
|
|
self._default_mode = config.default_mode
|
|
else:
|
|
# Build config from individual parameters
|
|
self._config = HeadroomConfig()
|
|
self._config.store_url = store_url
|
|
self._config.default_mode = self._default_mode
|
|
self._config.cache_optimizer.enabled = enable_cache_optimizer
|
|
self._config.cache_optimizer.enable_semantic_cache = enable_semantic_cache
|
|
|
|
if model_context_limits:
|
|
self._config.model_context_limits.update(model_context_limits)
|
|
|
|
# Initialize storage
|
|
self._storage = create_storage(store_url)
|
|
|
|
# Initialize transform pipeline
|
|
self._pipeline = TransformPipeline(self._config, provider=self._provider)
|
|
self._pipeline_extensions = PipelineExtensionManager(
|
|
extensions=self._config.pipeline_extensions,
|
|
discover=self._config.discover_pipeline_extensions,
|
|
)
|
|
|
|
# Initialize cache optimizer
|
|
self._cache_optimizer: BaseCacheOptimizer | None = None
|
|
self._semantic_cache_layer: SemanticCacheLayer | None = None
|
|
|
|
if enable_cache_optimizer:
|
|
if cache_optimizer is not None:
|
|
self._cache_optimizer = cache_optimizer
|
|
else:
|
|
# Auto-detect from provider
|
|
provider_name = self._provider.name.lower()
|
|
if CacheOptimizerRegistry.is_registered(provider_name):
|
|
cache_config = CacheConfig(
|
|
min_cacheable_tokens=self._config.cache_optimizer.min_cacheable_tokens,
|
|
)
|
|
self._cache_optimizer = CacheOptimizerRegistry.get(
|
|
provider_name,
|
|
config=cache_config,
|
|
)
|
|
|
|
# Wrap with semantic cache if enabled
|
|
if enable_semantic_cache and self._cache_optimizer is not None:
|
|
self._semantic_cache_layer = SemanticCacheLayer(
|
|
self._cache_optimizer,
|
|
similarity_threshold=self._config.cache_optimizer.semantic_cache_similarity,
|
|
max_entries=self._config.cache_optimizer.semantic_cache_max_entries,
|
|
ttl_seconds=self._config.cache_optimizer.semantic_cache_ttl_seconds,
|
|
)
|
|
|
|
# Public API - OpenAI style
|
|
self.chat = type("Chat", (), {"completions": ChatCompletions(self)})()
|
|
# Public API - Anthropic style
|
|
self.messages = Messages(self)
|
|
self._pipeline_extensions.emit(
|
|
PipelineStage.SETUP,
|
|
operation="sdk.setup",
|
|
provider=self._provider.name.lower(),
|
|
metadata={
|
|
"default_mode": self._default_mode.value,
|
|
"cache_optimizer_enabled": enable_cache_optimizer,
|
|
"semantic_cache_enabled": enable_semantic_cache,
|
|
},
|
|
)
|
|
|
|
def _get_tokenizer(self, model: str) -> Tokenizer:
|
|
"""Get tokenizer for model using provider."""
|
|
token_counter = self._provider.get_token_counter(model)
|
|
return Tokenizer(token_counter, model)
|
|
|
|
def _get_context_limit(self, model: str) -> int:
|
|
"""Get context limit from user config or provider."""
|
|
# User override takes precedence
|
|
limit = self._config.get_context_limit(model)
|
|
if limit is not None:
|
|
return limit
|
|
# Fall back to provider
|
|
return self._provider.get_context_limit(model)
|
|
|
|
def _create(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
stream: bool = False,
|
|
headroom_mode: str | None = None,
|
|
headroom_cache_prefix_tokens: int | None = None,
|
|
headroom_output_buffer_tokens: int | None = None,
|
|
headroom_keep_turns: int | None = None,
|
|
headroom_tool_profiles: dict[str, dict[str, Any]] | None = None,
|
|
api_style: str = "openai",
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
"""Internal implementation of create."""
|
|
request_id = generate_request_id()
|
|
timestamp = datetime.now(timezone.utc).replace(tzinfo=None)
|
|
mode = HeadroomMode(headroom_mode) if headroom_mode else self._default_mode
|
|
|
|
input_event = self._pipeline_extensions.emit(
|
|
PipelineStage.INPUT_RECEIVED,
|
|
operation="sdk.request",
|
|
request_id=request_id,
|
|
provider=self._provider.name.lower(),
|
|
model=model,
|
|
messages=messages,
|
|
metadata={"api_style": api_style, "stream": stream, "mode": mode.value},
|
|
)
|
|
if input_event.messages is not None:
|
|
messages = input_event.messages
|
|
|
|
tokenizer = self._get_tokenizer(model)
|
|
|
|
# Analyze original messages
|
|
blocks, block_breakdown, waste_signals = parse_messages(messages, tokenizer)
|
|
tokens_before = tokenizer.count_messages(messages)
|
|
|
|
# Compute cache alignment score
|
|
aligner = CacheAligner(self._config.cache_aligner)
|
|
cache_alignment_score = aligner.get_alignment_score(messages)
|
|
|
|
# Compute stable prefix hash
|
|
stable_prefix_hash = compute_prefix_hash(messages)
|
|
|
|
# Cache optimizer metrics (populated later if optimizer is used)
|
|
cache_optimizer_used = None
|
|
cache_optimizer_strategy = None
|
|
cacheable_tokens = 0
|
|
breakpoints_inserted = 0
|
|
estimated_cache_hit = False
|
|
estimated_savings_percent = 0.0
|
|
semantic_cache_hit = False
|
|
cached_response = None
|
|
|
|
# Apply transforms if in optimize mode
|
|
if mode == HeadroomMode.OPTIMIZE:
|
|
output_buffer = headroom_output_buffer_tokens or self._config.output_buffer_tokens
|
|
model_limit = self._get_context_limit(model)
|
|
|
|
result = self._pipeline.apply(
|
|
messages,
|
|
model,
|
|
model_limit=model_limit,
|
|
output_buffer=output_buffer,
|
|
tool_profiles=headroom_tool_profiles or {},
|
|
)
|
|
|
|
optimized_messages = result.messages
|
|
tokens_after = result.tokens_after
|
|
transforms_applied = result.transforms_applied
|
|
|
|
routing_markers = summarize_routing_markers(transforms_applied)
|
|
if routing_markers:
|
|
routed_event = self._pipeline_extensions.emit(
|
|
PipelineStage.INPUT_ROUTED,
|
|
operation="sdk.request",
|
|
request_id=request_id,
|
|
provider=self._provider.name.lower(),
|
|
model=model,
|
|
messages=optimized_messages,
|
|
metadata={
|
|
"routing_markers": routing_markers,
|
|
"transforms_applied": transforms_applied,
|
|
},
|
|
)
|
|
if routed_event.messages is not None:
|
|
optimized_messages = routed_event.messages
|
|
|
|
# Apply provider-specific cache optimization
|
|
if self._cache_optimizer is not None or self._semantic_cache_layer is not None:
|
|
cache_context = OptimizationContext(
|
|
provider=self._provider.name.lower(),
|
|
model=model,
|
|
query=self._extract_query(optimized_messages),
|
|
)
|
|
|
|
# Check semantic cache first (if enabled)
|
|
if self._semantic_cache_layer is not None:
|
|
cache_result = self._semantic_cache_layer.process(
|
|
optimized_messages, cache_context
|
|
)
|
|
semantic_cache_hit = cache_result.semantic_cache_hit
|
|
if semantic_cache_hit:
|
|
cached_response = cache_result.cached_response
|
|
|
|
# Update metrics from cache result
|
|
cache_optimizer_used = getattr(
|
|
cache_result.metrics, "optimizer_name", None
|
|
) or (self._cache_optimizer.name if self._cache_optimizer else "")
|
|
cache_optimizer_strategy = getattr(cache_result.metrics, "strategy", "")
|
|
cacheable_tokens = cache_result.metrics.cacheable_tokens
|
|
breakpoints_inserted = cache_result.metrics.breakpoints_inserted
|
|
estimated_cache_hit = cache_result.metrics.estimated_cache_hit
|
|
estimated_savings_percent = cache_result.metrics.estimated_savings_percent
|
|
|
|
# Apply optimized messages (with cache_control blocks for Anthropic)
|
|
if cache_result.messages:
|
|
optimized_messages = cache_result.messages
|
|
|
|
elif self._cache_optimizer is not None:
|
|
# Direct cache optimizer (no semantic layer)
|
|
cache_result = self._cache_optimizer.optimize(optimized_messages, cache_context)
|
|
cache_optimizer_used = self._cache_optimizer.name
|
|
cache_optimizer_strategy = self._cache_optimizer.strategy.value
|
|
cacheable_tokens = cache_result.metrics.cacheable_tokens
|
|
breakpoints_inserted = cache_result.metrics.breakpoints_inserted
|
|
estimated_cache_hit = cache_result.metrics.estimated_cache_hit
|
|
estimated_savings_percent = cache_result.metrics.estimated_savings_percent
|
|
|
|
if cache_result.messages:
|
|
optimized_messages = cache_result.messages
|
|
|
|
transforms_applied.extend(
|
|
f"cache_optimizer:{t}" for t in (cache_result.transforms_applied or [])
|
|
)
|
|
|
|
compressed_event = self._pipeline_extensions.emit(
|
|
PipelineStage.INPUT_COMPRESSED,
|
|
operation="sdk.request",
|
|
request_id=request_id,
|
|
provider=self._provider.name.lower(),
|
|
model=model,
|
|
messages=optimized_messages,
|
|
metadata={
|
|
"tokens_before": tokens_before,
|
|
"tokens_after": tokens_after,
|
|
"transforms_applied": transforms_applied,
|
|
},
|
|
)
|
|
if compressed_event.messages is not None:
|
|
optimized_messages = compressed_event.messages
|
|
tokens_after = tokenizer.count_messages(optimized_messages)
|
|
|
|
# Recalculate prefix hash after optimization
|
|
stable_prefix_hash = compute_prefix_hash(optimized_messages)
|
|
else:
|
|
# Audit mode - no changes
|
|
optimized_messages = messages
|
|
tokens_after = tokens_before
|
|
transforms_applied = []
|
|
|
|
presend_event = self._pipeline_extensions.emit(
|
|
PipelineStage.PRE_SEND,
|
|
operation="sdk.request",
|
|
request_id=request_id,
|
|
provider=self._provider.name.lower(),
|
|
model=model,
|
|
messages=optimized_messages,
|
|
metadata={"api_style": api_style, "stream": stream},
|
|
)
|
|
if presend_event.messages is not None:
|
|
optimized_messages = presend_event.messages
|
|
tokens_after = tokenizer.count_messages(optimized_messages)
|
|
stable_prefix_hash = compute_prefix_hash(optimized_messages)
|
|
|
|
# Create metrics
|
|
metrics = RequestMetrics(
|
|
request_id=request_id,
|
|
timestamp=timestamp,
|
|
model=model,
|
|
stream=stream,
|
|
mode=mode.value,
|
|
tokens_input_before=tokens_before,
|
|
tokens_input_after=tokens_after,
|
|
block_breakdown=block_breakdown,
|
|
waste_signals=waste_signals.to_dict(),
|
|
stable_prefix_hash=stable_prefix_hash,
|
|
cache_alignment_score=cache_alignment_score,
|
|
transforms_applied=transforms_applied,
|
|
messages_hash=compute_messages_hash(messages),
|
|
# Cache optimizer metrics
|
|
cache_optimizer_used=cache_optimizer_used,
|
|
cache_optimizer_strategy=cache_optimizer_strategy,
|
|
cacheable_tokens=cacheable_tokens,
|
|
breakpoints_inserted=breakpoints_inserted,
|
|
estimated_cache_hit=estimated_cache_hit,
|
|
estimated_savings_percent=estimated_savings_percent,
|
|
semantic_cache_hit=semantic_cache_hit,
|
|
)
|
|
|
|
# Update session stats
|
|
self._update_session_stats(
|
|
mode=mode,
|
|
tokens_before=tokens_before,
|
|
tokens_after=tokens_after,
|
|
cache_hit=semantic_cache_hit,
|
|
)
|
|
|
|
# Return cached response if semantic cache hit
|
|
if semantic_cache_hit and cached_response is not None:
|
|
self._storage.save(metrics)
|
|
return cached_response
|
|
|
|
# Call underlying client based on API style
|
|
try:
|
|
response = call_client_transport(
|
|
api_style,
|
|
self,
|
|
model=model,
|
|
messages=optimized_messages,
|
|
stream=stream,
|
|
metrics=metrics,
|
|
**kwargs,
|
|
)
|
|
|
|
self._pipeline_extensions.emit(
|
|
PipelineStage.POST_SEND,
|
|
operation="sdk.request",
|
|
request_id=request_id,
|
|
provider=self._provider.name.lower(),
|
|
model=model,
|
|
messages=optimized_messages,
|
|
response=response,
|
|
metadata={"api_style": api_style, "stream": stream},
|
|
)
|
|
self._pipeline_extensions.emit(
|
|
PipelineStage.RESPONSE_RECEIVED,
|
|
operation="sdk.request",
|
|
request_id=request_id,
|
|
provider=self._provider.name.lower(),
|
|
model=model,
|
|
response=response,
|
|
metadata={"api_style": api_style, "stream": stream},
|
|
)
|
|
return response
|
|
|
|
except Exception as e:
|
|
metrics.error = str(e)
|
|
self._storage.save(metrics)
|
|
raise
|
|
|
|
def _call_openai(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
stream: bool,
|
|
metrics: RequestMetrics,
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
"""Call OpenAI-style API."""
|
|
return call_client_transport(
|
|
"openai",
|
|
self,
|
|
model=model,
|
|
messages=messages,
|
|
stream=stream,
|
|
metrics=metrics,
|
|
**kwargs,
|
|
)
|
|
|
|
def _call_anthropic(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
stream: bool,
|
|
metrics: RequestMetrics,
|
|
**kwargs: Any,
|
|
) -> Any:
|
|
"""Call Anthropic-style API."""
|
|
return call_client_transport(
|
|
"anthropic",
|
|
self,
|
|
model=model,
|
|
messages=messages,
|
|
stream=stream,
|
|
metrics=metrics,
|
|
**kwargs,
|
|
)
|
|
|
|
def _wrap_stream(
|
|
self,
|
|
stream: Iterator[Any],
|
|
metrics: RequestMetrics,
|
|
) -> Iterator[Any]:
|
|
"""Wrap stream to pass through chunks and save metrics at end."""
|
|
try:
|
|
yield from stream
|
|
finally:
|
|
# Save metrics when stream completes
|
|
# Note: output tokens unknown for streams
|
|
self._storage.save(metrics)
|
|
|
|
def _extract_query(self, messages: list[dict[str, Any]]) -> str:
|
|
"""Extract query from messages for semantic caching.
|
|
|
|
Returns the last user message content as the query.
|
|
"""
|
|
for msg in reversed(messages):
|
|
if msg.get("role") == "user":
|
|
content = msg.get("content", "")
|
|
if isinstance(content, str):
|
|
return content
|
|
elif isinstance(content, list):
|
|
# Content block format
|
|
for block in content:
|
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
text_val = block.get("text", "")
|
|
return str(text_val) if text_val else ""
|
|
return ""
|
|
return ""
|
|
|
|
def _store_response_in_semantic_cache(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
response: Any,
|
|
model: str,
|
|
) -> None:
|
|
"""Store response in semantic cache for future hits."""
|
|
if self._semantic_cache_layer is not None:
|
|
cache_context = OptimizationContext(
|
|
provider=self._provider.name.lower(),
|
|
model=model,
|
|
query=self._extract_query(messages),
|
|
)
|
|
# Extract response content for caching
|
|
response_data = self._extract_response_content(response)
|
|
if response_data:
|
|
self._semantic_cache_layer.store_response(messages, response_data, cache_context)
|
|
|
|
def _extract_response_content(self, response: Any) -> dict[str, Any] | None:
|
|
"""Extract cacheable content from API response."""
|
|
try:
|
|
# OpenAI format
|
|
if hasattr(response, "choices") and response.choices:
|
|
choice = response.choices[0]
|
|
if hasattr(choice, "message"):
|
|
return {
|
|
"role": "assistant",
|
|
"content": choice.message.content,
|
|
}
|
|
# Anthropic format
|
|
elif hasattr(response, "content"):
|
|
return {
|
|
"role": "assistant",
|
|
"content": response.content,
|
|
}
|
|
except Exception:
|
|
logger.debug("Failed to extract response content for semantic cache", exc_info=True)
|
|
return None
|
|
|
|
def _simulate(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: list[dict[str, Any]],
|
|
headroom_mode: str = "optimize",
|
|
headroom_output_buffer_tokens: int | None = None,
|
|
headroom_tool_profiles: dict[str, dict[str, Any]] | None = None,
|
|
) -> SimulationResult:
|
|
"""Internal implementation of simulate."""
|
|
tokenizer = self._get_tokenizer(model)
|
|
|
|
# Analyze original
|
|
blocks, block_breakdown, waste_signals = parse_messages(messages, tokenizer)
|
|
tokens_before = tokenizer.count_messages(messages)
|
|
|
|
# Compute original cache alignment
|
|
aligner = CacheAligner(self._config.cache_aligner)
|
|
cache_alignment_score = aligner.get_alignment_score(messages)
|
|
compute_prefix_hash(messages)
|
|
|
|
# Apply transforms
|
|
output_buffer = headroom_output_buffer_tokens or self._config.output_buffer_tokens
|
|
model_limit = self._get_context_limit(model)
|
|
|
|
result = self._pipeline.simulate(
|
|
messages,
|
|
model,
|
|
model_limit=model_limit,
|
|
output_buffer=output_buffer,
|
|
tool_profiles=headroom_tool_profiles or {},
|
|
)
|
|
|
|
tokens_saved = tokens_before - result.tokens_after
|
|
|
|
# Estimate cost savings using provider (use output_buffer tokens)
|
|
# Note: output_buffer reserves tokens for expected model output
|
|
cost_before = estimate_cost(tokens_before, output_buffer, model, provider=self._provider)
|
|
cost_after = estimate_cost(
|
|
result.tokens_after, output_buffer, model, provider=self._provider
|
|
)
|
|
|
|
if cost_before is not None and cost_after is not None:
|
|
savings = format_cost(cost_before - cost_after)
|
|
else:
|
|
savings = "N/A"
|
|
|
|
# Recalculate prefix hash after optimization
|
|
optimized_prefix_hash = compute_prefix_hash(result.messages)
|
|
|
|
return SimulationResult(
|
|
tokens_before=tokens_before,
|
|
tokens_after=result.tokens_after,
|
|
tokens_saved=tokens_saved,
|
|
transforms=result.transforms_applied,
|
|
estimated_savings=f"{savings} per request",
|
|
messages_optimized=result.messages,
|
|
block_breakdown=block_breakdown,
|
|
waste_signals=waste_signals.to_dict(),
|
|
stable_prefix_hash=optimized_prefix_hash,
|
|
cache_alignment_score=cache_alignment_score,
|
|
)
|
|
|
|
def get_metrics(
|
|
self,
|
|
start_time: datetime | None = None,
|
|
end_time: datetime | None = None,
|
|
model: str | None = None,
|
|
mode: str | None = None,
|
|
limit: int = 100,
|
|
) -> list[RequestMetrics]:
|
|
"""
|
|
Query stored metrics.
|
|
|
|
Args:
|
|
start_time: Filter by timestamp >= start_time.
|
|
end_time: Filter by timestamp <= end_time.
|
|
model: Filter by model name.
|
|
mode: Filter by mode.
|
|
limit: Maximum results.
|
|
|
|
Returns:
|
|
List of RequestMetrics.
|
|
"""
|
|
return self._storage.query(
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
model=model,
|
|
mode=mode,
|
|
limit=limit,
|
|
)
|
|
|
|
def get_summary(
|
|
self,
|
|
start_time: datetime | None = None,
|
|
end_time: datetime | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get summary statistics.
|
|
|
|
Args:
|
|
start_time: Filter by timestamp >= start_time.
|
|
end_time: Filter by timestamp <= end_time.
|
|
|
|
Returns:
|
|
Summary statistics dict.
|
|
"""
|
|
return self._storage.get_summary_stats(start_time, end_time)
|
|
|
|
def close(self) -> None:
|
|
"""Close storage connection."""
|
|
self._storage.close()
|
|
|
|
def __enter__(self) -> HeadroomClient:
|
|
"""Context manager entry."""
|
|
return self
|
|
|
|
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
"""Context manager exit."""
|
|
self.close()
|
|
|
|
def validate_setup(self) -> dict[str, Any]:
|
|
"""Validate that Headroom is properly configured.
|
|
|
|
This method checks:
|
|
- Provider is valid and can count tokens
|
|
- Storage is accessible and writable
|
|
- Configuration is valid
|
|
- Cache optimizer (if enabled) is working
|
|
|
|
Returns:
|
|
dict with validation results:
|
|
{
|
|
"valid": True/False,
|
|
"provider": {"ok": bool, "name": str, "error": str | None},
|
|
"storage": {"ok": bool, "url": str, "error": str | None},
|
|
"config": {"ok": bool, "mode": str, "error": str | None},
|
|
"cache_optimizer": {"ok": bool, "name": str | None, "error": str | None},
|
|
}
|
|
|
|
Raises:
|
|
ValidationError: If validation fails and raise_on_error=True.
|
|
|
|
Example:
|
|
client = HeadroomClient(...)
|
|
result = client.validate_setup()
|
|
if not result["valid"]:
|
|
print("Setup issues:", result)
|
|
"""
|
|
result: dict[str, Any] = {
|
|
"valid": True,
|
|
"provider": {"ok": False, "name": None, "error": None},
|
|
"storage": {"ok": False, "url": self._store_url, "error": None},
|
|
"config": {"ok": False, "mode": self._default_mode.value, "error": None},
|
|
"cache_optimizer": {"ok": True, "name": None, "error": None},
|
|
}
|
|
|
|
# Validate provider
|
|
try:
|
|
result["provider"]["name"] = self._provider.name
|
|
# Test token counting
|
|
test_messages = [{"role": "user", "content": "test"}]
|
|
tokenizer = self._get_tokenizer("gpt-4")
|
|
count = tokenizer.count_messages(test_messages)
|
|
if count > 0:
|
|
result["provider"]["ok"] = True
|
|
else:
|
|
result["provider"]["error"] = "Token count returned 0"
|
|
result["valid"] = False
|
|
except Exception as e:
|
|
result["provider"]["error"] = str(e)
|
|
result["valid"] = False
|
|
|
|
# Validate storage
|
|
try:
|
|
# Try to get summary (tests read)
|
|
self._storage.get_summary_stats()
|
|
result["storage"]["ok"] = True
|
|
except Exception as e:
|
|
result["storage"]["error"] = str(e)
|
|
result["valid"] = False
|
|
|
|
# Validate config
|
|
try:
|
|
# Check mode is valid
|
|
if self._default_mode in (HeadroomMode.AUDIT, HeadroomMode.OPTIMIZE):
|
|
result["config"]["ok"] = True
|
|
else:
|
|
result["config"]["error"] = f"Invalid mode: {self._default_mode}"
|
|
result["valid"] = False
|
|
except Exception as e:
|
|
result["config"]["error"] = str(e)
|
|
result["valid"] = False
|
|
|
|
# Validate cache optimizer (if enabled)
|
|
if self._cache_optimizer is not None:
|
|
try:
|
|
result["cache_optimizer"]["name"] = self._cache_optimizer.name
|
|
result["cache_optimizer"]["ok"] = True
|
|
except Exception as e:
|
|
result["cache_optimizer"]["error"] = str(e)
|
|
# Don't fail validation for cache optimizer issues
|
|
elif self._config.cache_optimizer.enabled:
|
|
result["cache_optimizer"]["error"] = "Enabled but no optimizer loaded"
|
|
# Don't fail validation, just warn
|
|
|
|
return result
|
|
|
|
def get_stats(self) -> dict[str, Any]:
|
|
"""Get quick statistics without database query.
|
|
|
|
This returns in-memory stats tracked during this session.
|
|
For historical metrics, use get_metrics() or get_summary().
|
|
|
|
Returns:
|
|
dict with session statistics:
|
|
{
|
|
"session": {
|
|
"requests_total": int,
|
|
"requests_optimized": int,
|
|
"requests_audit": int,
|
|
"tokens_saved_total": int,
|
|
"cache_hits": int,
|
|
},
|
|
"config": {
|
|
"mode": str,
|
|
"provider": str,
|
|
"cache_optimizer": str | None,
|
|
"semantic_cache": bool,
|
|
},
|
|
"transforms": {
|
|
"smart_crusher_enabled": bool,
|
|
"cache_aligner_enabled": bool,
|
|
},
|
|
}
|
|
|
|
Example:
|
|
stats = client.get_stats()
|
|
print(f"Saved {stats['session']['tokens_saved_total']} tokens this session")
|
|
"""
|
|
# Initialize session stats if not present
|
|
if not hasattr(self, "_session_stats"):
|
|
self._session_stats = {
|
|
"requests_total": 0,
|
|
"requests_optimized": 0,
|
|
"requests_audit": 0,
|
|
"tokens_saved_total": 0,
|
|
"cache_hits": 0,
|
|
}
|
|
|
|
return {
|
|
"session": dict(self._session_stats),
|
|
"config": {
|
|
"mode": self._default_mode.value,
|
|
"provider": self._provider.name,
|
|
"cache_optimizer": (self._cache_optimizer.name if self._cache_optimizer else None),
|
|
"semantic_cache": self._semantic_cache_layer is not None,
|
|
},
|
|
"transforms": {
|
|
"smart_crusher_enabled": self._config.smart_crusher.enabled,
|
|
"cache_aligner_enabled": self._config.cache_aligner.enabled,
|
|
},
|
|
}
|
|
|
|
def _update_session_stats(
|
|
self,
|
|
mode: HeadroomMode,
|
|
tokens_before: int,
|
|
tokens_after: int,
|
|
cache_hit: bool = False,
|
|
) -> None:
|
|
"""Update in-memory session statistics."""
|
|
if not hasattr(self, "_session_stats"):
|
|
self._session_stats = {
|
|
"requests_total": 0,
|
|
"requests_optimized": 0,
|
|
"requests_audit": 0,
|
|
"tokens_saved_total": 0,
|
|
"cache_hits": 0,
|
|
}
|
|
|
|
self._session_stats["requests_total"] += 1
|
|
|
|
if mode == HeadroomMode.OPTIMIZE:
|
|
self._session_stats["requests_optimized"] += 1
|
|
self._session_stats["tokens_saved_total"] += max(0, tokens_before - tokens_after)
|
|
else:
|
|
self._session_stats["requests_audit"] += 1
|
|
|
|
if cache_hit:
|
|
self._session_stats["cache_hits"] += 1
|