Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions tee_gateway/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
COMPLETIONS_OPG_SESSION_MAX_SPEND,
FACILITATOR_URL,
OHTTP_OPG_SESSION_MAX_SPEND,
SETTLEMENT_POLL_INTERVAL_SECONDS,
SETTLEMENT_POLL_TIMEOUT_SECONDS,
UPTO_SESSION_IDLE_TIMEOUT_SECONDS,
UPTO_SESSION_MAX_TIMEOUT_SECONDS,
UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS,
)

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -267,7 +272,17 @@ def _init_payment_middleware(facilitator_url: str) -> None:
Called once from set_provider_keys() after the facilitator URL is known.
Swaps application.wsgi_app so all subsequent requests flow through it.
"""
facilitator = HTTPFacilitatorClientSync(FacilitatorConfig(url=facilitator_url))
# wait_for_settlement: the facilitator settles asynchronously (202 + job id).
# The reaper must confirm the on-chain result before marking a session settled,
# otherwise an expired/reverted settlement is silently dropped.
facilitator = HTTPFacilitatorClientSync(
FacilitatorConfig(
url=facilitator_url,
wait_for_settlement=True,
settlement_poll_interval=SETTLEMENT_POLL_INTERVAL_SECONDS,
settlement_poll_timeout=SETTLEMENT_POLL_TIMEOUT_SECONDS,
)
)
server = x402ResourceServerSync(facilitator) # type: ignore[arg-type]
store = SessionStore()

Expand All @@ -290,6 +305,7 @@ def _init_payment_middleware(facilitator_url: str) -> None:
},
),
network=BASE_MAINNET_NETWORK,
max_timeout_seconds=UPTO_SESSION_MAX_TIMEOUT_SECONDS,
),
],
extensions={
Expand All @@ -313,6 +329,7 @@ def _init_payment_middleware(facilitator_url: str) -> None:
},
),
network=BASE_MAINNET_NETWORK,
max_timeout_seconds=UPTO_SESSION_MAX_TIMEOUT_SECONDS,
),
],
extensions={
Expand All @@ -336,6 +353,7 @@ def _init_payment_middleware(facilitator_url: str) -> None:
},
),
network=BASE_MAINNET_NETWORK,
max_timeout_seconds=UPTO_SESSION_MAX_TIMEOUT_SECONDS,
),
],
extensions={
Expand All @@ -358,7 +376,8 @@ def _init_payment_middleware(facilitator_url: str) -> None:
server=server,
session_store=store,
cost_per_request=100000000000000, # static precheck/fallback estimate
session_idle_timeout=100,
session_idle_timeout=UPTO_SESSION_IDLE_TIMEOUT_SECONDS,
settlement_safety_margin=UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS,
session_cost_calculator=_session_cost_calculator,
)
logger.info(
Expand Down
40 changes: 36 additions & 4 deletions tee_gateway/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,53 @@
# by compute_session_cost() in pricing.py.
# ---------------------------------------------------------------------------

# /v1/chat/completions — maximum OPG spend per session (18 decimals: 100000000000000000 = 0.1 OPG).
# /v1/chat/completions — maximum OPG spend per session (18 decimals: 5000000000000000000 = 5 OPG).
# This is the upper-bound amount presented to the client during the x402 pre-check handshake.
# The x402 "upto" scheme allows the actual charge to be any value up to this cap;
# the real per-request cost is settled dynamically by compute_session_cost() in pricing.py
# based on actual token usage, so clients are never overcharged beyond what they consumed.
CHAT_COMPLETIONS_OPG_SESSION_MAX_SPEND: str = "100000000000000000"
CHAT_COMPLETIONS_OPG_SESSION_MAX_SPEND: str = "5000000000000000000"

# /v1/ohttp — maximum OPG spend per session (18 decimals: 5000000000000000000 = 5 OPG).
# OHTTP image-generation requests can be much more expensive than text chat,
# so the relay-paid encrypted endpoint needs a higher per-session cap.
OHTTP_OPG_SESSION_MAX_SPEND: str = "5000000000000000000"

# /v1/completions — maximum OPG spend per session (18 decimals: 100000000000000000 = 0.1 OPG).
# /v1/completions — maximum OPG spend per session (18 decimals: 5000000000000000000 = 5 OPG).
# This is the upper-bound amount presented to the client during the x402 pre-check handshake.
# The x402 "upto" scheme allows the actual charge to be any value up to this cap;
# the real per-request cost is settled dynamically by compute_session_cost() in pricing.py
# based on actual token usage, so clients are never overcharged beyond what they consumed.
COMPLETIONS_OPG_SESSION_MAX_SPEND: str = "100000000000000000"
COMPLETIONS_OPG_SESSION_MAX_SPEND: str = "5000000000000000000"

# ---------------------------------------------------------------------------
# x402 "upto" session settlement timing
# ---------------------------------------------------------------------------
# A draw-down ("upto") session reuses ONE signed Permit2 authorization whose
# on-chain deadline is fixed at signing time (session creation) and never
# refreshed. If the accumulated tab is settled after that deadline the on-chain
# ``settle`` reverts and the whole session's payment is silently lost. These
# knobs keep the settlement schedule safely inside the signed window.
#
# INVARIANT (must hold or long sessions drop payments):
# UPTO_SESSION_IDLE_TIMEOUT_SECONDS
# < UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS
# < UPTO_SESSION_MAX_TIMEOUT_SECONDS
# and the safety margin must exceed SETTLEMENT_POLL_TIMEOUT_SECONDS so a
# force-settled tab has time to confirm on-chain before the deadline.

# Validity window signed into the Permit2 authorization (deadline = creation + this).
# Advertised to clients as ``maxTimeoutSeconds`` in the 402 requirements.
UPTO_SESSION_MAX_TIMEOUT_SECONDS: int = 600

# Force-settle a session (and stop accepting new draw-downs against it) this many
# seconds before its authorization deadline, leaving room for on-chain confirmation.
UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS: int = 120

# Settle a session once it has been idle (no new requests) this long.
UPTO_SESSION_IDLE_TIMEOUT_SECONDS: int = 60

# Deferred settlement is asynchronous on the facilitator (202 + job id). Poll the
# job-status endpoint until the on-chain result is known instead of assuming success.
SETTLEMENT_POLL_INTERVAL_SECONDS: float = 2.0
SETTLEMENT_POLL_TIMEOUT_SECONDS: float = 100.0
60 changes: 60 additions & 0 deletions tee_gateway/test/test_settlement_timing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Guard tests for the x402 "upto" session settlement timing constants.

A draw-down ("upto") session reuses ONE signed Permit2 authorization whose
on-chain deadline is fixed at session creation (created_at + max_timeout) and
never refreshed. If the accumulated tab is settled after that deadline the
on-chain ``settle`` reverts and the whole session's payment is silently lost.

These tests lock in the ordering invariant between the settlement timing knobs
so a future edit to definitions.py can't silently reintroduce the drop.
"""

