Metadata-Version: 2.4
Name: autotune-hook
Version: 0.1.2
Summary: Autotune Hook
Author: Autotune
License-Expression: LicenseRef-Proprietary
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: httpx
Requires-Dist: httpx>=0.27; extra == "httpx"

# Autotune Hook

Python instrumentation for reporting OpenRouter calls to AutoTune.

## Transport integration (recommended)

When requests are built inside an SDK (openai-python, pydantic-ai), wrap the
app's httpx transport once and every OpenRouter chat completion that flows
through it is shadow-reported in the background — no call-site changes,
observation only, and AutoTune being down can never block or fail a model
call. Requires the `httpx` extra (`pip install autotune-hook[httpx]`).

```python
import httpx
from autotune_hook import AutotuneAsyncTransport

client = httpx.AsyncClient(transport=AutotuneAsyncTransport())
openrouter = AsyncOpenAI(base_url="https://openrouter.ai/api/v1", http_client=client)
```

Already wrapping transports (retries, sanitizers)? Put the AutoTune transport
outermost so one logical call maps to one recorded run — the final response
the app saw.

## Wrapper / decorator integration

For call sites that hold the request payload directly. These also honor cloud
workflow policy: the request is preflighted, and AutoTune answers with an
explicit envelope — `{"mode": "shadow", "ttl_seconds": N}` (execute the
wrapped call, report its output in the background, and skip further
preflights for that workflow until the TTL expires) or
`{"mode": "proxy", "response": {...}}` (return the AutoTune-executed
response). Any response without the envelope falls back to the app's own
call, so an intermediary's bare 200 can never be mistaken for an LLM
response. Streaming requests pass through unobserved.

```python
from autotune_hook import AutotuneHook

hook = AutotuneHook(workflow="email-triage")

def call_openrouter(payload):
    return hook.openrouter_call(
        payload,
        lambda: openrouter_client.chat.completions.create(**payload),
    )
```

Decorator form:

```python
@hook.openrouter()
def call_openrouter(**payload):
    return openrouter_client.chat.completions.create(**payload)
```

## Sessions

```python
from autotune_hook import with_session

with with_session(session_id=run_id, user_id=user_id, workflow="ocr"):
    ...  # every hooked/teed call inside is attributed
```

## Reporting behavior

Reports ride a single background thread with a bounded queue: they never
block the calling thread or event loop, overflow drops reports (with a
warning) rather than growing memory, and `flush_reports()` drains pending
reports at worker shutdown (also registered via `atexit`). Inline `data:`
URLs larger than 256 bytes (base64 images) are replaced with a
size-and-hash placeholder before reporting; disable with
`AUTOTUNE_HOOK_REDACT_DATA_URLS=0`. Reports whose serialized size exceeds
`AUTOTUNE_HOOK_MAX_REPORT_BYTES` (default 4 MB) are dropped with a warning.

## Configuration

Set `AUTOTUNE_ENDPOINT` and, when required, `AUTOTUNE_API_KEY` in the
environment. If `AUTOTUNE_ENDPOINT` is unset, the hook stays fully out of the
way.

Dynamic attribution belongs in code, not static process env. Pass workflow,
session, user, project, and environment values through `with_session(...)`,
`AutotuneHook(...)`, or per-call metadata so each request is attributed to the
right run.
