autotune-hook 0.1.0__py3-none-any.whl
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,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
|
+
]
|
autotune_hook/hook.py
ADDED
|
@@ -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()
|
autotune_hook/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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,7 @@
|
|
|
1
|
+
autotune_hook/__init__.py,sha256=bDHZCVWjkNwW31pccjfBjY6Rkf8GnkrXg7nWAgv3g9Q,301
|
|
2
|
+
autotune_hook/hook.py,sha256=ASoCgWw_epT07G8MI5Kv_UJ7PYksvyvEySpTGRNwnOo,14759
|
|
3
|
+
autotune_hook/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
4
|
+
autotune_hook-0.1.0.dist-info/METADATA,sha256=xZeJ8jILL8d-BG8q7pRzKQtRJ_iA8u4mxMMT5PvPHTc,1322
|
|
5
|
+
autotune_hook-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
autotune_hook-0.1.0.dist-info/top_level.txt,sha256=DxfTuGA08Jo3AzAXFBl2ckYVSD0jfB33EIUANJ9gGZw,14
|
|
7
|
+
autotune_hook-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
autotune_hook
|