from tee_gateway.definitions import (
SETTLEMENT_POLL_TIMEOUT_SECONDS,
UPTO_SESSION_IDLE_TIMEOUT_SECONDS,
UPTO_SESSION_MAX_TIMEOUT_SECONDS,
UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS,
)


def test_timing_constants_are_positive():
"""Every timing knob must be a positive duration."""
assert UPTO_SESSION_IDLE_TIMEOUT_SECONDS > 0
assert UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS > 0
assert UPTO_SESSION_MAX_TIMEOUT_SECONDS > 0
assert SETTLEMENT_POLL_TIMEOUT_SECONDS > 0


def test_idle_timeout_below_safety_margin():
"""An idle session must settle (idle timeout) well before the force-settle
safety window, so normal traffic settles via idle and never races the
deadline."""
assert UPTO_SESSION_IDLE_TIMEOUT_SECONDS < UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS


def test_safety_margin_below_max_timeout():
"""The force-settle point (deadline - safety_margin) must be strictly before
the authorization deadline, leaving a positive window to settle in."""
assert UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS < UPTO_SESSION_MAX_TIMEOUT_SECONDS


def test_safety_margin_covers_poll_timeout():
"""Force-settling starts safety_margin seconds before the deadline, and the
reaper then polls the facilitator for up to SETTLEMENT_POLL_TIMEOUT_SECONDS
to confirm the on-chain result. The margin must exceed the poll timeout so a
force-settled tab can confirm before its authorization expires."""
assert UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS > SETTLEMENT_POLL_TIMEOUT_SECONDS


def test_full_ordering_invariant():
"""The complete invariant documented in definitions.py, in one assertion:

poll_timeout < safety_margin < max_timeout
idle_timeout < safety_margin
"""
assert (
UPTO_SESSION_IDLE_TIMEOUT_SECONDS
< UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS
< UPTO_SESSION_MAX_TIMEOUT_SECONDS
)
assert SETTLEMENT_POLL_TIMEOUT_SECONDS < UPTO_SETTLEMENT_SAFETY_MARGIN_SECONDS
Loading