Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:03:20 +08:00

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