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
122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
"""Turn hooks — observe and optionally re-drive a single model turn.
|
|
|
|
A *turn hook* wraps the proxy's buffered upstream call: it sees the outbound
|
|
request and the model's response, and may call the model again (via
|
|
``call_model``) to return a *replacement* response — transparently to the client.
|
|
That covers a range of turn-level behaviors an extension might want: resolving an
|
|
injected tool call, enforcing a guardrail, retrying on a bad response, serving a
|
|
cached answer, or running a small model→proxy→model loop before handing back the
|
|
final answer.
|
|
|
|
Hooks are registered by opt-in proxy extensions (``proxy/extensions.py``). This
|
|
module is **inert unless a hook is registered**: the runner helpers return their
|
|
input unchanged, so with no hooks the proxy behaves exactly as if this module did
|
|
not exist. Hooks must never raise — a failing hook is logged and skipped so it
|
|
cannot take the proxy down.
|
|
|
|
Stability: the ``TurnHook`` protocol and the registry/runner functions are part
|
|
of the extension surface (see ``proxy/extensions.py``); signature changes follow
|
|
the same deprecation policy.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Awaitable, Callable
|
|
from dataclasses import dataclass
|
|
from typing import Any, Protocol, runtime_checkable
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# Re-drive the model with a message list; returns the provider's response JSON.
|
|
# The exact message/response shapes are provider-native (Anthropic / OpenAI /
|
|
# Google), matching whatever the surrounding handler already works with.
|
|
CallModel = Callable[[list[dict[str, Any]]], Awaitable[dict[str, Any]]]
|
|
|
|
|
|
@dataclass
|
|
class TurnContext:
|
|
"""The request side of one model turn, as the proxy forwards it upstream.
|
|
|
|
``tools`` and ``messages`` are the live objects the handler is about to send
|
|
(or just sent); a hook's ``on_request`` may mutate them in place.
|
|
"""
|
|
|
|
provider: str # "anthropic" | "openai" | "google" | ...
|
|
model: str
|
|
messages: list[dict[str, Any]]
|
|
tools: Any = None # provider-native tools value (list, or None)
|
|
config: Any = None
|
|
|
|
|
|
@runtime_checkable
|
|
class TurnHook(Protocol):
|
|
"""A registered turn observer. Both methods are optional (a hook may define
|
|
either); missing methods are simply skipped."""
|
|
|
|
name: str
|
|
|
|
def on_request(self, ctx: TurnContext) -> None:
|
|
"""Inspect / mutate ``ctx`` (e.g. ``ctx.tools``) before it goes upstream."""
|
|
|
|
async def on_response(
|
|
self, ctx: TurnContext, response: dict[str, Any], call_model: CallModel
|
|
) -> dict[str, Any] | None:
|
|
"""Return a replacement response, or ``None`` to leave it unchanged.
|
|
|
|
May ``await call_model(messages)`` to re-drive the model (e.g. to resolve
|
|
an injected tool call), looping as needed before returning the final."""
|
|
|
|
|
|
_hooks: list[TurnHook] = []
|
|
|
|
|
|
def register_turn_hook(hook: TurnHook) -> None:
|
|
"""Register a hook. Called by an extension's ``install(app, config)``."""
|
|
_hooks.append(hook)
|
|
log.info("registered turn hook: %s", getattr(hook, "name", type(hook).__name__))
|
|
|
|
|
|
def registered_turn_hooks() -> list[TurnHook]:
|
|
return list(_hooks)
|
|
|
|
|
|
def clear_turn_hooks() -> None:
|
|
"""Test/reset helper."""
|
|
_hooks.clear()
|
|
|
|
|
|
def run_request_hooks(ctx: TurnContext) -> None:
|
|
"""Run every hook's ``on_request``. Inert when none are registered; never raises."""
|
|
for hook in _hooks:
|
|
fn = getattr(hook, "on_request", None)
|
|
if fn is None:
|
|
continue
|
|
try:
|
|
fn(ctx)
|
|
except Exception: # a hook must never break the proxy
|
|
log.exception("turn hook %r on_request failed", getattr(hook, "name", hook))
|
|
|
|
|
|
async def run_response_hooks(
|
|
ctx: TurnContext, response: dict[str, Any], call_model: CallModel
|
|
) -> dict[str, Any]:
|
|
"""Run every hook's ``on_response``, chaining any replacements.
|
|
|
|
Returns the (possibly replaced) response. Inert when no hooks are registered
|
|
(returns ``response`` unchanged); a failing hook is logged and skipped.
|
|
"""
|
|
current = response
|
|
for hook in _hooks:
|
|
fn = getattr(hook, "on_response", None)
|
|
if fn is None:
|
|
continue
|
|
try:
|
|
replacement = await fn(ctx, current, call_model)
|
|
except Exception:
|
|
log.exception("turn hook %r on_response failed", getattr(hook, "name", hook))
|
|
continue
|
|
if replacement is not None:
|
|
current = replacement
|
|
return current
|