autotune-hook 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: autotune-hook
3
+ Version: 0.1.0
4
+ Summary: Autotune Hook
5
+ Author: Autotune
6
+ License-Expression: LicenseRef-Proprietary
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Autotune Hook
14
+
15
+ Python hook for wrapping OpenRouter calls with AutoTune workflow policy.
16
+
17
+ ```python
18
+ from autotune_hook import AutotuneHook
19
+
20
+ hook = AutotuneHook(workflow="email-triage")
21
+
22
+ def call_openrouter(payload):
23
+ return hook.openrouter_call(
24
+ payload,
25
+ lambda: openrouter_client.chat.completions.create(**payload),
26
+ )
27
+ ```
28
+
29
+ Decorator form:
30
+
31
+ ```python
32
+ @hook.openrouter()
33
+ def call_openrouter(**payload):
34
+ return openrouter_client.chat.completions.create(**payload)
35
+ ```
36
+
37
+ Configuration defaults to environment variables:
38
+
39
+ - `AUTOTUNE_ENDPOINT`
40
+ - `AUTOTUNE_API_KEY`
41
+ - `AUTOTUNE_WORKFLOW`
42
+ - `AUTOTUNE_PROJECT`
43
+ - `AUTOTUNE_ENVIRONMENT`
44
+ - `AUTOTUNE_SESSION_ID`
45
+ - `AUTOTUNE_USER_ID`
46
+
47
+ The hook sends the OpenRouter request to AutoTune first. A JSON boolean response
48
+ means shadow mode, so the wrapped provider call executes and its output is
49
+ reported in the background. A non-boolean AutoTune response is returned directly.
@@ -0,0 +1,37 @@
1
+ # Autotune Hook
2
+
3
+ Python hook for wrapping OpenRouter calls with AutoTune workflow policy.
4
+
5
+ ```python
6
+ from autotune_hook import AutotuneHook
7
+
8
+ hook = AutotuneHook(workflow="email-triage")
9
+
10
+ def call_openrouter(payload):
11
+ return hook.openrouter_call(
12
+ payload,
13
+ lambda: openrouter_client.chat.completions.create(**payload),
14
+ )
15
+ ```
16
+
17
+ Decorator form:
18
+
19
+ ```python
20
+ @hook.openrouter()
21
+ def call_openrouter(**payload):
22
+ return openrouter_client.chat.completions.create(**payload)
23
+ ```
24
+
25
+ Configuration defaults to environment variables:
26
+
27
+ - `AUTOTUNE_ENDPOINT`
28
+ - `AUTOTUNE_API_KEY`
29
+ - `AUTOTUNE_WORKFLOW`
30
+ - `AUTOTUNE_PROJECT`
31
+ - `AUTOTUNE_ENVIRONMENT`
32
+ - `AUTOTUNE_SESSION_ID`
33
+ - `AUTOTUNE_USER_ID`
34
+
35
+ The hook sends the OpenRouter request to AutoTune first. A JSON boolean response
36
+ means shadow mode, so the wrapped provider call executes and its output is
37
+ reported in the background. A non-boolean AutoTune response is returned directly.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "autotune-hook"
7
+ version = "0.1.0"
8
+ description = "Autotune Hook"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "LicenseRef-Proprietary"
12
+ authors = [{ name = "Autotune" }]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Topic :: Software Development :: Libraries :: Python Modules",
17
+ ]
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+
22
+ [tool.setuptools.package-data]
23
+ autotune_hook = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ from .hook import (
2
+ AutotuneHook,
3
+ AutotuneHookConfig,
4
+ autotune_openrouter,
5
+ current_session,
6
+ with_session,
7
+ )
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ __all__ = [
12
+ "AutotuneHook",
13
+ "AutotuneHookConfig",
14
+ "autotune_openrouter",
15
+ "current_session",
16
+ "with_session",
17
+ "__version__",
18
+ ]
@@ -0,0 +1,423 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import contextvars
6
+ import datetime as _dt
7
+ import functools
8
+ import inspect
9
+ import json
10
+ import logging
11
+ import os
12
+ import threading
13
+ import time
14
+ import uuid
15
+ import urllib.error
16
+ import urllib.request
17
+ from dataclasses import dataclass
18
+ from typing import Any, Callable, Iterator, Mapping, MutableMapping, TypeVar
19
+
20
+ LOGGER = logging.getLogger("autotune_hook")
21
+ T = TypeVar("T")
22
+
23
+ JsonMap = dict[str, Any]
24
+ RequestExtractor = Callable[[tuple[Any, ...], dict[str, Any]], Mapping[str, Any] | None]
25
+ ResponseAdapter = Callable[[Any], T]
26
+
27
+
28
+ _session: contextvars.ContextVar[JsonMap] = contextvars.ContextVar(
29
+ "autotune_session",
30
+ default={},
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class AutotuneHookConfig:
36
+ endpoint: str | None = None
37
+ api_key: str | None = None
38
+ workflow: str | None = None
39
+ project: str | None = None
40
+ environment: str | None = None
41
+ session_id: str | None = None
42
+ user_id: str | None = None
43
+ timeout_seconds: float = 2.0
44
+
45
+ @classmethod
46
+ def from_env(cls) -> "AutotuneHookConfig":
47
+ return cls(
48
+ endpoint=os.getenv("AUTOTUNE_ENDPOINT"),
49
+ api_key=os.getenv("AUTOTUNE_API_KEY"),
50
+ workflow=os.getenv("AUTOTUNE_WORKFLOW"),
51
+ project=os.getenv("AUTOTUNE_PROJECT"),
52
+ environment=os.getenv("AUTOTUNE_ENVIRONMENT"),
53
+ session_id=os.getenv("AUTOTUNE_SESSION_ID"),
54
+ user_id=os.getenv("AUTOTUNE_USER_ID"),
55
+ timeout_seconds=float(os.getenv("AUTOTUNE_HOOK_TIMEOUT", "2.0")),
56
+ )
57
+
58
+
59
+ class AutotuneHook:
60
+ def __init__(
61
+ self,
62
+ *,
63
+ endpoint: str | None = None,
64
+ api_key: str | None = None,
65
+ workflow: str | None = None,
66
+ project: str | None = None,
67
+ environment: str | None = None,
68
+ session_id: str | None = None,
69
+ user_id: str | None = None,
70
+ timeout_seconds: float | None = None,
71
+ ) -> None:
72
+ env = AutotuneHookConfig.from_env()
73
+ self.config = AutotuneHookConfig(
74
+ endpoint=endpoint if endpoint is not None else env.endpoint,
75
+ api_key=api_key if api_key is not None else env.api_key,
76
+ workflow=workflow if workflow is not None else env.workflow,
77
+ project=project if project is not None else env.project,
78
+ environment=environment if environment is not None else env.environment,
79
+ session_id=session_id if session_id is not None else env.session_id,
80
+ user_id=user_id if user_id is not None else env.user_id,
81
+ timeout_seconds=(
82
+ timeout_seconds
83
+ if timeout_seconds is not None
84
+ else env.timeout_seconds
85
+ ),
86
+ )
87
+
88
+ def openrouter_call(
89
+ self,
90
+ request: Mapping[str, Any],
91
+ call: Callable[[], T],
92
+ *,
93
+ metadata: Mapping[str, Any] | None = None,
94
+ response_adapter: ResponseAdapter[T] | None = None,
95
+ ) -> T:
96
+ """Wrap one OpenRouter call.
97
+
98
+ AutoTune receives the request first. If cloud policy returns a JSON
99
+ boolean, this method executes ``call`` and reports its observed output in
100
+ the background. If AutoTune returns a provider response, that response is
101
+ returned directly, optionally through ``response_adapter``.
102
+ """
103
+ started_at = _now_iso()
104
+ started = time.perf_counter()
105
+ run_id = str(uuid.uuid4())
106
+ request_body = _with_metadata(dict(request), self._metadata(metadata))
107
+ decision = self._preflight(run_id, request_body, started_at)
108
+ should_report = decision is _SHADOW
109
+ if decision is not _SHADOW and decision is not _SHADOW_NO_REPORT:
110
+ return _adapt_response(decision, response_adapter)
111
+
112
+ try:
113
+ response = call()
114
+ except Exception as exc:
115
+ latency_ms = int((time.perf_counter() - started) * 1000)
116
+ if should_report:
117
+ self._report_background(
118
+ run_id=run_id,
119
+ request=request_body,
120
+ started_at=started_at,
121
+ latency_ms=latency_ms,
122
+ error=_serialize_error(exc),
123
+ )
124
+ raise
125
+
126
+ latency_ms = int((time.perf_counter() - started) * 1000)
127
+ if should_report:
128
+ self._report_background(
129
+ run_id=run_id,
130
+ request=request_body,
131
+ started_at=started_at,
132
+ latency_ms=latency_ms,
133
+ response=_jsonish(response),
134
+ status_code=_read_status_code(response),
135
+ )
136
+ return response
137
+
138
+ async def openrouter_call_async(
139
+ self,
140
+ request: Mapping[str, Any],
141
+ call: Callable[[], Any],
142
+ *,
143
+ metadata: Mapping[str, Any] | None = None,
144
+ response_adapter: ResponseAdapter[T] | None = None,
145
+ ) -> Any:
146
+ started_at = _now_iso()
147
+ started = time.perf_counter()
148
+ run_id = str(uuid.uuid4())
149
+ request_body = _with_metadata(dict(request), self._metadata(metadata))
150
+ decision = await asyncio.to_thread(self._preflight, run_id, request_body, started_at)
151
+ should_report = decision is _SHADOW
152
+ if decision is not _SHADOW and decision is not _SHADOW_NO_REPORT:
153
+ return _adapt_response(decision, response_adapter)
154
+
155
+ try:
156
+ response = call()
157
+ if inspect.isawaitable(response):
158
+ response = await response
159
+ except Exception as exc:
160
+ latency_ms = int((time.perf_counter() - started) * 1000)
161
+ if should_report:
162
+ self._report_task(
163
+ run_id=run_id,
164
+ request=request_body,
165
+ started_at=started_at,
166
+ latency_ms=latency_ms,
167
+ error=_serialize_error(exc),
168
+ )
169
+ raise
170
+
171
+ latency_ms = int((time.perf_counter() - started) * 1000)
172
+ if should_report:
173
+ self._report_task(
174
+ run_id=run_id,
175
+ request=request_body,
176
+ started_at=started_at,
177
+ latency_ms=latency_ms,
178
+ response=_jsonish(response),
179
+ status_code=_read_status_code(response),
180
+ )
181
+ return response
182
+
183
+ def openrouter(
184
+ self,
185
+ *,
186
+ request_extractor: RequestExtractor | None = None,
187
+ metadata: Mapping[str, Any] | None = None,
188
+ response_adapter: ResponseAdapter[Any] | None = None,
189
+ ) -> Callable[[Callable[..., T]], Callable[..., T]]:
190
+ extractor = request_extractor or _default_request_extractor
191
+
192
+ def decorator(func: Callable[..., T]) -> Callable[..., T]:
193
+ if inspect.iscoroutinefunction(func):
194
+
195
+ @functools.wraps(func)
196
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
197
+ request = extractor(args, kwargs)
198
+ if request is None:
199
+ LOGGER.warning("AutoTune hook could not infer OpenRouter request")
200
+ return await func(*args, **kwargs) # type: ignore[misc]
201
+ return await self.openrouter_call_async(
202
+ request,
203
+ lambda: func(*args, **kwargs),
204
+ metadata=metadata,
205
+ response_adapter=response_adapter,
206
+ )
207
+
208
+ return async_wrapper # type: ignore[return-value]
209
+
210
+ @functools.wraps(func)
211
+ def wrapper(*args: Any, **kwargs: Any) -> T:
212
+ request = extractor(args, kwargs)
213
+ if request is None:
214
+ LOGGER.warning("AutoTune hook could not infer OpenRouter request")
215
+ return func(*args, **kwargs)
216
+ return self.openrouter_call(
217
+ request,
218
+ lambda: func(*args, **kwargs),
219
+ metadata=metadata,
220
+ response_adapter=response_adapter,
221
+ )
222
+
223
+ return wrapper
224
+
225
+ return decorator
226
+
227
+ def _metadata(self, metadata: Mapping[str, Any] | None = None) -> JsonMap:
228
+ session = current_session()
229
+ pairs = {
230
+ "workflow_name": session.get("workflow") or self.config.workflow,
231
+ "project_name": session.get("project") or self.config.project,
232
+ "environment_name": session.get("environment") or self.config.environment,
233
+ "session_id": session.get("session_id") or self.config.session_id,
234
+ "user_id": session.get("user_id") or self.config.user_id,
235
+ }
236
+ merged = {key: value for key, value in pairs.items() if value}
237
+ if metadata:
238
+ merged.update(dict(metadata))
239
+ return merged
240
+
241
+ def _hook_url(self) -> str | None:
242
+ if not self.config.endpoint:
243
+ return None
244
+ return (
245
+ self.config.endpoint.rstrip("/")
246
+ + "/v1/hooks/openrouter/chat/completions"
247
+ )
248
+
249
+ def _preflight(self, run_id: str, request: JsonMap, started_at: str) -> Any:
250
+ url = self._hook_url()
251
+ if not url:
252
+ return _SHADOW
253
+ payload = {
254
+ "provider": "openrouter",
255
+ "run_id": run_id,
256
+ "request": request,
257
+ "started_at": started_at,
258
+ "metadata": request.get("metadata", {}),
259
+ }
260
+ response = self._post_json(url, payload)
261
+ if response is _HTTP_ERROR:
262
+ return _SHADOW_NO_REPORT
263
+ return _SHADOW if isinstance(response, bool) else response
264
+
265
+ def _report_background(self, **run: Any) -> None:
266
+ thread = threading.Thread(target=self._report, kwargs=run, daemon=True)
267
+ thread.start()
268
+
269
+ def _report_task(self, **run: Any) -> None:
270
+ async def send() -> None:
271
+ await asyncio.to_thread(self._report, **run)
272
+
273
+ try:
274
+ asyncio.create_task(send())
275
+ except RuntimeError:
276
+ self._report_background(**run)
277
+
278
+ def _report(self, **run: Any) -> None:
279
+ url = self._hook_url()
280
+ if not url:
281
+ return
282
+ payload = {
283
+ "run": {
284
+ "run_id": run["run_id"],
285
+ "provider": "openrouter",
286
+ "request": run["request"],
287
+ "started_at": run["started_at"],
288
+ "latency_ms": run["latency_ms"],
289
+ "metadata": run["request"].get("metadata", {}),
290
+ }
291
+ }
292
+ if run.get("response") is not None:
293
+ payload["run"]["response"] = run["response"]
294
+ if run.get("error") is not None:
295
+ payload["run"]["error"] = run["error"]
296
+ if run.get("status_code") is not None:
297
+ payload["run"]["status_code"] = run["status_code"]
298
+ self._post_json(url, payload)
299
+
300
+ def _post_json(self, url: str, payload: Mapping[str, Any]) -> Any:
301
+ body = json.dumps(_jsonish(payload), separators=(",", ":")).encode("utf-8")
302
+ headers = {"content-type": "application/json"}
303
+ if self.config.api_key:
304
+ headers["authorization"] = f"Bearer {self.config.api_key}"
305
+ request = urllib.request.Request(url, data=body, headers=headers, method="POST")
306
+ try:
307
+ with urllib.request.urlopen(
308
+ request,
309
+ timeout=self.config.timeout_seconds,
310
+ ) as response:
311
+ raw = response.read()
312
+ if not raw:
313
+ return None
314
+ content_type = response.headers.get("content-type", "")
315
+ if "application/json" in content_type:
316
+ return json.loads(raw.decode("utf-8"))
317
+ return raw.decode("utf-8", "replace")
318
+ except (OSError, urllib.error.URLError, urllib.error.HTTPError) as exc:
319
+ LOGGER.warning("AutoTune hook request failed: %s", exc)
320
+ return _HTTP_ERROR
321
+
322
+
323
+ def autotune_openrouter(**options: Any) -> Callable[[Callable[..., T]], Callable[..., T]]:
324
+ """Convenience decorator using environment-backed configuration."""
325
+ return AutotuneHook().openrouter(**options)
326
+
327
+
328
+ @contextlib.contextmanager
329
+ def with_session(**context: Any) -> Iterator[None]:
330
+ parent = current_session()
331
+ token = _session.set({**parent, **{k: v for k, v in context.items() if v}})
332
+ try:
333
+ yield
334
+ finally:
335
+ _session.reset(token)
336
+
337
+
338
+ def current_session() -> JsonMap:
339
+ return dict(_session.get({}))
340
+
341
+
342
+ def _default_request_extractor(
343
+ args: tuple[Any, ...],
344
+ kwargs: dict[str, Any],
345
+ ) -> Mapping[str, Any] | None:
346
+ if args and isinstance(args[0], Mapping):
347
+ return args[0]
348
+ request = kwargs.get("request")
349
+ if isinstance(request, Mapping):
350
+ return request
351
+ if kwargs:
352
+ return kwargs
353
+ return None
354
+
355
+
356
+ def _with_metadata(request: MutableMapping[str, Any], metadata: Mapping[str, Any]) -> JsonMap:
357
+ existing = request.get("metadata")
358
+ request["metadata"] = {
359
+ **(dict(existing) if isinstance(existing, Mapping) else {}),
360
+ **dict(metadata),
361
+ }
362
+ return dict(request)
363
+
364
+
365
+ def _adapt_response(value: Any, adapter: ResponseAdapter[T] | None) -> T:
366
+ return adapter(value) if adapter else value
367
+
368
+
369
+ def _jsonish(value: Any) -> Any:
370
+ if value is None or isinstance(value, (bool, int, float, str)):
371
+ return value
372
+ if isinstance(value, Mapping):
373
+ return {str(key): _jsonish(item) for key, item in value.items()}
374
+ if isinstance(value, (list, tuple, set)):
375
+ return [_jsonish(item) for item in value]
376
+ model_dump = getattr(value, "model_dump", None)
377
+ if callable(model_dump):
378
+ try:
379
+ return _jsonish(model_dump(mode="json"))
380
+ except TypeError:
381
+ return _jsonish(model_dump())
382
+ to_dict = getattr(value, "dict", None)
383
+ if callable(to_dict):
384
+ return _jsonish(to_dict())
385
+ json_method = getattr(value, "json", None)
386
+ if callable(json_method):
387
+ try:
388
+ parsed = json_method()
389
+ return _jsonish(parsed)
390
+ except Exception:
391
+ pass
392
+ text = getattr(value, "text", None)
393
+ if isinstance(text, str):
394
+ try:
395
+ return json.loads(text)
396
+ except json.JSONDecodeError:
397
+ return text
398
+ return str(value)
399
+
400
+
401
+ def _read_status_code(value: Any) -> int | None:
402
+ status = getattr(value, "status_code", None)
403
+ return status if isinstance(status, int) else None
404
+
405
+
406
+ def _serialize_error(exc: BaseException) -> JsonMap:
407
+ return {
408
+ "type": exc.__class__.__name__,
409
+ "message": str(exc),
410
+ }
411
+
412
+
413
+ def _now_iso() -> str:
414
+ return _dt.datetime.now(_dt.timezone.utc).isoformat().replace("+00:00", "Z")
415
+
416
+
417
+ class _Sentinel:
418
+ pass
419
+
420
+
421
+ _SHADOW = _Sentinel()
422
+ _SHADOW_NO_REPORT = _Sentinel()
423
+ _HTTP_ERROR = _Sentinel()
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: autotune-hook
3
+ Version: 0.1.0
4
+ Summary: Autotune Hook
5
+ Author: Autotune
6
+ License-Expression: LicenseRef-Proprietary
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Autotune Hook
14
+
15
+ Python hook for wrapping OpenRouter calls with AutoTune workflow policy.
16
+
17
+ ```python
18
+ from autotune_hook import AutotuneHook
19
+
20
+ hook = AutotuneHook(workflow="email-triage")
21
+
22
+ def call_openrouter(payload):
23
+ return hook.openrouter_call(
24
+ payload,
25
+ lambda: openrouter_client.chat.completions.create(**payload),
26
+ )
27
+ ```
28
+
29
+ Decorator form:
30
+
31
+ ```python
32
+ @hook.openrouter()
33
+ def call_openrouter(**payload):
34
+ return openrouter_client.chat.completions.create(**payload)
35
+ ```
36
+
37
+ Configuration defaults to environment variables:
38
+
39
+ - `AUTOTUNE_ENDPOINT`
40
+ - `AUTOTUNE_API_KEY`
41
+ - `AUTOTUNE_WORKFLOW`
42
+ - `AUTOTUNE_PROJECT`
43
+ - `AUTOTUNE_ENVIRONMENT`
44
+ - `AUTOTUNE_SESSION_ID`
45
+ - `AUTOTUNE_USER_ID`
46
+
47
+ The hook sends the OpenRouter request to AutoTune first. A JSON boolean response
48
+ means shadow mode, so the wrapped provider call executes and its output is
49
+ reported in the background. A non-boolean AutoTune response is returned directly.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/autotune_hook/__init__.py
4
+ src/autotune_hook/hook.py
5
+ src/autotune_hook/py.typed
6
+ src/autotune_hook.egg-info/PKG-INFO
7
+ src/autotune_hook.egg-info/SOURCES.txt
8
+ src/autotune_hook.egg-info/dependency_links.txt
9
+ src/autotune_hook.egg-info/top_level.txt
10
+ tests/test_hook.py
@@ -0,0 +1 @@
1
+ autotune_hook
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import threading
6
+ import time
7
+ import unittest
8
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9
+ from typing import Any
10
+
11
+ from autotune_hook import AutotuneHook, with_session
12
+
13
+
14
+ class HookHandler(BaseHTTPRequestHandler):
15
+ preflight_responses: list[Any] = [True]
16
+ preflights: list[dict[str, Any]] = []
17
+ reports: list[dict[str, Any]] = []
18
+
19
+ def do_POST(self) -> None:
20
+ length = int(self.headers.get("content-length", "0"))
21
+ body = json.loads(self.rfile.read(length).decode("utf-8"))
22
+ if "run" in body:
23
+ self.__class__.reports.append(body)
24
+ self.send_response(202)
25
+ self.send_header("content-type", "application/json")
26
+ self.end_headers()
27
+ self.wfile.write(b'{"status":"recorded"}')
28
+ return
29
+
30
+ self.__class__.preflights.append(body)
31
+ response = (
32
+ self.__class__.preflight_responses.pop(0)
33
+ if self.__class__.preflight_responses
34
+ else True
35
+ )
36
+ self.send_response(200)
37
+ self.send_header("content-type", "application/json")
38
+ self.end_headers()
39
+ self.wfile.write(json.dumps(response).encode("utf-8"))
40
+
41
+ def log_message(self, _format: str, *_args: Any) -> None:
42
+ return
43
+
44
+
45
+ class StubServer:
46
+ def __enter__(self) -> "StubServer":
47
+ HookHandler.preflight_responses = [True]
48
+ HookHandler.preflights = []
49
+ HookHandler.reports = []
50
+ self.server = ThreadingHTTPServer(("127.0.0.1", 0), HookHandler)
51
+ self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
52
+ self.thread.start()
53
+ host, port = self.server.server_address
54
+ self.url = f"http://{host}:{port}"
55
+ return self
56
+
57
+ def __exit__(self, *_exc: Any) -> None:
58
+ self.server.shutdown()
59
+ self.server.server_close()
60
+ self.thread.join(timeout=1)
61
+
62
+
63
+ def wait_for_report(count: int = 1) -> list[dict[str, Any]]:
64
+ deadline = time.time() + 2
65
+ while time.time() < deadline:
66
+ if len(HookHandler.reports) >= count:
67
+ return HookHandler.reports
68
+ time.sleep(0.02)
69
+ return HookHandler.reports
70
+
71
+
72
+ async def wait_for_report_async(count: int = 1) -> list[dict[str, Any]]:
73
+ deadline = time.time() + 2
74
+ while time.time() < deadline:
75
+ if len(HookHandler.reports) >= count:
76
+ return HookHandler.reports
77
+ await asyncio.sleep(0.02)
78
+ return HookHandler.reports
79
+
80
+
81
+ class AutotuneHookTest(unittest.TestCase):
82
+ def test_shadow_wrapper_reports_response(self) -> None:
83
+ with StubServer() as server:
84
+ hook = AutotuneHook(endpoint=server.url, workflow="email-triage", timeout_seconds=1)
85
+ payload = {
86
+ "model": "openrouter/stub",
87
+ "messages": [{"role": "user", "content": "hi"}],
88
+ }
89
+ provider_response = {
90
+ "choices": [
91
+ {"message": {"role": "assistant", "content": "hello"}}
92
+ ],
93
+ "usage": {"prompt_tokens": 1, "completion_tokens": 1},
94
+ }
95
+
96
+ result = hook.openrouter_call(payload, lambda: provider_response)
97
+
98
+ self.assertEqual(result, provider_response)
99
+ self.assertEqual(len(HookHandler.preflights), 1)
100
+ self.assertEqual(
101
+ HookHandler.preflights[0]["request"]["metadata"]["workflow_name"],
102
+ "email-triage",
103
+ )
104
+ reports = wait_for_report()
105
+ self.assertEqual(len(reports), 1)
106
+ self.assertEqual(reports[0]["run"]["response"], provider_response)
107
+ self.assertEqual(
108
+ reports[0]["run"]["request"]["metadata"]["workflow_name"],
109
+ "email-triage",
110
+ )
111
+
112
+ def test_proxy_response_short_circuits_provider_call(self) -> None:
113
+ proxy_response = {
114
+ "id": "chatcmpl-at",
115
+ "choices": [{"message": {"role": "assistant", "content": "from autotune"}}],
116
+ }
117
+ with StubServer() as server:
118
+ HookHandler.preflight_responses = [proxy_response]
119
+ hook = AutotuneHook(endpoint=server.url)
120
+ called = False
121
+
122
+ def provider() -> dict[str, Any]:
123
+ nonlocal called
124
+ called = True
125
+ return {"unexpected": True}
126
+
127
+ result = hook.openrouter_call({"messages": []}, provider)
128
+
129
+ self.assertEqual(result, proxy_response)
130
+ self.assertFalse(called)
131
+ self.assertEqual(HookHandler.reports, [])
132
+
133
+ def test_decorator_infers_kwargs_and_session(self) -> None:
134
+ with StubServer() as server:
135
+ hook = AutotuneHook(endpoint=server.url, workflow="fallback")
136
+
137
+ @hook.openrouter()
138
+ def call_openrouter(**payload: Any) -> dict[str, Any]:
139
+ return {
140
+ "choices": [
141
+ {"message": {"role": "assistant", "content": payload["messages"][0]["content"]}}
142
+ ]
143
+ }
144
+
145
+ with with_session(session_id="session-1", user_id="user-1", workflow="session-wf"):
146
+ result = call_openrouter(
147
+ model="stub",
148
+ messages=[{"role": "user", "content": "decorated"}],
149
+ )
150
+
151
+ self.assertEqual(result["choices"][0]["message"]["content"], "decorated")
152
+ reports = wait_for_report()
153
+ metadata = reports[0]["run"]["request"]["metadata"]
154
+ self.assertEqual(metadata["session_id"], "session-1")
155
+ self.assertEqual(metadata["user_id"], "user-1")
156
+ self.assertEqual(metadata["workflow_name"], "session-wf")
157
+
158
+ def test_autotune_unavailable_falls_back_to_provider(self) -> None:
159
+ hook = AutotuneHook(endpoint="http://127.0.0.1:1", timeout_seconds=0.05)
160
+ result = hook.openrouter_call({"messages": []}, lambda: {"ok": True})
161
+ self.assertEqual(result, {"ok": True})
162
+
163
+
164
+ class AutotuneHookAsyncTest(unittest.IsolatedAsyncioTestCase):
165
+ async def test_async_shadow_wrapper(self) -> None:
166
+ with StubServer() as server:
167
+ hook = AutotuneHook(endpoint=server.url, workflow="async-wf")
168
+
169
+ async def provider() -> dict[str, Any]:
170
+ await asyncio.sleep(0)
171
+ return {"choices": [{"message": {"content": "async"}}]}
172
+
173
+ result = await hook.openrouter_call_async({"messages": []}, provider)
174
+
175
+ self.assertEqual(result["choices"][0]["message"]["content"], "async")
176
+ reports = await wait_for_report_async()
177
+ self.assertEqual(reports[0]["run"]["request"]["metadata"]["workflow_name"], "async-wf")
178
+
179
+
180
+ if __name__ == "__main__":
181
+ unittest.main()