From 8aa39fb953af3e1b68fbb75f87235cccb460d086 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:36:05 -0400 Subject: [PATCH 01/24] opg management --- src/opengradient/client/llm.py | 2 +- src/opengradient/client/opg_token.py | 198 +++++++++++++++++++++++---- 2 files changed, 171 insertions(+), 29 deletions(-) diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index 5a1e67a9..c68001de 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -15,7 +15,7 @@ from x402.mechanisms.evm.upto.register import register_upto_evm_client from ..types import TEE_LLM, StreamChoice, StreamChunk, StreamDelta, TextGenerationOutput, x402SettlementMode -from .opg_token import Permit2ApprovalResult, ensure_opg_approval +from .opg_token import Permit2ApprovalResult, approve_opg, ensure_opg_allowance, ensure_opg_approval from .tee_connection import RegistryTEEConnection, StaticTEEConnection, TEEConnectionInterface from .tee_registry import TEERegistry diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index af80783a..0d7f9600 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -1,13 +1,17 @@ """OPG token Permit2 approval utilities for x402 payments.""" -from dataclasses import dataclass +import logging import time +import warnings +from dataclasses import dataclass from typing import Optional from eth_account.account import LocalAccount from web3 import Web3 from x402.mechanisms.evm.constants import PERMIT2_ADDRESS +logger = logging.getLogger(__name__) + BASE_OPG_ADDRESS = "0x240b09731D96979f50B2C649C9CE10FcF9C7987F" BASE_SEPOLIA_RPC = "https://sepolia.base.org" APPROVAL_TX_TIMEOUT = 120 @@ -53,42 +57,32 @@ class Permit2ApprovalResult: tx_hash: Optional[str] = None -def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: - """Ensure the Permit2 allowance for OPG is at least ``opg_amount``. - - Checks the current Permit2 allowance for the wallet. If the allowance - is already >= the requested amount, returns immediately without sending - a transaction. Otherwise, sends an ERC-20 approve transaction. +def _send_approve_tx( + wallet_account: LocalAccount, + w3: Web3, + token, + owner: str, + spender: str, + amount_base: int, +) -> Permit2ApprovalResult: + """Send an ERC-20 approve transaction and wait for confirmation. Args: - wallet_account: The wallet account to check and approve from. - opg_amount: Minimum number of OPG tokens required (e.g. ``5.0`` - for 5 OPG). Converted to base units (18 decimals) internally. + wallet_account: The wallet to sign the transaction with. + w3: Web3 instance connected to the RPC. + token: The ERC-20 contract instance. + owner: Checksummed owner address. + spender: Checksummed spender (Permit2) address. + amount_base: The amount to approve in base units (18 decimals). Returns: - Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). + Permit2ApprovalResult with the before/after allowance and tx hash. Raises: - RuntimeError: If the approval transaction fails. + RuntimeError: If the transaction reverts or fails. """ - amount_base = int(opg_amount * 10**18) - - w3 = Web3(Web3.HTTPProvider(BASE_SEPOLIA_RPC)) - token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI) - owner = Web3.to_checksum_address(wallet_account.address) - spender = Web3.to_checksum_address(PERMIT2_ADDRESS) - allowance_before = token.functions.allowance(owner, spender).call() - # Only approve if the allowance is less than 50% of the requested amount - if allowance_before >= amount_base * 0.5: - return Permit2ApprovalResult( - allowance_before=allowance_before, - allowance_after=allowance_before, - ) - try: approve_fn = token.functions.approve(spender, amount_base) nonce = w3.eth.get_transaction_count(owner, "pending") @@ -133,3 +127,151 @@ def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Perm raise except Exception as e: raise RuntimeError(f"Failed to approve Permit2 for OPG: {e}") + + +def _get_web3_and_contract(): + """Create a Web3 instance and OPG token contract.""" + w3 = Web3(Web3.HTTPProvider(BASE_SEPOLIA_RPC)) + token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI) + spender = Web3.to_checksum_address(PERMIT2_ADDRESS) + return w3, token, spender + + +def approve_opg(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: + """Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. + + Idempotent: if the current allowance is already >= ``opg_amount``, no + transaction is sent. + + Best for one-off usage — scripts, notebooks, CLI tools:: + + result = approve_opg(wallet, 5.0) + + Args: + wallet_account: The wallet account to check and approve from. + opg_amount: Number of OPG tokens to approve (e.g. ``5.0`` for 5 OPG). + Converted to base units (18 decimals) internally. + + Returns: + Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + + Raises: + RuntimeError: If the approval transaction fails. + """ + amount_base = int(opg_amount * 10**18) + + w3, token, spender = _get_web3_and_contract() + owner = Web3.to_checksum_address(wallet_account.address) + + allowance_before = token.functions.allowance(owner, spender).call() + + if allowance_before >= amount_base: + logger.debug("Permit2 allowance already sufficient (%s >= %s), skipping approval", allowance_before, amount_base) + return Permit2ApprovalResult( + allowance_before=allowance_before, + allowance_after=allowance_before, + ) + + logger.info("Permit2 allowance insufficient (%s < %s), sending approval tx", allowance_before, amount_base) + return _send_approve_tx(wallet_account, w3, token, owner, spender, amount_base) + + +def ensure_opg_allowance( + wallet_account: LocalAccount, + min_allowance: float, + approve_amount: Optional[float] = None, +) -> Permit2ApprovalResult: + """Ensure the Permit2 allowance stays above a minimum threshold. + + Only sends an approval transaction when the current allowance drops + below ``min_allowance``. When approval is needed, approves + ``approve_amount`` (defaults to ``10 * min_allowance``) to create a + buffer that survives multiple service restarts without re-approving. + + Best for backend servers that call this on startup:: + + # On startup — only sends a tx when allowance < 5 OPG, + # then approves 100 OPG so subsequent restarts are free. + result = ensure_opg_allowance(wallet, min_allowance=5.0, approve_amount=100.0) + + Args: + wallet_account: The wallet account to check and approve from. + min_allowance: The minimum acceptable allowance in OPG. A + transaction is only sent when the current allowance is + strictly below this value. + approve_amount: The amount of OPG to approve when a transaction + is needed. Defaults to ``10 * min_allowance``. Must be + >= ``min_allowance``. + + Returns: + Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + + Raises: + ValueError: If ``approve_amount`` is less than ``min_allowance``. + RuntimeError: If the approval transaction fails. + """ + if approve_amount is None: + approve_amount = min_allowance * 10 + + if approve_amount < min_allowance: + raise ValueError(f"approve_amount ({approve_amount}) must be >= min_allowance ({min_allowance})") + + min_base = int(min_allowance * 10**18) + approve_base = int(approve_amount * 10**18) + + w3, token, spender = _get_web3_and_contract() + owner = Web3.to_checksum_address(wallet_account.address) + + allowance_before = token.functions.allowance(owner, spender).call() + + if allowance_before >= min_base: + logger.debug( + "Permit2 allowance above minimum threshold (%s >= %s), skipping approval", + allowance_before, + min_base, + ) + return Permit2ApprovalResult( + allowance_before=allowance_before, + allowance_after=allowance_before, + ) + + logger.info( + "Permit2 allowance below minimum threshold (%s < %s), approving %s base units", + allowance_before, + min_base, + approve_base, + ) + return _send_approve_tx(wallet_account, w3, token, owner, spender, approve_base) + + +def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: + """Ensure the Permit2 allowance for OPG is at least ``opg_amount``. + + .. deprecated:: + Use ``approve_opg`` for one-off approvals or + ``ensure_opg_allowance`` for server-startup usage. + + Args: + wallet_account: The wallet account to check and approve from. + opg_amount: Minimum number of OPG tokens required (e.g. ``5.0`` + for 5 OPG). Converted to base units (18 decimals) internally. + + Returns: + Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + + Raises: + RuntimeError: If the approval transaction fails. + """ + warnings.warn( + "ensure_opg_approval is deprecated. Use approve_opg for one-off approvals " + "or ensure_opg_allowance for server-startup usage.", + DeprecationWarning, + stacklevel=2, + ) + return approve_opg(wallet_account, opg_amount) From a3a53e77c570860c2fe3b26c9318119570a4357a Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:39:18 -0400 Subject: [PATCH 02/24] tests --- README.md | 2 +- examples/langchain_react_agent.py | 2 +- examples/llm_chat.py | 2 +- examples/llm_chat_streaming.py | 2 +- examples/llm_tool_calling.py | 2 +- integrationtest/llm/test_llm_chat.py | 2 +- src/opengradient/__init__.py | 2 +- src/opengradient/client/__init__.py | 2 +- src/opengradient/client/llm.py | 66 ++++++- src/opengradient/client/opg_token.py | 3 +- stresstest/llm.py | 2 +- tests/opg_token_test.py | 262 +++++++++++++++++---------- tests/tee_connection_test.py | 16 +- 13 files changed, 253 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index bf990bc2..b5e9e937 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ hub = og.ModelHub(email="you@example.com", password="...") Before making LLM requests, your wallet must approve OPG token spending via the [Permit2](https://github.com/Uniswap/permit2) protocol. Call this once (it's idempotent — no transaction is sent if the allowance already covers the requested amount): ```python -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) ``` See [Payment Settlement](#payment-settlement) for details on settlement modes. diff --git a/examples/langchain_react_agent.py b/examples/langchain_react_agent.py index 9ca5faae..36105505 100644 --- a/examples/langchain_react_agent.py +++ b/examples/langchain_react_agent.py @@ -19,7 +19,7 @@ # One-time Permit2 approval for OPG spending (idempotent) llm_client = og.LLM(private_key=private_key) -llm_client.ensure_opg_approval(opg_amount=5) +llm_client.approve_opg(opg_amount=5) # Create the OpenGradient LangChain adapter llm = og.agents.langchain_adapter( diff --git a/examples/llm_chat.py b/examples/llm_chat.py index a2c599cb..8cf4fb7e 100644 --- a/examples/llm_chat.py +++ b/examples/llm_chat.py @@ -10,7 +10,7 @@ async def main(): llm = og.LLM(private_key=os.environ.get("OG_PRIVATE_KEY")) - llm.ensure_opg_approval(opg_amount=0.1) + llm.approve_opg(opg_amount=0.1) messages = [ {"role": "user", "content": "What is the capital of France?"}, diff --git a/examples/llm_chat_streaming.py b/examples/llm_chat_streaming.py index 17db4774..1078fbc5 100644 --- a/examples/llm_chat_streaming.py +++ b/examples/llm_chat_streaming.py @@ -6,7 +6,7 @@ async def main(): llm = og.LLM(private_key=os.environ.get("OG_PRIVATE_KEY")) - llm.ensure_opg_approval(opg_amount=0.1) + llm.approve_opg(opg_amount=0.1) messages = [ {"role": "user", "content": "What is Python?"}, diff --git a/examples/llm_tool_calling.py b/examples/llm_tool_calling.py index 51fbbbb3..c0f0ddfe 100644 --- a/examples/llm_tool_calling.py +++ b/examples/llm_tool_calling.py @@ -51,7 +51,7 @@ async def main(): ] # One-time Permit2 approval for OPG spending (idempotent) - llm.ensure_opg_approval(opg_amount=0.1) + llm.approve_opg(opg_amount=0.1) print("Testing Gemini tool calls...") print(f"Model: {og.TEE_LLM.GEMINI_2_5_FLASH_LITE}") diff --git a/integrationtest/llm/test_llm_chat.py b/integrationtest/llm/test_llm_chat.py index 632436ac..bd666812 100644 --- a/integrationtest/llm/test_llm_chat.py +++ b/integrationtest/llm/test_llm_chat.py @@ -99,7 +99,7 @@ def llm_client(): print("Account funded with ETH and OPG") llm = og.LLM(private_key=account.key.hex()) - llm.ensure_opg_approval(opg_amount=OPG_FUND_AMOUNT) + llm.approve_opg(opg_amount=OPG_FUND_AMOUNT) print("Permit2 approval complete") # Wait for the approval to propagate on-chain diff --git a/src/opengradient/__init__.py b/src/opengradient/__init__.py index a849d220..b7e94cde 100644 --- a/src/opengradient/__init__.py +++ b/src/opengradient/__init__.py @@ -26,7 +26,7 @@ llm = og.LLM(private_key="0x...") # One-time OPG token approval (idempotent -- skips if allowance is sufficient) -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) # Chat with an LLM (TEE-verified) response = asyncio.run(llm.chat( diff --git a/src/opengradient/client/__init__.py b/src/opengradient/client/__init__.py index 8e1ebbe5..b95480a1 100644 --- a/src/opengradient/client/__init__.py +++ b/src/opengradient/client/__init__.py @@ -17,7 +17,7 @@ # LLM inference (Base Sepolia OPG tokens) llm = og.LLM(private_key="0x...") -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) result = await llm.chat(model=og.TEE_LLM.CLAUDE_HAIKU_4_5, messages=[...]) # On-chain model inference (OpenGradient testnet gas tokens) diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index c68001de..59ddb99e 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -184,12 +184,72 @@ async def _call_with_tee_retry( # ── Public API ────────────────────────────────────────────────────── + def approve_opg(self, opg_amount: float) -> Permit2ApprovalResult: + """Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. + + Idempotent: if the current allowance is already >= ``opg_amount``, no + transaction is sent. Best for one-off usage — scripts, notebooks, CLI tools. + + Args: + opg_amount: Number of OPG tokens to approve (e.g. ``0.1`` + for 0.1 OPG). Must be at least 0.1 OPG. + + Returns: + Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + + Raises: + ValueError: If the OPG amount is less than 0.1. + RuntimeError: If the approval transaction fails. + """ + if opg_amount < 0.1: + raise ValueError("OPG amount must be at least 0.1.") + return approve_opg(self._wallet_account, opg_amount) + + def ensure_opg_allowance( + self, + min_allowance: float, + approve_amount: Optional[float] = None, + ) -> Permit2ApprovalResult: + """Ensure the Permit2 allowance stays above a minimum threshold. + + Only sends a transaction when the current allowance drops below + ``min_allowance``. When approval is needed, approves ``approve_amount`` + (defaults to ``10 * min_allowance``) to create a buffer that survives + multiple service restarts without re-approving. + + Best for backend servers that call this on startup:: + + llm.ensure_opg_allowance(min_allowance=5.0, approve_amount=100.0) + + Args: + min_allowance: The minimum acceptable allowance in OPG. Must be + at least 0.1 OPG. + approve_amount: The amount of OPG to approve when a transaction + is needed. Defaults to ``10 * min_allowance``. Must be + >= ``min_allowance``. + + Returns: + Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + + Raises: + ValueError: If ``min_allowance`` is less than 0.1 or + ``approve_amount`` is less than ``min_allowance``. + RuntimeError: If the approval transaction fails. + """ + if min_allowance < 0.1: + raise ValueError("min_allowance must be at least 0.1.") + return ensure_opg_allowance(self._wallet_account, min_allowance, approve_amount) + def ensure_opg_approval(self, opg_amount: float) -> Permit2ApprovalResult: """Ensure the Permit2 allowance for OPG is at least ``opg_amount``. - Checks the current Permit2 allowance for the wallet. If the allowance - is already >= the requested amount, returns immediately without sending - a transaction. Otherwise, sends an ERC-20 approve transaction. + .. deprecated:: + Use ``approve_opg`` for one-off approvals or + ``ensure_opg_allowance`` for server-startup usage. Args: opg_amount: Minimum number of OPG tokens required (e.g. ``0.1`` diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 0d7f9600..466bbc83 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -269,8 +269,7 @@ def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Perm RuntimeError: If the approval transaction fails. """ warnings.warn( - "ensure_opg_approval is deprecated. Use approve_opg for one-off approvals " - "or ensure_opg_allowance for server-startup usage.", + "ensure_opg_approval is deprecated. Use approve_opg for one-off approvals or ensure_opg_allowance for server-startup usage.", DeprecationWarning, stacklevel=2, ) diff --git a/stresstest/llm.py b/stresstest/llm.py index f9652c19..d84f3a17 100644 --- a/stresstest/llm.py +++ b/stresstest/llm.py @@ -13,7 +13,7 @@ async def main(private_key: str): llm = og.LLM(private_key=private_key) - llm.ensure_opg_approval(opg_amount=0.1) + llm.approve_opg(opg_amount=0.1) async def run_prompt(prompt: str): messages = [{"role": "user", "content": prompt}] diff --git a/tests/opg_token_test.py b/tests/opg_token_test.py index 0e44a072..b909a6e1 100644 --- a/tests/opg_token_test.py +++ b/tests/opg_token_test.py @@ -1,10 +1,12 @@ +import warnings from types import SimpleNamespace from unittest.mock import MagicMock import pytest from opengradient.client.opg_token import ( - Permit2ApprovalResult, + approve_opg, + ensure_opg_allowance, ensure_opg_approval, ) @@ -44,7 +46,35 @@ def _setup_allowance(mock_w3, allowance_value): return contract -class TestEnsureOpgApprovalSkips: +def _setup_approval_mocks(mock_web3, mock_wallet, contract): + """Set up common mocks for approval transactions.""" + approve_fn = MagicMock() + contract.functions.approve.return_value = approve_fn + approve_fn.estimate_gas.return_value = 50_000 + approve_fn.build_transaction.return_value = {"mock": "tx"} + + mock_web3.eth.get_transaction_count.return_value = 7 + mock_web3.eth.gas_price = 1_000_000_000 + mock_web3.eth.chain_id = 84532 + + signed = MagicMock() + signed.raw_transaction = b"\x00" + mock_wallet.sign_transaction.return_value = signed + + tx_hash = MagicMock() + tx_hash.hex.return_value = "0xabc123" + mock_web3.eth.send_raw_transaction.return_value = tx_hash + + receipt = SimpleNamespace(status=1) + mock_web3.eth.wait_for_transaction_receipt.return_value = receipt + + return approve_fn, tx_hash + + +# ── approve_opg tests ─────────────────────────────────────────────── + + +class TestApproveOpgSkips: """Cases where the existing allowance is sufficient.""" def test_exact_allowance_skips_tx(self, mock_wallet, mock_web3): @@ -53,7 +83,7 @@ def test_exact_allowance_skips_tx(self, mock_wallet, mock_web3): amount_base = int(amount * 10**18) _setup_allowance(mock_web3, amount_base) - result = ensure_opg_approval(mock_wallet, amount) + result = approve_opg(mock_wallet, amount) assert result.allowance_before == amount_base assert result.allowance_after == amount_base @@ -64,7 +94,7 @@ def test_excess_allowance_skips_tx(self, mock_wallet, mock_web3): amount_base = int(5.0 * 10**18) _setup_allowance(mock_web3, amount_base * 2) - result = ensure_opg_approval(mock_wallet, 5.0) + result = approve_opg(mock_wallet, 5.0) assert result.allowance_before == amount_base * 2 assert result.tx_hash is None @@ -73,12 +103,12 @@ def test_zero_amount_with_zero_allowance_skips(self, mock_wallet, mock_web3): """Requesting 0 tokens with 0 allowance should skip (0 >= 0).""" _setup_allowance(mock_web3, 0) - result = ensure_opg_approval(mock_wallet, 0.0) + result = approve_opg(mock_wallet, 0.0) assert result.tx_hash is None -class TestEnsureOpgApprovalSendsTx: +class TestApproveOpgSendsTx: """Cases where allowance is insufficient and a transaction is sent.""" def test_approval_sent_when_allowance_insufficient(self, mock_wallet, mock_web3): @@ -86,32 +116,13 @@ def test_approval_sent_when_allowance_insufficient(self, mock_wallet, mock_web3) amount = 5.0 amount_base = int(amount * 10**18) contract = _setup_allowance(mock_web3, 0) + approve_fn, _ = _setup_approval_mocks(mock_web3, mock_wallet, contract) - # Set up the approval transaction mocks - approve_fn = MagicMock() - contract.functions.approve.return_value = approve_fn - approve_fn.estimate_gas.return_value = 50_000 - approve_fn.build_transaction.return_value = {"mock": "tx"} - - mock_web3.eth.get_transaction_count.return_value = 7 - mock_web3.eth.gas_price = 1_000_000_000 - mock_web3.eth.chain_id = 84532 - - signed = MagicMock() - signed.raw_transaction = b"\x00" - mock_wallet.sign_transaction.return_value = signed - - tx_hash = MagicMock() - tx_hash.hex.return_value = "0xabc123" - mock_web3.eth.send_raw_transaction.return_value = tx_hash - - receipt = SimpleNamespace(status=1) - mock_web3.eth.wait_for_transaction_receipt.return_value = receipt - - # After approval the allowance call returns the new value - contract.functions.allowance.return_value.call.side_effect = [0, amount_base] + # Side effects: 1st call in approve_opg check, 2nd in _send_approve_tx before, + # 3rd in post-tx poll + contract.functions.allowance.return_value.call.side_effect = [0, 0, amount_base] - result = ensure_opg_approval(mock_wallet, amount) + result = approve_opg(mock_wallet, amount) assert result.allowance_before == 0 assert result.allowance_after == amount_base @@ -125,27 +136,11 @@ def test_approval_sent_when_allowance_insufficient(self, mock_wallet, mock_web3) def test_gas_estimate_has_20_percent_buffer(self, mock_wallet, mock_web3): """Gas limit should be estimatedGas * 1.2.""" contract = _setup_allowance(mock_web3, 0) + approve_fn, _ = _setup_approval_mocks(mock_web3, mock_wallet, contract) - approve_fn = MagicMock() - contract.functions.approve.return_value = approve_fn - approve_fn.estimate_gas.return_value = 50_000 - - mock_web3.eth.get_transaction_count.return_value = 0 - mock_web3.eth.gas_price = 1_000_000_000 - mock_web3.eth.chain_id = 84532 - - signed = MagicMock() - signed.raw_transaction = b"\x00" - mock_wallet.sign_transaction.return_value = signed + contract.functions.allowance.return_value.call.side_effect = [0, 0, int(1 * 10**18)] - tx_hash = MagicMock() - tx_hash.hex.return_value = "0x0" - mock_web3.eth.send_raw_transaction.return_value = tx_hash - mock_web3.eth.wait_for_transaction_receipt.return_value = SimpleNamespace(status=1) - - contract.functions.allowance.return_value.call.side_effect = [0, int(1 * 10**18)] - - ensure_opg_approval(mock_wallet, 1.0) + approve_opg(mock_wallet, 1.0) tx_dict = approve_fn.build_transaction.call_args[0][0] assert tx_dict["gas"] == int(50_000 * 1.2) @@ -154,36 +149,18 @@ def test_waits_for_allowance_update_after_receipt(self, mock_wallet, mock_web3, """After a successful receipt, poll allowance until the updated value is visible.""" monkeypatch.setattr("opengradient.client.opg_token.ALLOWANCE_POLL_INTERVAL", 0) contract = _setup_allowance(mock_web3, 0) - - approve_fn = MagicMock() - contract.functions.approve.return_value = approve_fn - approve_fn.estimate_gas.return_value = 50_000 - approve_fn.build_transaction.return_value = {"mock": "tx"} - - mock_web3.eth.get_transaction_count.return_value = 0 - mock_web3.eth.gas_price = 1_000_000_000 - mock_web3.eth.chain_id = 84532 - - signed = MagicMock() - signed.raw_transaction = b"\x00" - mock_wallet.sign_transaction.return_value = signed - - tx_hash = MagicMock() - tx_hash.hex.return_value = "0xconfirmed" - mock_web3.eth.send_raw_transaction.return_value = tx_hash - mock_web3.eth.wait_for_transaction_receipt.return_value = SimpleNamespace(status=1, blockNumber=100) + _setup_approval_mocks(mock_web3, mock_wallet, contract) amount_base = int(1.0 * 10**18) - contract.functions.allowance.return_value.call.side_effect = [0, 0, amount_base] + contract.functions.allowance.return_value.call.side_effect = [0, 0, 0, amount_base] - result = ensure_opg_approval(mock_wallet, 1.0) + result = approve_opg(mock_wallet, 1.0) assert result.allowance_before == 0 assert result.allowance_after == amount_base - assert result.tx_hash == "0xconfirmed" -class TestEnsureOpgApprovalErrors: +class TestApproveOpgErrors: """Error handling paths.""" def test_reverted_tx_raises(self, mock_wallet, mock_web3): @@ -208,7 +185,7 @@ def test_reverted_tx_raises(self, mock_wallet, mock_web3): mock_web3.eth.wait_for_transaction_receipt.return_value = SimpleNamespace(status=0) with pytest.raises(RuntimeError, match="reverted"): - ensure_opg_approval(mock_wallet, 5.0) + approve_opg(mock_wallet, 5.0) def test_generic_exception_wrapped(self, mock_wallet, mock_web3): """Non-RuntimeError exceptions are wrapped in RuntimeError.""" @@ -221,9 +198,9 @@ def test_generic_exception_wrapped(self, mock_wallet, mock_web3): mock_web3.eth.get_transaction_count.return_value = 0 with pytest.raises(RuntimeError, match="Failed to approve Permit2 for OPG"): - ensure_opg_approval(mock_wallet, 5.0) + approve_opg(mock_wallet, 5.0) - def test_opengradient_error_not_double_wrapped(self, mock_wallet, mock_web3): + def test_runtime_error_not_double_wrapped(self, mock_wallet, mock_web3): """RuntimeError raised inside the try block should propagate as-is.""" contract = _setup_allowance(mock_web3, 0) @@ -245,12 +222,127 @@ def test_opengradient_error_not_double_wrapped(self, mock_wallet, mock_web3): mock_web3.eth.wait_for_transaction_receipt.return_value = SimpleNamespace(status=0) with pytest.raises(RuntimeError, match="reverted") as exc_info: - ensure_opg_approval(mock_wallet, 5.0) + approve_opg(mock_wallet, 5.0) - # Should be the original error, not wrapped assert "Failed to approve" not in str(exc_info.value) +# ── ensure_opg_allowance tests ────────────────────────────────────── + + +class TestEnsureOpgAllowanceSkips: + """Cases where the existing allowance exceeds the minimum threshold.""" + + def test_above_minimum_skips(self, mock_wallet, mock_web3): + """When allowance >= min_allowance, no transaction is sent.""" + min_base = int(5.0 * 10**18) + _setup_allowance(mock_web3, min_base * 2) + + result = ensure_opg_allowance(mock_wallet, min_allowance=5.0) + + assert result.tx_hash is None + assert result.allowance_before == min_base * 2 + + def test_exact_minimum_skips(self, mock_wallet, mock_web3): + """When allowance == min_allowance, no transaction is sent.""" + min_base = int(5.0 * 10**18) + _setup_allowance(mock_web3, min_base) + + result = ensure_opg_allowance(mock_wallet, min_allowance=5.0) + + assert result.tx_hash is None + + +class TestEnsureOpgAllowanceSendsTx: + """Cases where allowance is below the minimum and a tx is needed.""" + + def test_approves_default_10x_amount(self, mock_wallet, mock_web3): + """When no approve_amount given, approves 10x min_allowance.""" + contract = _setup_allowance(mock_web3, 0) + _setup_approval_mocks(mock_web3, mock_wallet, contract) + + approve_base = int(50.0 * 10**18) # 10x of 5.0 + contract.functions.allowance.return_value.call.side_effect = [0, 0, approve_base] + + result = ensure_opg_allowance(mock_wallet, min_allowance=5.0) + + assert result.tx_hash == "0xabc123" + # Verify approve was called with 10x amount + args = contract.functions.approve.call_args[0] + assert args[1] == approve_base + + def test_approves_custom_amount(self, mock_wallet, mock_web3): + """When approve_amount is specified, uses that exact amount.""" + contract = _setup_allowance(mock_web3, 0) + _setup_approval_mocks(mock_web3, mock_wallet, contract) + + approve_base = int(100.0 * 10**18) + contract.functions.allowance.return_value.call.side_effect = [0, 0, approve_base] + + result = ensure_opg_allowance(mock_wallet, min_allowance=5.0, approve_amount=100.0) + + assert result.tx_hash == "0xabc123" + args = contract.functions.approve.call_args[0] + assert args[1] == approve_base + + def test_no_tx_on_restart_when_above_min(self, mock_wallet, mock_web3): + """Simulates server restart: allowance is above min but below approve_amount.""" + # After first approval of 100 OPG, some was consumed leaving 60 OPG. + # min_allowance=5.0 so no tx should be sent. + remaining = int(60.0 * 10**18) + _setup_allowance(mock_web3, remaining) + + result = ensure_opg_allowance(mock_wallet, min_allowance=5.0, approve_amount=100.0) + + assert result.tx_hash is None + assert result.allowance_before == remaining + + +class TestEnsureOpgAllowanceValidation: + """Input validation.""" + + def test_approve_amount_less_than_min_raises(self, mock_wallet, mock_web3): + """approve_amount < min_allowance should raise ValueError.""" + _setup_allowance(mock_web3, 0) + + with pytest.raises(ValueError, match="approve_amount.*must be >= min_allowance"): + ensure_opg_allowance(mock_wallet, min_allowance=10.0, approve_amount=5.0) + + +# ── ensure_opg_approval (deprecated) tests ────────────────────────── + + +class TestEnsureOpgApprovalDeprecated: + """The old function still works but emits a deprecation warning.""" + + def test_emits_deprecation_warning(self, mock_wallet, mock_web3): + """ensure_opg_approval should emit a DeprecationWarning.""" + _setup_allowance(mock_web3, int(10 * 10**18)) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ensure_opg_approval(mock_wallet, 5.0) + + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "deprecated" in str(w[0].message).lower() + + def test_delegates_to_approve_opg(self, mock_wallet, mock_web3): + """ensure_opg_approval should produce the same result as approve_opg.""" + amount_base = int(5.0 * 10**18) + _setup_allowance(mock_web3, amount_base) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = ensure_opg_approval(mock_wallet, 5.0) + + assert result.allowance_before == amount_base + assert result.tx_hash is None + + +# ── Amount conversion tests ───────────────────────────────────────── + + class TestAmountConversion: """Verify float-to-base-unit conversion.""" @@ -259,7 +351,7 @@ def test_fractional_amount(self, mock_wallet, mock_web3): expected_base = int(0.5 * 10**18) _setup_allowance(mock_web3, expected_base) - result = ensure_opg_approval(mock_wallet, 0.5) + result = approve_opg(mock_wallet, 0.5) assert result.allowance_before == expected_base assert result.tx_hash is None @@ -269,21 +361,7 @@ def test_large_amount(self, mock_wallet, mock_web3): expected_base = int(1000.0 * 10**18) _setup_allowance(mock_web3, expected_base) - result = ensure_opg_approval(mock_wallet, 1000.0) + result = approve_opg(mock_wallet, 1000.0) assert result.allowance_before == expected_base assert result.tx_hash is None - - -class TestPermit2ApprovalResult: - """Dataclass behavior.""" - - def test_default_tx_hash_is_none(self): - result = Permit2ApprovalResult(allowance_before=100, allowance_after=200) - assert result.tx_hash is None - - def test_fields(self): - result = Permit2ApprovalResult(allowance_before=0, allowance_after=500, tx_hash="0xabc") - assert result.allowance_before == 0 - assert result.allowance_after == 500 - assert result.tx_hash == "0xabc" diff --git a/tests/tee_connection_test.py b/tests/tee_connection_test.py index 2d54c771..3d5379fe 100644 --- a/tests/tee_connection_test.py +++ b/tests/tee_connection_test.py @@ -229,12 +229,16 @@ def slow_connect(self): mock_reg = _mock_registry_with_tee() conn = _make_registry_connection(registry=mock_reg) - with patch.object(RegistryTEEConnection, "_connect", slow_connect), patch( - "src.opengradient.client.tee_connection.build_ssl_context_from_der", - return_value=MagicMock(spec=ssl.SSLContext), - ), patch( - "src.opengradient.client.tee_connection.x402HttpxClient", - side_effect=FakeHTTPClient, + with ( + patch.object(RegistryTEEConnection, "_connect", slow_connect), + patch( + "src.opengradient.client.tee_connection.build_ssl_context_from_der", + return_value=MagicMock(spec=ssl.SSLContext), + ), + patch( + "src.opengradient.client.tee_connection.x402HttpxClient", + side_effect=FakeHTTPClient, + ), ): await asyncio.gather(conn.reconnect(), conn.reconnect()) From b33c627da453d158ea90f7a6630f710852938bfb Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:41:15 -0400 Subject: [PATCH 03/24] docs --- docs/CLAUDE_SDK_USERS.md | 2 +- docs/opengradient/client/index.md | 4 +- docs/opengradient/client/llm.md | 96 +++++++++++++++++++- docs/opengradient/client/model_hub.md | 9 +- docs/opengradient/client/opg_token.md | 108 ++++++++++++++++++++--- docs/opengradient/index.md | 2 +- src/opengradient/client/__init__.py | 2 +- tests/llm_test.py | 4 +- tutorials/01-verifiable-ai-agent.md | 6 +- tutorials/02-streaming-multi-provider.md | 6 +- tutorials/03-verified-tool-calling.md | 6 +- 11 files changed, 210 insertions(+), 35 deletions(-) diff --git a/docs/CLAUDE_SDK_USERS.md b/docs/CLAUDE_SDK_USERS.md index 22a5e4c8..c7e14690 100644 --- a/docs/CLAUDE_SDK_USERS.md +++ b/docs/CLAUDE_SDK_USERS.md @@ -22,7 +22,7 @@ import os llm = og.LLM(private_key=os.environ["OG_PRIVATE_KEY"]) # One-time OPG token approval (idempotent — skips if allowance already sufficient) -llm.ensure_opg_approval(opg_amount=0.1) +llm.approve_opg(opg_amount=0.1) # LLM Chat (TEE-verified with x402 payments, async) result = await llm.chat( diff --git a/docs/opengradient/client/index.md b/docs/opengradient/client/index.md index f7ee48ad..cc7395c8 100644 --- a/docs/opengradient/client/index.md +++ b/docs/opengradient/client/index.md @@ -14,7 +14,7 @@ OpenGradient Client -- service modules for the SDK. - **[model_hub](./model_hub)** -- Model repository management: create, version, and upload ML models - **[alpha](./alpha)** -- Alpha Testnet features: on-chain ONNX model inference (VANILLA, TEE, ZKML modes), workflow deployment, and scheduled ML model execution (OpenGradient testnet gas tokens) - **[twins](./twins)** -- Digital twins chat via OpenGradient verifiable inference -- **`opengradient.client.opg_token`** -- OPG token Permit2 approval utilities for x402 payments +- **[opg_token](./opg_token)** -- OPG token Permit2 approval utilities for x402 payments - **`opengradient.client.tee_registry`** -- TEE registry client for verified endpoints and TLS certificates ## Usage @@ -24,7 +24,7 @@ import opengradient as og # LLM inference (Base Sepolia OPG tokens) llm = og.LLM(private_key="0x...") -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) result = await llm.chat(model=og.TEE_LLM.CLAUDE_HAIKU_4_5, messages=[...]) # On-chain model inference (OpenGradient testnet gas tokens) diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index edf8230e..77588c36 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -60,6 +60,40 @@ resolves TEEs from the on-chain registry. --- +#### `approve_opg()` + +```python +def approve_opg(self, opg_amount: float) ‑> `Permit2ApprovalResult` +``` +Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. + +Idempotent: if the current allowance is already >= ``opg_amount``, no +transaction is sent. Best for one-off usage — scripts, notebooks, CLI tools. + +**Arguments** + +* **`opg_amount`**: Number of OPG tokens to approve (e.g. ``0.1`` + for 0.1 OPG). Must be at least 0.1 OPG. + +**Returns** + +Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + +**`Permit2ApprovalResult` fields:** + +* **`allowance_before`**: The Permit2 allowance before the method ran. +* **`allowance_after`**: The Permit2 allowance after the method ran. +* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. + +**Raises** + +* **`ValueError`**: If the OPG amount is less than 0.1. +* **`RuntimeError`**: If the approval transaction fails. + +--- + #### `chat()` ```python @@ -190,16 +224,64 @@ TextGenerationOutput: Generated text results including: --- +#### `ensure_opg_allowance()` + +```python +def ensure_opg_allowance( + self, + min_allowance: float, + approve_amount: Optional[float] = None +) ‑> `Permit2ApprovalResult` +``` +Ensure the Permit2 allowance stays above a minimum threshold. + +Only sends a transaction when the current allowance drops below +``min_allowance``. When approval is needed, approves ``approve_amount`` +(defaults to ``10 * min_allowance``) to create a buffer that survives +multiple service restarts without re-approving. + +Best for backend servers that call this on startup:: + + llm.ensure_opg_allowance(min_allowance=5.0, approve_amount=100.0) + +**Arguments** + +* **`min_allowance`**: The minimum acceptable allowance in OPG. Must be + at least 0.1 OPG. +* **`approve_amount`**: The amount of OPG to approve when a transaction + is needed. Defaults to ``10 * min_allowance``. Must be + >= ``min_allowance``. + +**Returns** + +Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + +**`Permit2ApprovalResult` fields:** + +* **`allowance_before`**: The Permit2 allowance before the method ran. +* **`allowance_after`**: The Permit2 allowance after the method ran. +* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. + +**Raises** + +* **`ValueError`**: If ``min_allowance`` is less than 0.1 or + ``approve_amount`` is less than ``min_allowance``. +* **`RuntimeError`**: If the approval transaction fails. + +--- + #### `ensure_opg_approval()` ```python -def ensure_opg_approval(self, opg_amount: float) ‑> [Permit2ApprovalResult](./opg_token) +def ensure_opg_approval(self, opg_amount: float) ‑> `Permit2ApprovalResult` ``` Ensure the Permit2 allowance for OPG is at least ``opg_amount``. -Checks the current Permit2 allowance for the wallet. If the allowance -is already >= the requested amount, returns immediately without sending -a transaction. Otherwise, sends an ERC-20 approve transaction. +.. deprecated:: + Use ``approve_opg`` for one-off approvals or + ``ensure_opg_allowance`` for server-startup usage. **Arguments** @@ -212,6 +294,12 @@ Permit2ApprovalResult: Contains ``allowance_before``, ``allowance_after``, and ``tx_hash`` (None when no approval was needed). +**`Permit2ApprovalResult` fields:** + +* **`allowance_before`**: The Permit2 allowance before the method ran. +* **`allowance_after`**: The Permit2 allowance after the method ran. +* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. + **Raises** * **`ValueError`**: If the OPG amount is less than 0.1. diff --git a/docs/opengradient/client/model_hub.md b/docs/opengradient/client/model_hub.md index e3d95916..795de4cf 100644 --- a/docs/opengradient/client/model_hub.md +++ b/docs/opengradient/client/model_hub.md @@ -43,15 +43,16 @@ Create a new model with the given model_name and model_desc, and a specified ver * **`model_name (str)`**: The name of the model. * **`model_desc (str)`**: The description of the model. -* **`version (str)`**: The version identifier (default is "1.00"). +* **`version (str)`**: A label used in the initial version notes (default is "1.00"). +* **`Note`**: the actual version string is assigned by the server. **Returns** -dict: The server response containing model details. +ModelRepository: Object containing the model name and server-assigned version string. **Raises** -* **`CreateModelError`**: If the model creation fails. +* **`RuntimeError`**: If the model creation fails. --- @@ -125,7 +126,7 @@ Upload a model file to the server. **Returns** -dict: The processed result. +FileUploadResult: The processed result. **Raises** diff --git a/docs/opengradient/client/opg_token.md b/docs/opengradient/client/opg_token.md index 4e507301..360ee4ea 100644 --- a/docs/opengradient/client/opg_token.md +++ b/docs/opengradient/client/opg_token.md @@ -1,5 +1,5 @@ --- -outline: [2,3] +outline: [2,4] --- [opengradient](../index) / [client](./index) / opg_token @@ -12,6 +12,98 @@ OPG token Permit2 approval utilities for x402 payments. --- +### `approve_opg()` + +```python +def approve_opg( + wallet_account: `LocalAccount`, + opg_amount: float +) ‑> `Permit2ApprovalResult` +``` +Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. + +Idempotent: if the current allowance is already >= ``opg_amount``, no +transaction is sent. + +Best for one-off usage — scripts, notebooks, CLI tools:: + + result = approve_opg(wallet, 5.0) + +**Arguments** + +* **`wallet_account`**: The wallet account to check and approve from. +* **`opg_amount`**: Number of OPG tokens to approve (e.g. ``5.0`` for 5 OPG). + Converted to base units (18 decimals) internally. + +**Returns** + +Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + +**`Permit2ApprovalResult` fields:** + +* **`allowance_before`**: The Permit2 allowance before the method ran. +* **`allowance_after`**: The Permit2 allowance after the method ran. +* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. + +**Raises** + +* **`RuntimeError`**: If the approval transaction fails. + +--- + +### `ensure_opg_allowance()` + +```python +def ensure_opg_allowance( + wallet_account: `LocalAccount`, + min_allowance: float, + approve_amount: Optional[float] = None +) ‑> `Permit2ApprovalResult` +``` +Ensure the Permit2 allowance stays above a minimum threshold. + +Only sends an approval transaction when the current allowance drops +below ``min_allowance``. When approval is needed, approves +``approve_amount`` (defaults to ``10 * min_allowance``) to create a +buffer that survives multiple service restarts without re-approving. + +Best for backend servers that call this on startup:: + + # On startup — only sends a tx when allowance < 5 OPG, + # then approves 100 OPG so subsequent restarts are free. + result = ensure_opg_allowance(wallet, min_allowance=5.0, approve_amount=100.0) + +**Arguments** + +* **`wallet_account`**: The wallet account to check and approve from. +* **`min_allowance`**: The minimum acceptable allowance in OPG. A + transaction is only sent when the current allowance is + strictly below this value. +* **`approve_amount`**: The amount of OPG to approve when a transaction + is needed. Defaults to ``10 * min_allowance``. Must be + >= ``min_allowance``. + +**Returns** + +Permit2ApprovalResult: Contains ``allowance_before``, + ``allowance_after``, and ``tx_hash`` (None when no approval + was needed). + +**`Permit2ApprovalResult` fields:** + +* **`allowance_before`**: The Permit2 allowance before the method ran. +* **`allowance_after`**: The Permit2 allowance after the method ran. +* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. + +**Raises** + +* **`ValueError`**: If ``approve_amount`` is less than ``min_allowance``. +* **`RuntimeError`**: If the approval transaction fails. + +--- + ### `ensure_opg_approval()` ```python @@ -22,9 +114,9 @@ def ensure_opg_approval( ``` Ensure the Permit2 allowance for OPG is at least ``opg_amount``. -Checks the current Permit2 allowance for the wallet. If the allowance -is already >= the requested amount, returns immediately without sending -a transaction. Otherwise, sends an ERC-20 approve transaction. +.. deprecated:: + Use ``approve_opg`` for one-off approvals or + ``ensure_opg_allowance`` for server-startup usage. **Arguments** @@ -64,10 +156,4 @@ Result of a Permit2 allowance check / approval. ```python def __init__(allowance_before: int, allowance_after: int, tx_hash: Optional[str] = None) -``` - -#### Variables - -* static `allowance_after` : int -* static `allowance_before` : int -* static `tx_hash` : Optional[str] \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/opengradient/index.md b/docs/opengradient/index.md index 06e70e21..b7487e99 100644 --- a/docs/opengradient/index.md +++ b/docs/opengradient/index.md @@ -35,7 +35,7 @@ import opengradient as og llm = og.LLM(private_key="0x...") # One-time OPG token approval (idempotent -- skips if allowance is sufficient) -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) # Chat with an LLM (TEE-verified) response = asyncio.run(llm.chat( diff --git a/src/opengradient/client/__init__.py b/src/opengradient/client/__init__.py index b95480a1..ce5c5a40 100644 --- a/src/opengradient/client/__init__.py +++ b/src/opengradient/client/__init__.py @@ -44,6 +44,6 @@ "Twins": False, "client": False, "exceptions": False, - "opg_token": False, + "opg_token": True, "tee_registry": False, } diff --git a/tests/llm_test.py b/tests/llm_test.py index ce0cd48b..5c5de70d 100644 --- a/tests/llm_test.py +++ b/tests/llm_test.py @@ -488,7 +488,7 @@ async def test_tools_with_stream_falls_back_to_single_chunk(self, fake_http): assert chunks[0].choices[0].finish_reason == "tool_calls" -# ── ensure_opg_approval tests ──────────────────────────────────────── +# ── approve_opg tests ──────────────────────────────────────── class TestEnsureOpgApproval: @@ -496,7 +496,7 @@ def test_rejects_amount_below_minimum(self, fake_http): llm = _make_llm() with pytest.raises(ValueError, match="at least"): - llm.ensure_opg_approval(opg_amount=0.01) + llm.approve_opg(opg_amount=0.01) # ── Lifecycle tests ────────────────────────────────────────────────── diff --git a/tutorials/01-verifiable-ai-agent.md b/tutorials/01-verifiable-ai-agent.md index 1ae369eb..a4edfdc0 100644 --- a/tutorials/01-verifiable-ai-agent.md +++ b/tutorials/01-verifiable-ai-agent.md @@ -36,7 +36,7 @@ export OG_PRIVATE_KEY="0x..." ## Step 1: Initialize and Create the LangChain Adapter Before making any LLM calls, you need to approve OPG token spending for the x402 -payment protocol. The `ensure_opg_approval` method checks your wallet's current +payment protocol. The `approve_opg` method checks your wallet's current Permit2 allowance and only sends an on-chain transaction if the allowance is below the requested amount -- so it's safe to call every time. @@ -48,7 +48,7 @@ private_key = os.environ["OG_PRIVATE_KEY"] # Approve OPG spending for x402 payments (idempotent -- skips if already approved). llm_client = og.LLM(private_key=private_key) -llm_client.ensure_opg_approval(opg_amount=5) +llm_client.approve_opg(opg_amount=5) # Create the LangChain chat model backed by OpenGradient TEE. # The adapter creates its own internal LLM client. The approval above applies @@ -289,7 +289,7 @@ private_key = os.environ["OG_PRIVATE_KEY"] # Approve OPG spending for x402 payments (idempotent -- skips if already approved). llm_client = og.LLM(private_key=private_key) -llm_client.ensure_opg_approval(opg_amount=5) +llm_client.approve_opg(opg_amount=5) # Alpha client for on-chain model inference. alpha = og.Alpha(private_key=private_key) diff --git a/tutorials/02-streaming-multi-provider.md b/tutorials/02-streaming-multi-provider.md index 57ee4ce7..8687bb8b 100644 --- a/tutorials/02-streaming-multi-provider.md +++ b/tutorials/02-streaming-multi-provider.md @@ -36,7 +36,7 @@ export OG_PRIVATE_KEY="0x..." Start with the simplest possible call -- send a message and get a response. Before making any LLM calls, approve OPG token spending for the x402 payment protocol using -`ensure_opg_approval`. This is idempotent -- it checks the current Permit2 allowance +`approve_opg`. This is idempotent -- it checks the current Permit2 allowance and only sends a transaction if the allowance is below the requested amount. ```python @@ -53,7 +53,7 @@ if not private_key: llm = og.LLM(private_key=private_key) # Approve OPG spending for x402 payments (one-time, idempotent). -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) async def main(): result = await llm.chat( @@ -274,7 +274,7 @@ if not private_key: llm = og.LLM(private_key=private_key) # Approve OPG spending for x402 payments (idempotent -- skips if already approved). -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) PROMPT = "Explain what a Trusted Execution Environment is in two sentences." diff --git a/tutorials/03-verified-tool-calling.md b/tutorials/03-verified-tool-calling.md index 86ea1198..1a4e8419 100644 --- a/tutorials/03-verified-tool-calling.md +++ b/tutorials/03-verified-tool-calling.md @@ -36,7 +36,7 @@ export OG_PRIVATE_KEY="0x..." ## Step 1: Initialize the Client Before making any LLM calls, approve OPG token spending for the x402 payment -protocol. The `ensure_opg_approval` method is idempotent -- it checks the current +protocol. The `approve_opg` method is idempotent -- it checks the current Permit2 allowance and only sends a transaction if the allowance is below the requested amount. @@ -55,7 +55,7 @@ if not private_key: llm = og.LLM(private_key=private_key) # Approve OPG spending for x402 payments (one-time, idempotent). -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) ``` ## Step 2: Define Local Tool Implementations @@ -318,7 +318,7 @@ if not private_key: llm = og.LLM(private_key=private_key) # Approve OPG spending for x402 payments (idempotent -- skips if already approved). -llm.ensure_opg_approval(opg_amount=5) +llm.approve_opg(opg_amount=5) # ── Mock data ───────────────────────────────────────────────────────────── PORTFOLIO = {"ETH": {"amount": 5.0, "avg_cost": 1950.00}, From dd4b12586aebf8bc09a84dc32b68d2e14341ff69 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:42:46 -0400 Subject: [PATCH 04/24] fix check --- src/opengradient/client/opg_token.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 466bbc83..4b5446f7 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -8,6 +8,7 @@ from eth_account.account import LocalAccount from web3 import Web3 +from web3.types import ChecksumAddress from x402.mechanisms.evm.constants import PERMIT2_ADDRESS logger = logging.getLogger(__name__) @@ -61,8 +62,8 @@ def _send_approve_tx( wallet_account: LocalAccount, w3: Web3, token, - owner: str, - spender: str, + owner: ChecksumAddress, + spender: ChecksumAddress, amount_base: int, ) -> Permit2ApprovalResult: """Send an ERC-20 approve transaction and wait for confirmation. From 3804a44de8058db59c4a136b34dedd31fbe72b34 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:46:44 -0400 Subject: [PATCH 05/24] fix check --- docs/opengradient/client/index.md | 2 +- docs/opengradient/client/llm.md | 24 +--- docs/opengradient/client/opg_token.md | 159 -------------------------- src/opengradient/client/__init__.py | 2 +- 4 files changed, 5 insertions(+), 182 deletions(-) delete mode 100644 docs/opengradient/client/opg_token.md diff --git a/docs/opengradient/client/index.md b/docs/opengradient/client/index.md index cc7395c8..b7f6bac0 100644 --- a/docs/opengradient/client/index.md +++ b/docs/opengradient/client/index.md @@ -14,7 +14,7 @@ OpenGradient Client -- service modules for the SDK. - **[model_hub](./model_hub)** -- Model repository management: create, version, and upload ML models - **[alpha](./alpha)** -- Alpha Testnet features: on-chain ONNX model inference (VANILLA, TEE, ZKML modes), workflow deployment, and scheduled ML model execution (OpenGradient testnet gas tokens) - **[twins](./twins)** -- Digital twins chat via OpenGradient verifiable inference -- **[opg_token](./opg_token)** -- OPG token Permit2 approval utilities for x402 payments +- **`opengradient.client.opg_token`** -- OPG token Permit2 approval utilities for x402 payments - **`opengradient.client.tee_registry`** -- TEE registry client for verified endpoints and TLS certificates ## Usage diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index 77588c36..89734039 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -63,7 +63,7 @@ resolves TEEs from the on-chain registry. #### `approve_opg()` ```python -def approve_opg(self, opg_amount: float) ‑> `Permit2ApprovalResult` +def approve_opg(self, opg_amount: float) ‑> [Permit2ApprovalResult](./opg_token) ``` Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. @@ -81,12 +81,6 @@ Permit2ApprovalResult: Contains ``allowance_before``, ``allowance_after``, and ``tx_hash`` (None when no approval was needed). -**`Permit2ApprovalResult` fields:** - -* **`allowance_before`**: The Permit2 allowance before the method ran. -* **`allowance_after`**: The Permit2 allowance after the method ran. -* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. - **Raises** * **`ValueError`**: If the OPG amount is less than 0.1. @@ -231,7 +225,7 @@ def ensure_opg_allowance( self, min_allowance: float, approve_amount: Optional[float] = None -) ‑> `Permit2ApprovalResult` +) ‑> [Permit2ApprovalResult](./opg_token) ``` Ensure the Permit2 allowance stays above a minimum threshold. @@ -258,12 +252,6 @@ Permit2ApprovalResult: Contains ``allowance_before``, ``allowance_after``, and ``tx_hash`` (None when no approval was needed). -**`Permit2ApprovalResult` fields:** - -* **`allowance_before`**: The Permit2 allowance before the method ran. -* **`allowance_after`**: The Permit2 allowance after the method ran. -* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. - **Raises** * **`ValueError`**: If ``min_allowance`` is less than 0.1 or @@ -275,7 +263,7 @@ Permit2ApprovalResult: Contains ``allowance_before``, #### `ensure_opg_approval()` ```python -def ensure_opg_approval(self, opg_amount: float) ‑> `Permit2ApprovalResult` +def ensure_opg_approval(self, opg_amount: float) ‑> [Permit2ApprovalResult](./opg_token) ``` Ensure the Permit2 allowance for OPG is at least ``opg_amount``. @@ -294,12 +282,6 @@ Permit2ApprovalResult: Contains ``allowance_before``, ``allowance_after``, and ``tx_hash`` (None when no approval was needed). -**`Permit2ApprovalResult` fields:** - -* **`allowance_before`**: The Permit2 allowance before the method ran. -* **`allowance_after`**: The Permit2 allowance after the method ran. -* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. - **Raises** * **`ValueError`**: If the OPG amount is less than 0.1. diff --git a/docs/opengradient/client/opg_token.md b/docs/opengradient/client/opg_token.md deleted file mode 100644 index 360ee4ea..00000000 --- a/docs/opengradient/client/opg_token.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -outline: [2,4] ---- - -[opengradient](../index) / [client](./index) / opg_token - -# Package opengradient.client.opg_token - -OPG token Permit2 approval utilities for x402 payments. - -## Functions - ---- - -### `approve_opg()` - -```python -def approve_opg( - wallet_account: `LocalAccount`, - opg_amount: float -) ‑> `Permit2ApprovalResult` -``` -Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. - -Idempotent: if the current allowance is already >= ``opg_amount``, no -transaction is sent. - -Best for one-off usage — scripts, notebooks, CLI tools:: - - result = approve_opg(wallet, 5.0) - -**Arguments** - -* **`wallet_account`**: The wallet account to check and approve from. -* **`opg_amount`**: Number of OPG tokens to approve (e.g. ``5.0`` for 5 OPG). - Converted to base units (18 decimals) internally. - -**Returns** - -Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). - -**`Permit2ApprovalResult` fields:** - -* **`allowance_before`**: The Permit2 allowance before the method ran. -* **`allowance_after`**: The Permit2 allowance after the method ran. -* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. - -**Raises** - -* **`RuntimeError`**: If the approval transaction fails. - ---- - -### `ensure_opg_allowance()` - -```python -def ensure_opg_allowance( - wallet_account: `LocalAccount`, - min_allowance: float, - approve_amount: Optional[float] = None -) ‑> `Permit2ApprovalResult` -``` -Ensure the Permit2 allowance stays above a minimum threshold. - -Only sends an approval transaction when the current allowance drops -below ``min_allowance``. When approval is needed, approves -``approve_amount`` (defaults to ``10 * min_allowance``) to create a -buffer that survives multiple service restarts without re-approving. - -Best for backend servers that call this on startup:: - - # On startup — only sends a tx when allowance < 5 OPG, - # then approves 100 OPG so subsequent restarts are free. - result = ensure_opg_allowance(wallet, min_allowance=5.0, approve_amount=100.0) - -**Arguments** - -* **`wallet_account`**: The wallet account to check and approve from. -* **`min_allowance`**: The minimum acceptable allowance in OPG. A - transaction is only sent when the current allowance is - strictly below this value. -* **`approve_amount`**: The amount of OPG to approve when a transaction - is needed. Defaults to ``10 * min_allowance``. Must be - >= ``min_allowance``. - -**Returns** - -Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). - -**`Permit2ApprovalResult` fields:** - -* **`allowance_before`**: The Permit2 allowance before the method ran. -* **`allowance_after`**: The Permit2 allowance after the method ran. -* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. - -**Raises** - -* **`ValueError`**: If ``approve_amount`` is less than ``min_allowance``. -* **`RuntimeError`**: If the approval transaction fails. - ---- - -### `ensure_opg_approval()` - -```python -def ensure_opg_approval( - wallet_account: `LocalAccount`, - opg_amount: float -) ‑> `Permit2ApprovalResult` -``` -Ensure the Permit2 allowance for OPG is at least ``opg_amount``. - -.. deprecated:: - Use ``approve_opg`` for one-off approvals or - ``ensure_opg_allowance`` for server-startup usage. - -**Arguments** - -* **`wallet_account`**: The wallet account to check and approve from. -* **`opg_amount`**: Minimum number of OPG tokens required (e.g. ``5.0`` - for 5 OPG). Converted to base units (18 decimals) internally. - -**Returns** - -Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). - -**`Permit2ApprovalResult` fields:** - -* **`allowance_before`**: The Permit2 allowance before the method ran. -* **`allowance_after`**: The Permit2 allowance after the method ran. -* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. - -**Raises** - -* **`RuntimeError`**: If the approval transaction fails. - -## Classes - -### `Permit2ApprovalResult` - -Result of a Permit2 allowance check / approval. - -**Attributes** - -* **`allowance_before`**: The Permit2 allowance before the method ran. -* **`allowance_after`**: The Permit2 allowance after the method ran. -* **`tx_hash`**: Transaction hash of the approval, or None if no transaction was needed. - -#### Constructor - -```python -def __init__(allowance_before: int, allowance_after: int, tx_hash: Optional[str] = None) -``` \ No newline at end of file diff --git a/src/opengradient/client/__init__.py b/src/opengradient/client/__init__.py index ce5c5a40..b95480a1 100644 --- a/src/opengradient/client/__init__.py +++ b/src/opengradient/client/__init__.py @@ -44,6 +44,6 @@ "Twins": False, "client": False, "exceptions": False, - "opg_token": True, + "opg_token": False, "tee_registry": False, } From 1aa8d8e7846da145eba488fe097e10a6ee105f4f Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:50:22 -0400 Subject: [PATCH 06/24] allowance --- README.md | 4 ++-- docs/CLAUDE_SDK_USERS.md | 4 ++-- docs/opengradient/client/index.md | 2 +- docs/opengradient/client/llm.md | 2 +- docs/opengradient/index.md | 2 +- examples/langchain_react_agent.py | 4 ++-- examples/llm_chat.py | 2 +- examples/llm_chat_streaming.py | 2 +- examples/llm_tool_calling.py | 4 ++-- src/opengradient/__init__.py | 2 +- src/opengradient/client/__init__.py | 2 +- src/opengradient/client/llm.py | 6 +++--- src/opengradient/client/opg_token.py | 6 +++--- stresstest/llm.py | 2 +- tutorials/01-verifiable-ai-agent.md | 14 +++++++------- tutorials/02-streaming-multi-provider.md | 14 +++++++------- tutorials/03-verified-tool-calling.md | 15 +++++++-------- 17 files changed, 43 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b5e9e937..a889b3e1 100644 --- a/README.md +++ b/README.md @@ -88,10 +88,10 @@ hub = og.ModelHub(email="you@example.com", password="...") ### OPG Token Approval -Before making LLM requests, your wallet must approve OPG token spending via the [Permit2](https://github.com/Uniswap/permit2) protocol. Call this once (it's idempotent — no transaction is sent if the allowance already covers the requested amount): +Before making LLM requests, your wallet must approve OPG token spending via the [Permit2](https://github.com/Uniswap/permit2) protocol. This only sends an on-chain transaction when the current allowance drops below the threshold: ```python -llm.approve_opg(opg_amount=5) +llm.ensure_opg_allowance(min_allowance=5) ``` See [Payment Settlement](#payment-settlement) for details on settlement modes. diff --git a/docs/CLAUDE_SDK_USERS.md b/docs/CLAUDE_SDK_USERS.md index c7e14690..c45a34bb 100644 --- a/docs/CLAUDE_SDK_USERS.md +++ b/docs/CLAUDE_SDK_USERS.md @@ -21,8 +21,8 @@ import os # Create an LLM client llm = og.LLM(private_key=os.environ["OG_PRIVATE_KEY"]) -# One-time OPG token approval (idempotent — skips if allowance already sufficient) -llm.approve_opg(opg_amount=0.1) +# Ensure sufficient OPG allowance (only sends tx when below threshold) +llm.ensure_opg_allowance(min_allowance=0.1) # LLM Chat (TEE-verified with x402 payments, async) result = await llm.chat( diff --git a/docs/opengradient/client/index.md b/docs/opengradient/client/index.md index b7f6bac0..e81a524b 100644 --- a/docs/opengradient/client/index.md +++ b/docs/opengradient/client/index.md @@ -24,7 +24,7 @@ import opengradient as og # LLM inference (Base Sepolia OPG tokens) llm = og.LLM(private_key="0x...") -llm.approve_opg(opg_amount=5) +llm.ensure_opg_allowance(min_allowance=5) result = await llm.chat(model=og.TEE_LLM.CLAUDE_HAIKU_4_5, messages=[...]) # On-chain model inference (OpenGradient testnet gas tokens) diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index 89734039..f145100d 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -21,7 +21,7 @@ Supports both streaming and non-streaming responses. All request methods (``chat``, ``completion``) are async. Before making LLM requests, ensure your wallet has approved sufficient -OPG tokens for Permit2 spending by calling ``ensure_opg_approval``. +OPG tokens for Permit2 spending by calling ``ensure_opg_allowance``. This only sends an on-chain transaction when the current allowance is below the requested amount. diff --git a/docs/opengradient/index.md b/docs/opengradient/index.md index b7487e99..b61194f5 100644 --- a/docs/opengradient/index.md +++ b/docs/opengradient/index.md @@ -35,7 +35,7 @@ import opengradient as og llm = og.LLM(private_key="0x...") # One-time OPG token approval (idempotent -- skips if allowance is sufficient) -llm.approve_opg(opg_amount=5) +llm.ensure_opg_allowance(min_allowance=5) # Chat with an LLM (TEE-verified) response = asyncio.run(llm.chat( diff --git a/examples/langchain_react_agent.py b/examples/langchain_react_agent.py index 36105505..aec95696 100644 --- a/examples/langchain_react_agent.py +++ b/examples/langchain_react_agent.py @@ -17,9 +17,9 @@ private_key = os.environ["OG_PRIVATE_KEY"] -# One-time Permit2 approval for OPG spending (idempotent) +# Ensure sufficient OPG allowance for Permit2 spending llm_client = og.LLM(private_key=private_key) -llm_client.approve_opg(opg_amount=5) +llm_client.ensure_opg_allowance(min_allowance=5) # Create the OpenGradient LangChain adapter llm = og.agents.langchain_adapter( diff --git a/examples/llm_chat.py b/examples/llm_chat.py index 8cf4fb7e..34d0edd6 100644 --- a/examples/llm_chat.py +++ b/examples/llm_chat.py @@ -10,7 +10,7 @@ async def main(): llm = og.LLM(private_key=os.environ.get("OG_PRIVATE_KEY")) - llm.approve_opg(opg_amount=0.1) + llm.ensure_opg_allowance(min_allowance=0.1) messages = [ {"role": "user", "content": "What is the capital of France?"}, diff --git a/examples/llm_chat_streaming.py b/examples/llm_chat_streaming.py index 1078fbc5..91811f18 100644 --- a/examples/llm_chat_streaming.py +++ b/examples/llm_chat_streaming.py @@ -6,7 +6,7 @@ async def main(): llm = og.LLM(private_key=os.environ.get("OG_PRIVATE_KEY")) - llm.approve_opg(opg_amount=0.1) + llm.ensure_opg_allowance(min_allowance=0.1) messages = [ {"role": "user", "content": "What is Python?"}, diff --git a/examples/llm_tool_calling.py b/examples/llm_tool_calling.py index c0f0ddfe..6c2a985f 100644 --- a/examples/llm_tool_calling.py +++ b/examples/llm_tool_calling.py @@ -50,8 +50,8 @@ async def main(): {"role": "user", "content": "What's the weather like in Dallas, Texas? Give me the temperature in fahrenheit."}, ] - # One-time Permit2 approval for OPG spending (idempotent) - llm.approve_opg(opg_amount=0.1) + # Ensure sufficient OPG allowance for Permit2 spending + llm.ensure_opg_allowance(min_allowance=0.1) print("Testing Gemini tool calls...") print(f"Model: {og.TEE_LLM.GEMINI_2_5_FLASH_LITE}") diff --git a/src/opengradient/__init__.py b/src/opengradient/__init__.py index b7e94cde..b74a1b66 100644 --- a/src/opengradient/__init__.py +++ b/src/opengradient/__init__.py @@ -26,7 +26,7 @@ llm = og.LLM(private_key="0x...") # One-time OPG token approval (idempotent -- skips if allowance is sufficient) -llm.approve_opg(opg_amount=5) +llm.ensure_opg_allowance(min_allowance=5) # Chat with an LLM (TEE-verified) response = asyncio.run(llm.chat( diff --git a/src/opengradient/client/__init__.py b/src/opengradient/client/__init__.py index b95480a1..59e98242 100644 --- a/src/opengradient/client/__init__.py +++ b/src/opengradient/client/__init__.py @@ -17,7 +17,7 @@ # LLM inference (Base Sepolia OPG tokens) llm = og.LLM(private_key="0x...") -llm.approve_opg(opg_amount=5) +llm.ensure_opg_allowance(min_allowance=5) result = await llm.chat(model=og.TEE_LLM.CLAUDE_HAIKU_4_5, messages=[...]) # On-chain model inference (OpenGradient testnet gas tokens) diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index 59ddb99e..67d4d48d 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -58,7 +58,7 @@ class LLM: All request methods (``chat``, ``completion``) are async. Before making LLM requests, ensure your wallet has approved sufficient - OPG tokens for Permit2 spending by calling ``ensure_opg_approval``. + OPG tokens for Permit2 spending by calling ``ensure_opg_allowance``. This only sends an on-chain transaction when the current allowance is below the requested amount. @@ -69,8 +69,8 @@ class LLM: # Via hardcoded URL (development / self-hosted) llm = og.LLM.from_url(private_key="0x...", llm_server_url="https://1.2.3.4") - # One-time approval (idempotent — skips if allowance is already sufficient) - llm.ensure_opg_approval(opg_amount=5) + # Ensure sufficient OPG allowance (only sends tx when below threshold) + llm.ensure_opg_allowance(min_allowance=5) result = await llm.chat(model=TEE_LLM.CLAUDE_HAIKU_4_5, messages=[...]) result = await llm.completion(model=TEE_LLM.CLAUDE_HAIKU_4_5, prompt="Hello") diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 4b5446f7..05157558 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -188,7 +188,7 @@ def ensure_opg_allowance( Only sends an approval transaction when the current allowance drops below ``min_allowance``. When approval is needed, approves - ``approve_amount`` (defaults to ``10 * min_allowance``) to create a + ``approve_amount`` (defaults to ``2 * min_allowance``) to create a buffer that survives multiple service restarts without re-approving. Best for backend servers that call this on startup:: @@ -203,7 +203,7 @@ def ensure_opg_allowance( transaction is only sent when the current allowance is strictly below this value. approve_amount: The amount of OPG to approve when a transaction - is needed. Defaults to ``10 * min_allowance``. Must be + is needed. Defaults to ``2 * min_allowance``. Must be >= ``min_allowance``. Returns: @@ -216,7 +216,7 @@ def ensure_opg_allowance( RuntimeError: If the approval transaction fails. """ if approve_amount is None: - approve_amount = min_allowance * 10 + approve_amount = min_allowance * 2 if approve_amount < min_allowance: raise ValueError(f"approve_amount ({approve_amount}) must be >= min_allowance ({min_allowance})") diff --git a/stresstest/llm.py b/stresstest/llm.py index d84f3a17..45430b83 100644 --- a/stresstest/llm.py +++ b/stresstest/llm.py @@ -13,7 +13,7 @@ async def main(private_key: str): llm = og.LLM(private_key=private_key) - llm.approve_opg(opg_amount=0.1) + llm.ensure_opg_allowance(min_allowance=0.1) async def run_prompt(prompt: str): messages = [{"role": "user", "content": prompt}] diff --git a/tutorials/01-verifiable-ai-agent.md b/tutorials/01-verifiable-ai-agent.md index a4edfdc0..9eea4363 100644 --- a/tutorials/01-verifiable-ai-agent.md +++ b/tutorials/01-verifiable-ai-agent.md @@ -36,9 +36,9 @@ export OG_PRIVATE_KEY="0x..." ## Step 1: Initialize and Create the LangChain Adapter Before making any LLM calls, you need to approve OPG token spending for the x402 -payment protocol. The `approve_opg` method checks your wallet's current -Permit2 allowance and only sends an on-chain transaction if the allowance is below -the requested amount -- so it's safe to call every time. +payment protocol. The `ensure_opg_allowance` method checks your wallet's current +Permit2 allowance and only sends an on-chain transaction if the allowance drops +below the threshold -- so it's safe to call every time. ```python import os @@ -46,9 +46,9 @@ import opengradient as og private_key = os.environ["OG_PRIVATE_KEY"] -# Approve OPG spending for x402 payments (idempotent -- skips if already approved). +# Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). llm_client = og.LLM(private_key=private_key) -llm_client.approve_opg(opg_amount=5) +llm_client.ensure_opg_allowance(min_allowance=5) # Create the LangChain chat model backed by OpenGradient TEE. # The adapter creates its own internal LLM client. The approval above applies @@ -287,9 +287,9 @@ SAMPLE_PRICES = { # ── Clients ─────────────────────────────────────────────────────────────── private_key = os.environ["OG_PRIVATE_KEY"] -# Approve OPG spending for x402 payments (idempotent -- skips if already approved). +# Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). llm_client = og.LLM(private_key=private_key) -llm_client.approve_opg(opg_amount=5) +llm_client.ensure_opg_allowance(min_allowance=5) # Alpha client for on-chain model inference. alpha = og.Alpha(private_key=private_key) diff --git a/tutorials/02-streaming-multi-provider.md b/tutorials/02-streaming-multi-provider.md index 8687bb8b..60f9b57c 100644 --- a/tutorials/02-streaming-multi-provider.md +++ b/tutorials/02-streaming-multi-provider.md @@ -35,9 +35,9 @@ export OG_PRIVATE_KEY="0x..." ## Step 1: Basic Non-Streaming Chat Start with the simplest possible call -- send a message and get a response. Before -making any LLM calls, approve OPG token spending for the x402 payment protocol using -`approve_opg`. This is idempotent -- it checks the current Permit2 allowance -and only sends a transaction if the allowance is below the requested amount. +making any LLM calls, ensure sufficient OPG token allowance for the x402 payment +protocol using `ensure_opg_allowance`. This only sends a transaction when the +current allowance drops below the threshold. ```python import asyncio @@ -52,8 +52,8 @@ if not private_key: llm = og.LLM(private_key=private_key) -# Approve OPG spending for x402 payments (one-time, idempotent). -llm.approve_opg(opg_amount=5) +# Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). +llm.ensure_opg_allowance(min_allowance=5) async def main(): result = await llm.chat( @@ -273,8 +273,8 @@ if not private_key: llm = og.LLM(private_key=private_key) -# Approve OPG spending for x402 payments (idempotent -- skips if already approved). -llm.approve_opg(opg_amount=5) +# Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). +llm.ensure_opg_allowance(min_allowance=5) PROMPT = "Explain what a Trusted Execution Environment is in two sentences." diff --git a/tutorials/03-verified-tool-calling.md b/tutorials/03-verified-tool-calling.md index 1a4e8419..99f58594 100644 --- a/tutorials/03-verified-tool-calling.md +++ b/tutorials/03-verified-tool-calling.md @@ -35,10 +35,9 @@ export OG_PRIVATE_KEY="0x..." ## Step 1: Initialize the Client -Before making any LLM calls, approve OPG token spending for the x402 payment -protocol. The `approve_opg` method is idempotent -- it checks the current -Permit2 allowance and only sends a transaction if the allowance is below the -requested amount. +Before making any LLM calls, ensure sufficient OPG token allowance for the x402 +payment protocol. The `ensure_opg_allowance` method only sends a transaction when +the current allowance drops below the threshold. ```python import json @@ -54,8 +53,8 @@ if not private_key: llm = og.LLM(private_key=private_key) -# Approve OPG spending for x402 payments (one-time, idempotent). -llm.approve_opg(opg_amount=5) +# Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). +llm.ensure_opg_allowance(min_allowance=5) ``` ## Step 2: Define Local Tool Implementations @@ -317,8 +316,8 @@ if not private_key: llm = og.LLM(private_key=private_key) -# Approve OPG spending for x402 payments (idempotent -- skips if already approved). -llm.approve_opg(opg_amount=5) +# Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). +llm.ensure_opg_allowance(min_allowance=5) # ── Mock data ───────────────────────────────────────────────────────────── PORTFOLIO = {"ETH": {"amount": 5.0, "avg_cost": 1950.00}, From d9eb8d8871a2a603ceb7bd6a8d7a10e6e20ec2a7 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:52:57 -0400 Subject: [PATCH 07/24] rm allowance --- src/opengradient/client/llm.py | 24 ----------------------- src/opengradient/client/opg_token.py | 29 ---------------------------- 2 files changed, 53 deletions(-) diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index 67d4d48d..d3d40ba4 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -244,30 +244,6 @@ def ensure_opg_allowance( raise ValueError("min_allowance must be at least 0.1.") return ensure_opg_allowance(self._wallet_account, min_allowance, approve_amount) - def ensure_opg_approval(self, opg_amount: float) -> Permit2ApprovalResult: - """Ensure the Permit2 allowance for OPG is at least ``opg_amount``. - - .. deprecated:: - Use ``approve_opg`` for one-off approvals or - ``ensure_opg_allowance`` for server-startup usage. - - Args: - opg_amount: Minimum number of OPG tokens required (e.g. ``0.1`` - for 0.1 OPG). Must be at least 0.1 OPG. - - Returns: - Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). - - Raises: - ValueError: If the OPG amount is less than 0.1. - RuntimeError: If the approval transaction fails. - """ - if opg_amount < 0.1: - raise ValueError("OPG amount must be at least 0.1.") - return ensure_opg_approval(self._wallet_account, opg_amount) - async def completion( self, model: TEE_LLM, diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 05157558..435d4d53 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -246,32 +246,3 @@ def ensure_opg_allowance( min_base, approve_base, ) - return _send_approve_tx(wallet_account, w3, token, owner, spender, approve_base) - - -def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: - """Ensure the Permit2 allowance for OPG is at least ``opg_amount``. - - .. deprecated:: - Use ``approve_opg`` for one-off approvals or - ``ensure_opg_allowance`` for server-startup usage. - - Args: - wallet_account: The wallet account to check and approve from. - opg_amount: Minimum number of OPG tokens required (e.g. ``5.0`` - for 5 OPG). Converted to base units (18 decimals) internally. - - Returns: - Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). - - Raises: - RuntimeError: If the approval transaction fails. - """ - warnings.warn( - "ensure_opg_approval is deprecated. Use approve_opg for one-off approvals or ensure_opg_allowance for server-startup usage.", - DeprecationWarning, - stacklevel=2, - ) - return approve_opg(wallet_account, opg_amount) From 301bf70d2dc17090fa94e57a48521925bbe82bf3 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:55:27 -0400 Subject: [PATCH 08/24] tests --- src/opengradient/client/llm.py | 2 +- src/opengradient/client/opg_token.py | 2 +- tests/opg_token_test.py | 41 ++-------------------------- 3 files changed, 5 insertions(+), 40 deletions(-) diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index d3d40ba4..031e9a84 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -15,7 +15,7 @@ from x402.mechanisms.evm.upto.register import register_upto_evm_client from ..types import TEE_LLM, StreamChoice, StreamChunk, StreamDelta, TextGenerationOutput, x402SettlementMode -from .opg_token import Permit2ApprovalResult, approve_opg, ensure_opg_allowance, ensure_opg_approval +from .opg_token import Permit2ApprovalResult, approve_opg, ensure_opg_allowance from .tee_connection import RegistryTEEConnection, StaticTEEConnection, TEEConnectionInterface from .tee_registry import TEERegistry diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 435d4d53..caac2260 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -2,7 +2,6 @@ import logging import time -import warnings from dataclasses import dataclass from typing import Optional @@ -246,3 +245,4 @@ def ensure_opg_allowance( min_base, approve_base, ) + return _send_approve_tx(wallet_account, w3, token, owner, spender, approve_base) diff --git a/tests/opg_token_test.py b/tests/opg_token_test.py index b909a6e1..38ffb54a 100644 --- a/tests/opg_token_test.py +++ b/tests/opg_token_test.py @@ -7,7 +7,6 @@ from opengradient.client.opg_token import ( approve_opg, ensure_opg_allowance, - ensure_opg_approval, ) OWNER_ADDRESS = "0x1234567890abcdef1234567890ABCDEF12345678" @@ -256,12 +255,12 @@ def test_exact_minimum_skips(self, mock_wallet, mock_web3): class TestEnsureOpgAllowanceSendsTx: """Cases where allowance is below the minimum and a tx is needed.""" - def test_approves_default_10x_amount(self, mock_wallet, mock_web3): - """When no approve_amount given, approves 10x min_allowance.""" + def test_approves_default_with_greater_amount(self, mock_wallet, mock_web3): + """When no approve_amount given, approves 2x min_allowance.""" contract = _setup_allowance(mock_web3, 0) _setup_approval_mocks(mock_web3, mock_wallet, contract) - approve_base = int(50.0 * 10**18) # 10x of 5.0 + approve_base = int(10.0 * 10**18) # 2x of 5.0 contract.functions.allowance.return_value.call.side_effect = [0, 0, approve_base] result = ensure_opg_allowance(mock_wallet, min_allowance=5.0) @@ -309,40 +308,6 @@ def test_approve_amount_less_than_min_raises(self, mock_wallet, mock_web3): ensure_opg_allowance(mock_wallet, min_allowance=10.0, approve_amount=5.0) -# ── ensure_opg_approval (deprecated) tests ────────────────────────── - - -class TestEnsureOpgApprovalDeprecated: - """The old function still works but emits a deprecation warning.""" - - def test_emits_deprecation_warning(self, mock_wallet, mock_web3): - """ensure_opg_approval should emit a DeprecationWarning.""" - _setup_allowance(mock_web3, int(10 * 10**18)) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - ensure_opg_approval(mock_wallet, 5.0) - - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "deprecated" in str(w[0].message).lower() - - def test_delegates_to_approve_opg(self, mock_wallet, mock_web3): - """ensure_opg_approval should produce the same result as approve_opg.""" - amount_base = int(5.0 * 10**18) - _setup_allowance(mock_web3, amount_base) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - result = ensure_opg_approval(mock_wallet, 5.0) - - assert result.allowance_before == amount_base - assert result.tx_hash is None - - -# ── Amount conversion tests ───────────────────────────────────────── - - class TestAmountConversion: """Verify float-to-base-unit conversion.""" From 24a7cb5e679211eb5fa7e393b831d63fd232e4be Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 22:56:59 -0400 Subject: [PATCH 09/24] rm debug --- src/opengradient/client/opg_token.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index caac2260..7342cea6 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -168,7 +168,6 @@ def approve_opg(wallet_account: LocalAccount, opg_amount: float) -> Permit2Appro allowance_before = token.functions.allowance(owner, spender).call() if allowance_before >= amount_base: - logger.debug("Permit2 allowance already sufficient (%s >= %s), skipping approval", allowance_before, amount_base) return Permit2ApprovalResult( allowance_before=allowance_before, allowance_after=allowance_before, @@ -229,11 +228,6 @@ def ensure_opg_allowance( allowance_before = token.functions.allowance(owner, spender).call() if allowance_before >= min_base: - logger.debug( - "Permit2 allowance above minimum threshold (%s >= %s), skipping approval", - allowance_before, - min_base, - ) return Permit2ApprovalResult( allowance_before=allowance_before, allowance_after=allowance_before, From c25bf52fb22cc06f7fd34011f18f2df5fd537836 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:00:07 -0400 Subject: [PATCH 10/24] simpple approval --- docs/opengradient/client/llm.md | 33 ++-------------------------- src/opengradient/client/llm.py | 16 ++++++-------- src/opengradient/client/opg_token.py | 21 +++++------------- 3 files changed, 14 insertions(+), 56 deletions(-) diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index f145100d..52059309 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -73,7 +73,7 @@ transaction is sent. Best for one-off usage — scripts, notebooks, CLI tools. **Arguments** * **`opg_amount`**: Number of OPG tokens to approve (e.g. ``0.1`` - for 0.1 OPG). Must be at least 0.1 OPG. + for 0.1 OPG). Must be at least 0.01 OPG. **Returns** @@ -83,7 +83,7 @@ Permit2ApprovalResult: Contains ``allowance_before``, **Raises** -* **`ValueError`**: If the OPG amount is less than 0.1. +* **`ValueError`**: If the OPG amount is less than 0.01. * **`RuntimeError`**: If the approval transaction fails. --- @@ -256,33 +256,4 @@ Permit2ApprovalResult: Contains ``allowance_before``, * **`ValueError`**: If ``min_allowance`` is less than 0.1 or ``approve_amount`` is less than ``min_allowance``. -* **`RuntimeError`**: If the approval transaction fails. - ---- - -#### `ensure_opg_approval()` - -```python -def ensure_opg_approval(self, opg_amount: float) ‑> [Permit2ApprovalResult](./opg_token) -``` -Ensure the Permit2 allowance for OPG is at least ``opg_amount``. - -.. deprecated:: - Use ``approve_opg`` for one-off approvals or - ``ensure_opg_allowance`` for server-startup usage. - -**Arguments** - -* **`opg_amount`**: Minimum number of OPG tokens required (e.g. ``0.1`` - for 0.1 OPG). Must be at least 0.1 OPG. - -**Returns** - -Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). - -**Raises** - -* **`ValueError`**: If the OPG amount is less than 0.1. * **`RuntimeError`**: If the approval transaction fails. \ No newline at end of file diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index 031e9a84..bfc492d1 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -185,26 +185,24 @@ async def _call_with_tee_retry( # ── Public API ────────────────────────────────────────────────────── def approve_opg(self, opg_amount: float) -> Permit2ApprovalResult: - """Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. + """Approve Permit2 to spend ``opg_amount`` OPG tokens. - Idempotent: if the current allowance is already >= ``opg_amount``, no - transaction is sent. Best for one-off usage — scripts, notebooks, CLI tools. + Always sends an approval transaction regardless of the current allowance. Args: opg_amount: Number of OPG tokens to approve (e.g. ``0.1`` - for 0.1 OPG). Must be at least 0.1 OPG. + for 0.1 OPG). Must be at least 0.01 OPG. Returns: Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). + ``allowance_after``, and ``tx_hash``. Raises: - ValueError: If the OPG amount is less than 0.1. + ValueError: If the OPG amount is less than 0.01. RuntimeError: If the approval transaction fails. """ - if opg_amount < 0.1: - raise ValueError("OPG amount must be at least 0.1.") + if opg_amount < 0.01: + raise ValueError("OPG amount must be at least 0.01.") return approve_opg(self._wallet_account, opg_amount) def ensure_opg_allowance( diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 7342cea6..0d648d4e 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -138,24 +138,22 @@ def _get_web3_and_contract(): def approve_opg(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: - """Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. + """Approve Permit2 to spend ``opg_amount`` OPG tokens. - Idempotent: if the current allowance is already >= ``opg_amount``, no - transaction is sent. + Always sends an approval transaction regardless of the current allowance. - Best for one-off usage — scripts, notebooks, CLI tools:: + Example:: result = approve_opg(wallet, 5.0) Args: - wallet_account: The wallet account to check and approve from. + wallet_account: The wallet account to approve from. opg_amount: Number of OPG tokens to approve (e.g. ``5.0`` for 5 OPG). Converted to base units (18 decimals) internally. Returns: Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). + ``allowance_after``, and ``tx_hash``. Raises: RuntimeError: If the approval transaction fails. @@ -165,15 +163,6 @@ def approve_opg(wallet_account: LocalAccount, opg_amount: float) -> Permit2Appro w3, token, spender = _get_web3_and_contract() owner = Web3.to_checksum_address(wallet_account.address) - allowance_before = token.functions.allowance(owner, spender).call() - - if allowance_before >= amount_base: - return Permit2ApprovalResult( - allowance_before=allowance_before, - allowance_after=allowance_before, - ) - - logger.info("Permit2 allowance insufficient (%s < %s), sending approval tx", allowance_before, amount_base) return _send_approve_tx(wallet_account, w3, token, owner, spender, amount_base) From 01a1a0698fdee888676f481b38909047496bc7f0 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:00:22 -0400 Subject: [PATCH 11/24] docs --- docs/opengradient/client/llm.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index 52059309..4e66d0bc 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -65,10 +65,9 @@ resolves TEEs from the on-chain registry. ```python def approve_opg(self, opg_amount: float) ‑> [Permit2ApprovalResult](./opg_token) ``` -Approve Permit2 to spend ``opg_amount`` OPG if the current allowance is insufficient. +Approve Permit2 to spend ``opg_amount`` OPG tokens. -Idempotent: if the current allowance is already >= ``opg_amount``, no -transaction is sent. Best for one-off usage — scripts, notebooks, CLI tools. +Always sends an approval transaction regardless of the current allowance. **Arguments** @@ -78,8 +77,7 @@ transaction is sent. Best for one-off usage — scripts, notebooks, CLI tools. **Returns** Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash`` (None when no approval - was needed). + ``allowance_after``, and ``tx_hash``. **Raises** From 62e8d4319eeee2e8a430f4590a6f074cf7a60fac Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:01:50 -0400 Subject: [PATCH 12/24] docs --- docs/opengradient/client/llm.md | 4 +--- src/opengradient/client/llm.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index 4e66d0bc..9b211927 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -21,9 +21,7 @@ Supports both streaming and non-streaming responses. All request methods (``chat``, ``completion``) are async. Before making LLM requests, ensure your wallet has approved sufficient -OPG tokens for Permit2 spending by calling ``ensure_opg_allowance``. -This only sends an on-chain transaction when the current allowance is -below the requested amount. +OPG tokens for Permit2 spending by calling ``ensure_opg_allowance`` or ``approve_opg``. #### Constructor diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index bfc492d1..9383b125 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -58,9 +58,7 @@ class LLM: All request methods (``chat``, ``completion``) are async. Before making LLM requests, ensure your wallet has approved sufficient - OPG tokens for Permit2 spending by calling ``ensure_opg_allowance``. - This only sends an on-chain transaction when the current allowance is - below the requested amount. + OPG tokens for Permit2 spending by calling ``ensure_opg_allowance`` or ``approve_opg``. Usage: # Via on-chain registry (default) From cc7253202398ad4ae4395eb4501d773db93e15d9 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:15:58 -0400 Subject: [PATCH 13/24] keep 1 --- docs/opengradient/client/llm.md | 28 +--- integrationtest/llm/test_llm_chat.py | 2 +- src/opengradient/client/llm.py | 25 +--- src/opengradient/client/opg_token.py | 45 +++--- tests/llm_test.py | 11 -- tests/opg_token_test.py | 203 ++++++--------------------- 6 files changed, 60 insertions(+), 254 deletions(-) diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index 9b211927..6e611b51 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -21,7 +21,7 @@ Supports both streaming and non-streaming responses. All request methods (``chat``, ``completion``) are async. Before making LLM requests, ensure your wallet has approved sufficient -OPG tokens for Permit2 spending by calling ``ensure_opg_allowance`` or ``approve_opg``. +OPG tokens for Permit2 spending by calling ``ensure_opg_allowance``. #### Constructor @@ -58,32 +58,6 @@ resolves TEEs from the on-chain registry. --- -#### `approve_opg()` - -```python -def approve_opg(self, opg_amount: float) ‑> [Permit2ApprovalResult](./opg_token) -``` -Approve Permit2 to spend ``opg_amount`` OPG tokens. - -Always sends an approval transaction regardless of the current allowance. - -**Arguments** - -* **`opg_amount`**: Number of OPG tokens to approve (e.g. ``0.1`` - for 0.1 OPG). Must be at least 0.01 OPG. - -**Returns** - -Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash``. - -**Raises** - -* **`ValueError`**: If the OPG amount is less than 0.01. -* **`RuntimeError`**: If the approval transaction fails. - ---- - #### `chat()` ```python diff --git a/integrationtest/llm/test_llm_chat.py b/integrationtest/llm/test_llm_chat.py index bd666812..f6124063 100644 --- a/integrationtest/llm/test_llm_chat.py +++ b/integrationtest/llm/test_llm_chat.py @@ -99,7 +99,7 @@ def llm_client(): print("Account funded with ETH and OPG") llm = og.LLM(private_key=account.key.hex()) - llm.approve_opg(opg_amount=OPG_FUND_AMOUNT) + llm.ensure_opg_allowance(min_allowance=OPG_FUND_AMOUNT, approve_amount=OPG_FUND_AMOUNT) print("Permit2 approval complete") # Wait for the approval to propagate on-chain diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index 9383b125..ec053231 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -15,7 +15,7 @@ from x402.mechanisms.evm.upto.register import register_upto_evm_client from ..types import TEE_LLM, StreamChoice, StreamChunk, StreamDelta, TextGenerationOutput, x402SettlementMode -from .opg_token import Permit2ApprovalResult, approve_opg, ensure_opg_allowance +from .opg_token import Permit2ApprovalResult, ensure_opg_allowance from .tee_connection import RegistryTEEConnection, StaticTEEConnection, TEEConnectionInterface from .tee_registry import TEERegistry @@ -58,7 +58,7 @@ class LLM: All request methods (``chat``, ``completion``) are async. Before making LLM requests, ensure your wallet has approved sufficient - OPG tokens for Permit2 spending by calling ``ensure_opg_allowance`` or ``approve_opg``. + OPG tokens for Permit2 spending by calling ``ensure_opg_allowance``. Usage: # Via on-chain registry (default) @@ -182,27 +182,6 @@ async def _call_with_tee_retry( # ── Public API ────────────────────────────────────────────────────── - def approve_opg(self, opg_amount: float) -> Permit2ApprovalResult: - """Approve Permit2 to spend ``opg_amount`` OPG tokens. - - Always sends an approval transaction regardless of the current allowance. - - Args: - opg_amount: Number of OPG tokens to approve (e.g. ``0.1`` - for 0.1 OPG). Must be at least 0.01 OPG. - - Returns: - Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash``. - - Raises: - ValueError: If the OPG amount is less than 0.01. - RuntimeError: If the approval transaction fails. - """ - if opg_amount < 0.01: - raise ValueError("OPG amount must be at least 0.01.") - return approve_opg(self._wallet_account, opg_amount) - def ensure_opg_allowance( self, min_allowance: float, diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 0d648d4e..86515231 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -39,6 +39,13 @@ "stateMutability": "nonpayable", "type": "function", }, + { + "inputs": [{"name": "account", "type": "address"}], + "name": "balanceOf", + "outputs": [{"name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, ] @@ -137,35 +144,6 @@ def _get_web3_and_contract(): return w3, token, spender -def approve_opg(wallet_account: LocalAccount, opg_amount: float) -> Permit2ApprovalResult: - """Approve Permit2 to spend ``opg_amount`` OPG tokens. - - Always sends an approval transaction regardless of the current allowance. - - Example:: - - result = approve_opg(wallet, 5.0) - - Args: - wallet_account: The wallet account to approve from. - opg_amount: Number of OPG tokens to approve (e.g. ``5.0`` for 5 OPG). - Converted to base units (18 decimals) internally. - - Returns: - Permit2ApprovalResult: Contains ``allowance_before``, - ``allowance_after``, and ``tx_hash``. - - Raises: - RuntimeError: If the approval transaction fails. - """ - amount_base = int(opg_amount * 10**18) - - w3, token, spender = _get_web3_and_contract() - owner = Web3.to_checksum_address(wallet_account.address) - - return _send_approve_tx(wallet_account, w3, token, owner, spender, amount_base) - - def ensure_opg_allowance( wallet_account: LocalAccount, min_allowance: float, @@ -222,6 +200,15 @@ def ensure_opg_allowance( allowance_after=allowance_before, ) + balance = token.functions.balanceOf(owner).call() + if approve_base > balance: + logger.warning( + "Requested approve_amount (%.6f OPG) exceeds wallet balance (%.6f OPG), capping approval to wallet balance", + approve_amount, + balance / 10**18, + ) + approve_base = balance + logger.info( "Permit2 allowance below minimum threshold (%s < %s), approving %s base units", allowance_before, diff --git a/tests/llm_test.py b/tests/llm_test.py index 5c5de70d..0dd08aa9 100644 --- a/tests/llm_test.py +++ b/tests/llm_test.py @@ -488,17 +488,6 @@ async def test_tools_with_stream_falls_back_to_single_chunk(self, fake_http): assert chunks[0].choices[0].finish_reason == "tool_calls" -# ── approve_opg tests ──────────────────────────────────────── - - -class TestEnsureOpgApproval: - def test_rejects_amount_below_minimum(self, fake_http): - llm = _make_llm() - - with pytest.raises(ValueError, match="at least"): - llm.approve_opg(opg_amount=0.01) - - # ── Lifecycle tests ────────────────────────────────────────────────── diff --git a/tests/opg_token_test.py b/tests/opg_token_test.py index 38ffb54a..2e7fd88a 100644 --- a/tests/opg_token_test.py +++ b/tests/opg_token_test.py @@ -1,11 +1,8 @@ -import warnings -from types import SimpleNamespace from unittest.mock import MagicMock import pytest from opengradient.client.opg_token import ( - approve_opg, ensure_opg_allowance, ) @@ -37,10 +34,12 @@ def mock_web3(monkeypatch): return mock_w3 -def _setup_allowance(mock_w3, allowance_value): - """Configure the mock contract to return a specific allowance.""" +def _setup_allowance(mock_w3, allowance_value, balance=None): + """Configure the mock contract to return a specific allowance and balance.""" contract = MagicMock() contract.functions.allowance.return_value.call.return_value = allowance_value + # Default balance to a large value so existing tests aren't affected + contract.functions.balanceOf.return_value.call.return_value = balance if balance is not None else int(1_000_000 * 10**18) mock_w3.eth.contract.return_value = contract return contract @@ -70,162 +69,6 @@ def _setup_approval_mocks(mock_web3, mock_wallet, contract): return approve_fn, tx_hash -# ── approve_opg tests ─────────────────────────────────────────────── - - -class TestApproveOpgSkips: - """Cases where the existing allowance is sufficient.""" - - def test_exact_allowance_skips_tx(self, mock_wallet, mock_web3): - """When allowance == requested amount, no transaction is sent.""" - amount = 5.0 - amount_base = int(amount * 10**18) - _setup_allowance(mock_web3, amount_base) - - result = approve_opg(mock_wallet, amount) - - assert result.allowance_before == amount_base - assert result.allowance_after == amount_base - assert result.tx_hash is None - - def test_excess_allowance_skips_tx(self, mock_wallet, mock_web3): - """When allowance > requested amount, no transaction is sent.""" - amount_base = int(5.0 * 10**18) - _setup_allowance(mock_web3, amount_base * 2) - - result = approve_opg(mock_wallet, 5.0) - - assert result.allowance_before == amount_base * 2 - assert result.tx_hash is None - - def test_zero_amount_with_zero_allowance_skips(self, mock_wallet, mock_web3): - """Requesting 0 tokens with 0 allowance should skip (0 >= 0).""" - _setup_allowance(mock_web3, 0) - - result = approve_opg(mock_wallet, 0.0) - - assert result.tx_hash is None - - -class TestApproveOpgSendsTx: - """Cases where allowance is insufficient and a transaction is sent.""" - - def test_approval_sent_when_allowance_insufficient(self, mock_wallet, mock_web3): - """When allowance < requested, an approve tx is sent.""" - amount = 5.0 - amount_base = int(amount * 10**18) - contract = _setup_allowance(mock_web3, 0) - approve_fn, _ = _setup_approval_mocks(mock_web3, mock_wallet, contract) - - # Side effects: 1st call in approve_opg check, 2nd in _send_approve_tx before, - # 3rd in post-tx poll - contract.functions.allowance.return_value.call.side_effect = [0, 0, amount_base] - - result = approve_opg(mock_wallet, amount) - - assert result.allowance_before == 0 - assert result.allowance_after == amount_base - assert result.tx_hash == "0xabc123" - - # Verify the approve was called with the right amount - contract.functions.approve.assert_called_once() - args = contract.functions.approve.call_args[0] - assert args[1] == amount_base - - def test_gas_estimate_has_20_percent_buffer(self, mock_wallet, mock_web3): - """Gas limit should be estimatedGas * 1.2.""" - contract = _setup_allowance(mock_web3, 0) - approve_fn, _ = _setup_approval_mocks(mock_web3, mock_wallet, contract) - - contract.functions.allowance.return_value.call.side_effect = [0, 0, int(1 * 10**18)] - - approve_opg(mock_wallet, 1.0) - - tx_dict = approve_fn.build_transaction.call_args[0][0] - assert tx_dict["gas"] == int(50_000 * 1.2) - - def test_waits_for_allowance_update_after_receipt(self, mock_wallet, mock_web3, monkeypatch): - """After a successful receipt, poll allowance until the updated value is visible.""" - monkeypatch.setattr("opengradient.client.opg_token.ALLOWANCE_POLL_INTERVAL", 0) - contract = _setup_allowance(mock_web3, 0) - _setup_approval_mocks(mock_web3, mock_wallet, contract) - - amount_base = int(1.0 * 10**18) - contract.functions.allowance.return_value.call.side_effect = [0, 0, 0, amount_base] - - result = approve_opg(mock_wallet, 1.0) - - assert result.allowance_before == 0 - assert result.allowance_after == amount_base - - -class TestApproveOpgErrors: - """Error handling paths.""" - - def test_reverted_tx_raises(self, mock_wallet, mock_web3): - """A reverted transaction raises RuntimeError.""" - contract = _setup_allowance(mock_web3, 0) - - approve_fn = MagicMock() - contract.functions.approve.return_value = approve_fn - approve_fn.estimate_gas.return_value = 50_000 - - mock_web3.eth.get_transaction_count.return_value = 0 - mock_web3.eth.gas_price = 1_000_000_000 - mock_web3.eth.chain_id = 84532 - - signed = MagicMock() - signed.raw_transaction = b"\x00" - mock_wallet.sign_transaction.return_value = signed - - tx_hash = MagicMock() - tx_hash.hex.return_value = "0xfailed" - mock_web3.eth.send_raw_transaction.return_value = tx_hash - mock_web3.eth.wait_for_transaction_receipt.return_value = SimpleNamespace(status=0) - - with pytest.raises(RuntimeError, match="reverted"): - approve_opg(mock_wallet, 5.0) - - def test_generic_exception_wrapped(self, mock_wallet, mock_web3): - """Non-RuntimeError exceptions are wrapped in RuntimeError.""" - contract = _setup_allowance(mock_web3, 0) - - approve_fn = MagicMock() - contract.functions.approve.return_value = approve_fn - approve_fn.estimate_gas.side_effect = ConnectionError("RPC unavailable") - - mock_web3.eth.get_transaction_count.return_value = 0 - - with pytest.raises(RuntimeError, match="Failed to approve Permit2 for OPG"): - approve_opg(mock_wallet, 5.0) - - def test_runtime_error_not_double_wrapped(self, mock_wallet, mock_web3): - """RuntimeError raised inside the try block should propagate as-is.""" - contract = _setup_allowance(mock_web3, 0) - - approve_fn = MagicMock() - contract.functions.approve.return_value = approve_fn - approve_fn.estimate_gas.return_value = 50_000 - - mock_web3.eth.get_transaction_count.return_value = 0 - mock_web3.eth.gas_price = 1_000_000_000 - mock_web3.eth.chain_id = 84532 - - signed = MagicMock() - signed.raw_transaction = b"\x00" - mock_wallet.sign_transaction.return_value = signed - - tx_hash = MagicMock() - tx_hash.hex.return_value = "0xfailed" - mock_web3.eth.send_raw_transaction.return_value = tx_hash - mock_web3.eth.wait_for_transaction_receipt.return_value = SimpleNamespace(status=0) - - with pytest.raises(RuntimeError, match="reverted") as exc_info: - approve_opg(mock_wallet, 5.0) - - assert "Failed to approve" not in str(exc_info.value) - - # ── ensure_opg_allowance tests ────────────────────────────────────── @@ -297,6 +140,40 @@ def test_no_tx_on_restart_when_above_min(self, mock_wallet, mock_web3): assert result.allowance_before == remaining +class TestEnsureOpgAllowanceBalanceCheck: + """Balance-aware approval capping.""" + + def test_approve_amount_capped_to_balance(self, mock_wallet, mock_web3): + """When approve_amount > balance >= min_allowance, cap to balance.""" + balance = int(0.1 * 10**18) + contract = _setup_allowance(mock_web3, 0, balance=balance) + _setup_approval_mocks(mock_web3, mock_wallet, contract) + + # allowance calls: 1st for the check, 2nd in _send_approve_tx before, 3rd in post-tx poll + contract.functions.allowance.return_value.call.side_effect = [0, 0, balance] + + result = ensure_opg_allowance(mock_wallet, min_allowance=0.1) + + # Default approve_amount would be 0.2, but balance is only 0.1 — capped + args = contract.functions.approve.call_args[0] + assert args[1] == balance + assert result.tx_hash == "0xabc123" + + def test_no_cap_when_balance_sufficient(self, mock_wallet, mock_web3): + """When balance >= approve_amount, no capping occurs.""" + balance = int(1.0 * 10**18) + approve_base = int(0.2 * 10**18) + contract = _setup_allowance(mock_web3, 0, balance=balance) + _setup_approval_mocks(mock_web3, mock_wallet, contract) + + contract.functions.allowance.return_value.call.side_effect = [0, 0, approve_base] + + result = ensure_opg_allowance(mock_wallet, min_allowance=0.1) + + args = contract.functions.approve.call_args[0] + assert args[1] == approve_base + + class TestEnsureOpgAllowanceValidation: """Input validation.""" @@ -316,7 +193,7 @@ def test_fractional_amount(self, mock_wallet, mock_web3): expected_base = int(0.5 * 10**18) _setup_allowance(mock_web3, expected_base) - result = approve_opg(mock_wallet, 0.5) + result = ensure_opg_allowance(mock_wallet, min_allowance=0.5) assert result.allowance_before == expected_base assert result.tx_hash is None @@ -326,7 +203,7 @@ def test_large_amount(self, mock_wallet, mock_web3): expected_base = int(1000.0 * 10**18) _setup_allowance(mock_web3, expected_base) - result = approve_opg(mock_wallet, 1000.0) + result = ensure_opg_allowance(mock_wallet, min_allowance=1000.0) assert result.allowance_before == expected_base assert result.tx_hash is None From 2c42b02425fee934d2d5aac5a14f2eb812143300 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:21:34 -0400 Subject: [PATCH 14/24] sanity check --- src/opengradient/client/opg_token.py | 5 ++++- tests/llm_test.py | 4 ++-- tests/opg_token_test.py | 8 ++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 86515231..1ccc3339 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -182,7 +182,6 @@ def ensure_opg_allowance( """ if approve_amount is None: approve_amount = min_allowance * 2 - if approve_amount < min_allowance: raise ValueError(f"approve_amount ({approve_amount}) must be >= min_allowance ({min_allowance})") @@ -201,6 +200,10 @@ def ensure_opg_allowance( ) balance = token.functions.balanceOf(owner).call() + if balance == 0: + raise ValueError( + f"Wallet {owner} has no OPG tokens. Fund the wallet before approving." + ) if approve_base > balance: logger.warning( "Requested approve_amount (%.6f OPG) exceeds wallet balance (%.6f OPG), capping approval to wallet balance", diff --git a/tests/llm_test.py b/tests/llm_test.py index 0dd08aa9..8b49fa57 100644 --- a/tests/llm_test.py +++ b/tests/llm_test.py @@ -13,8 +13,8 @@ import httpx import pytest -from src.opengradient.client.llm import LLM -from src.opengradient.types import TEE_LLM, x402SettlementMode +from opengradient.client.llm import LLM +from opengradient.types import TEE_LLM, x402SettlementMode # ── Fake HTTP transport ────────────────────────────────────────────── diff --git a/tests/opg_token_test.py b/tests/opg_token_test.py index 2e7fd88a..66b8e65f 100644 --- a/tests/opg_token_test.py +++ b/tests/opg_token_test.py @@ -1,3 +1,4 @@ +from types import SimpleNamespace from unittest.mock import MagicMock import pytest @@ -159,6 +160,13 @@ def test_approve_amount_capped_to_balance(self, mock_wallet, mock_web3): assert args[1] == balance assert result.tx_hash == "0xabc123" + def test_zero_balance_raises(self, mock_wallet, mock_web3): + """When balance is 0, raises ValueError.""" + _setup_allowance(mock_web3, 0, balance=0) + + with pytest.raises(ValueError, match="has no OPG tokens"): + ensure_opg_allowance(mock_wallet, min_allowance=0.1) + def test_no_cap_when_balance_sufficient(self, mock_wallet, mock_web3): """When balance >= approve_amount, no capping occurs.""" balance = int(1.0 * 10**18) From c3bd22577db742047251fa9743b7f9a72680ffce Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:22:38 -0400 Subject: [PATCH 15/24] sanity check --- src/opengradient/client/opg_token.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 1ccc3339..0c17f5e4 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -177,7 +177,8 @@ def ensure_opg_allowance( was needed). Raises: - ValueError: If ``approve_amount`` is less than ``min_allowance``. + ValueError: If ``approve_amount`` is less than ``min_allowance``, + or if the wallet has zero OPG balance. RuntimeError: If the approval transaction fails. """ if approve_amount is None: From 574e8588c5232a4da068ff0f6b88f6a2be0e87e3 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:27:27 -0400 Subject: [PATCH 16/24] checks --- src/opengradient/client/opg_token.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index 0c17f5e4..ce0308b1 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -186,14 +186,13 @@ def ensure_opg_allowance( if approve_amount < min_allowance: raise ValueError(f"approve_amount ({approve_amount}) must be >= min_allowance ({min_allowance})") - min_base = int(min_allowance * 10**18) - approve_base = int(approve_amount * 10**18) - w3, token, spender = _get_web3_and_contract() owner = Web3.to_checksum_address(wallet_account.address) - allowance_before = token.functions.allowance(owner, spender).call() + min_base = int(min_allowance * 10**18) + approve_base = int(approve_amount * 10**18) + if allowance_before >= min_base: return Permit2ApprovalResult( allowance_before=allowance_before, @@ -202,10 +201,13 @@ def ensure_opg_allowance( balance = token.functions.balanceOf(owner).call() if balance == 0: + raise ValueError(f"Wallet {owner} has no OPG tokens. Fund the wallet before approving.") + elif min_base > balance: raise ValueError( - f"Wallet {owner} has no OPG tokens. Fund the wallet before approving." + f"Wallet {owner} has less OPG tokens than the minimum allowance ({min_base} < {balance}). " + f"Fund the wallet with at least {min_base / 10**18} OPG before approving." ) - if approve_base > balance: + elif approve_base > balance: logger.warning( "Requested approve_amount (%.6f OPG) exceeds wallet balance (%.6f OPG), capping approval to wallet balance", approve_amount, From 49472fcd671960d5b48b46c7be515e5ce68c59e5 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:28:19 -0400 Subject: [PATCH 17/24] test --- tests/llm_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/llm_test.py b/tests/llm_test.py index 8b49fa57..8e5aba28 100644 --- a/tests/llm_test.py +++ b/tests/llm_test.py @@ -107,11 +107,11 @@ async def aread(self) -> bytes: # so LLM.__init__ runs its real code but gets our FakeHTTPClient. _PATCHES = { - "x402_httpx": "src.opengradient.client.tee_connection.x402HttpxClient", - "x402_client": "src.opengradient.client.llm.x402Client", - "signer": "src.opengradient.client.llm.EthAccountSigner", - "register_exact": "src.opengradient.client.llm.register_exact_evm_client", - "register_upto": "src.opengradient.client.llm.register_upto_evm_client", + "x402_httpx": "opengradient.client.tee_connection.x402HttpxClient", + "x402_client": "opengradient.client.llm.x402Client", + "signer": "opengradient.client.llm.EthAccountSigner", + "register_exact": "opengradient.client.llm.register_exact_evm_client", + "register_upto": "opengradient.client.llm.register_upto_evm_client", } From f3e415d7073936f10c5f6b02c518dcdca53427ae Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:29:17 -0400 Subject: [PATCH 18/24] test --- src/opengradient/client/llm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index ec053231..04068b91 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -191,7 +191,7 @@ def ensure_opg_allowance( Only sends a transaction when the current allowance drops below ``min_allowance``. When approval is needed, approves ``approve_amount`` - (defaults to ``10 * min_allowance``) to create a buffer that survives + (defaults to ``2 * min_allowance``) to create a buffer that survives multiple service restarts without re-approving. Best for backend servers that call this on startup:: @@ -202,7 +202,7 @@ def ensure_opg_allowance( min_allowance: The minimum acceptable allowance in OPG. Must be at least 0.1 OPG. approve_amount: The amount of OPG to approve when a transaction - is needed. Defaults to ``10 * min_allowance``. Must be + is needed. Defaults to ``2 * min_allowance``. Must be >= ``min_allowance``. Returns: From 8ec4ddc247a84c0f74660d91f72963b351587c11 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Thu, 26 Mar 2026 23:29:32 -0400 Subject: [PATCH 19/24] test --- docs/opengradient/client/llm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index 6e611b51..bd68e815 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -201,7 +201,7 @@ Ensure the Permit2 allowance stays above a minimum threshold. Only sends a transaction when the current allowance drops below ``min_allowance``. When approval is needed, approves ``approve_amount`` -(defaults to ``10 * min_allowance``) to create a buffer that survives +(defaults to ``2 * min_allowance``) to create a buffer that survives multiple service restarts without re-approving. Best for backend servers that call this on startup:: @@ -213,7 +213,7 @@ Best for backend servers that call this on startup:: * **`min_allowance`**: The minimum acceptable allowance in OPG. Must be at least 0.1 OPG. * **`approve_amount`**: The amount of OPG to approve when a transaction - is needed. Defaults to ``10 * min_allowance``. Must be + is needed. Defaults to ``2 * min_allowance``. Must be >= ``min_allowance``. **Returns** From 78c86d712d18360ea306426d40c7cdfbd08b7f9a Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Fri, 27 Mar 2026 11:51:12 -0400 Subject: [PATCH 20/24] revert to old name --- README.md | 2 +- docs/CLAUDE_SDK_USERS.md | 2 +- docs/opengradient/client/index.md | 2 +- docs/opengradient/client/llm.md | 8 ++++---- docs/opengradient/index.md | 2 +- examples/langchain_react_agent.py | 2 +- examples/llm_chat.py | 2 +- examples/llm_chat_streaming.py | 2 +- examples/llm_tool_calling.py | 2 +- integrationtest/llm/test_llm_chat.py | 2 +- src/opengradient/__init__.py | 2 +- src/opengradient/client/__init__.py | 2 +- src/opengradient/client/llm.py | 12 +++++------ src/opengradient/client/opg_token.py | 4 ++-- stresstest/llm.py | 2 +- tests/opg_token_test.py | 26 ++++++++++++------------ tutorials/01-verifiable-ai-agent.md | 6 +++--- tutorials/02-streaming-multi-provider.md | 6 +++--- tutorials/03-verified-tool-calling.md | 6 +++--- 19 files changed, 46 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index a889b3e1..0892a0a3 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ hub = og.ModelHub(email="you@example.com", password="...") Before making LLM requests, your wallet must approve OPG token spending via the [Permit2](https://github.com/Uniswap/permit2) protocol. This only sends an on-chain transaction when the current allowance drops below the threshold: ```python -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) ``` See [Payment Settlement](#payment-settlement) for details on settlement modes. diff --git a/docs/CLAUDE_SDK_USERS.md b/docs/CLAUDE_SDK_USERS.md index c45a34bb..1619c681 100644 --- a/docs/CLAUDE_SDK_USERS.md +++ b/docs/CLAUDE_SDK_USERS.md @@ -22,7 +22,7 @@ import os llm = og.LLM(private_key=os.environ["OG_PRIVATE_KEY"]) # Ensure sufficient OPG allowance (only sends tx when below threshold) -llm.ensure_opg_allowance(min_allowance=0.1) +llm.ensure_opg_approval(min_allowance=0.1) # LLM Chat (TEE-verified with x402 payments, async) result = await llm.chat( diff --git a/docs/opengradient/client/index.md b/docs/opengradient/client/index.md index e81a524b..0a9397f3 100644 --- a/docs/opengradient/client/index.md +++ b/docs/opengradient/client/index.md @@ -24,7 +24,7 @@ import opengradient as og # LLM inference (Base Sepolia OPG tokens) llm = og.LLM(private_key="0x...") -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) result = await llm.chat(model=og.TEE_LLM.CLAUDE_HAIKU_4_5, messages=[...]) # On-chain model inference (OpenGradient testnet gas tokens) diff --git a/docs/opengradient/client/llm.md b/docs/opengradient/client/llm.md index bd68e815..2dd07e0b 100644 --- a/docs/opengradient/client/llm.md +++ b/docs/opengradient/client/llm.md @@ -21,7 +21,7 @@ Supports both streaming and non-streaming responses. All request methods (``chat``, ``completion``) are async. Before making LLM requests, ensure your wallet has approved sufficient -OPG tokens for Permit2 spending by calling ``ensure_opg_allowance``. +OPG tokens for Permit2 spending by calling ``ensure_opg_approval``. #### Constructor @@ -188,10 +188,10 @@ TextGenerationOutput: Generated text results including: --- -#### `ensure_opg_allowance()` +#### `ensure_opg_approval()` ```python -def ensure_opg_allowance( +def ensure_opg_approval( self, min_allowance: float, approve_amount: Optional[float] = None @@ -206,7 +206,7 @@ multiple service restarts without re-approving. Best for backend servers that call this on startup:: - llm.ensure_opg_allowance(min_allowance=5.0, approve_amount=100.0) + llm.ensure_opg_approval(min_allowance=5.0, approve_amount=100.0) **Arguments** diff --git a/docs/opengradient/index.md b/docs/opengradient/index.md index b61194f5..c9b01fda 100644 --- a/docs/opengradient/index.md +++ b/docs/opengradient/index.md @@ -35,7 +35,7 @@ import opengradient as og llm = og.LLM(private_key="0x...") # One-time OPG token approval (idempotent -- skips if allowance is sufficient) -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) # Chat with an LLM (TEE-verified) response = asyncio.run(llm.chat( diff --git a/examples/langchain_react_agent.py b/examples/langchain_react_agent.py index aec95696..7d372cae 100644 --- a/examples/langchain_react_agent.py +++ b/examples/langchain_react_agent.py @@ -19,7 +19,7 @@ # Ensure sufficient OPG allowance for Permit2 spending llm_client = og.LLM(private_key=private_key) -llm_client.ensure_opg_allowance(min_allowance=5) +llm_client.ensure_opg_approval(min_allowance=5) # Create the OpenGradient LangChain adapter llm = og.agents.langchain_adapter( diff --git a/examples/llm_chat.py b/examples/llm_chat.py index 34d0edd6..68bf19e2 100644 --- a/examples/llm_chat.py +++ b/examples/llm_chat.py @@ -10,7 +10,7 @@ async def main(): llm = og.LLM(private_key=os.environ.get("OG_PRIVATE_KEY")) - llm.ensure_opg_allowance(min_allowance=0.1) + llm.ensure_opg_approval(min_allowance=0.1) messages = [ {"role": "user", "content": "What is the capital of France?"}, diff --git a/examples/llm_chat_streaming.py b/examples/llm_chat_streaming.py index 91811f18..d6cb6023 100644 --- a/examples/llm_chat_streaming.py +++ b/examples/llm_chat_streaming.py @@ -6,7 +6,7 @@ async def main(): llm = og.LLM(private_key=os.environ.get("OG_PRIVATE_KEY")) - llm.ensure_opg_allowance(min_allowance=0.1) + llm.ensure_opg_approval(min_allowance=0.1) messages = [ {"role": "user", "content": "What is Python?"}, diff --git a/examples/llm_tool_calling.py b/examples/llm_tool_calling.py index 6c2a985f..b7632983 100644 --- a/examples/llm_tool_calling.py +++ b/examples/llm_tool_calling.py @@ -51,7 +51,7 @@ async def main(): ] # Ensure sufficient OPG allowance for Permit2 spending - llm.ensure_opg_allowance(min_allowance=0.1) + llm.ensure_opg_approval(min_allowance=0.1) print("Testing Gemini tool calls...") print(f"Model: {og.TEE_LLM.GEMINI_2_5_FLASH_LITE}") diff --git a/integrationtest/llm/test_llm_chat.py b/integrationtest/llm/test_llm_chat.py index f6124063..8130868d 100644 --- a/integrationtest/llm/test_llm_chat.py +++ b/integrationtest/llm/test_llm_chat.py @@ -99,7 +99,7 @@ def llm_client(): print("Account funded with ETH and OPG") llm = og.LLM(private_key=account.key.hex()) - llm.ensure_opg_allowance(min_allowance=OPG_FUND_AMOUNT, approve_amount=OPG_FUND_AMOUNT) + llm.ensure_opg_approval(min_allowance=OPG_FUND_AMOUNT, approve_amount=OPG_FUND_AMOUNT) print("Permit2 approval complete") # Wait for the approval to propagate on-chain diff --git a/src/opengradient/__init__.py b/src/opengradient/__init__.py index b74a1b66..89d5ff08 100644 --- a/src/opengradient/__init__.py +++ b/src/opengradient/__init__.py @@ -26,7 +26,7 @@ llm = og.LLM(private_key="0x...") # One-time OPG token approval (idempotent -- skips if allowance is sufficient) -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) # Chat with an LLM (TEE-verified) response = asyncio.run(llm.chat( diff --git a/src/opengradient/client/__init__.py b/src/opengradient/client/__init__.py index 59e98242..7206d385 100644 --- a/src/opengradient/client/__init__.py +++ b/src/opengradient/client/__init__.py @@ -17,7 +17,7 @@ # LLM inference (Base Sepolia OPG tokens) llm = og.LLM(private_key="0x...") -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) result = await llm.chat(model=og.TEE_LLM.CLAUDE_HAIKU_4_5, messages=[...]) # On-chain model inference (OpenGradient testnet gas tokens) diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index 04068b91..ed54fd99 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -15,7 +15,7 @@ from x402.mechanisms.evm.upto.register import register_upto_evm_client from ..types import TEE_LLM, StreamChoice, StreamChunk, StreamDelta, TextGenerationOutput, x402SettlementMode -from .opg_token import Permit2ApprovalResult, ensure_opg_allowance +from .opg_token import Permit2ApprovalResult, ensure_opg_approval from .tee_connection import RegistryTEEConnection, StaticTEEConnection, TEEConnectionInterface from .tee_registry import TEERegistry @@ -58,7 +58,7 @@ class LLM: All request methods (``chat``, ``completion``) are async. Before making LLM requests, ensure your wallet has approved sufficient - OPG tokens for Permit2 spending by calling ``ensure_opg_allowance``. + OPG tokens for Permit2 spending by calling ``ensure_opg_approval``. Usage: # Via on-chain registry (default) @@ -68,7 +68,7 @@ class LLM: llm = og.LLM.from_url(private_key="0x...", llm_server_url="https://1.2.3.4") # Ensure sufficient OPG allowance (only sends tx when below threshold) - llm.ensure_opg_allowance(min_allowance=5) + llm.ensure_opg_approval(min_allowance=5) result = await llm.chat(model=TEE_LLM.CLAUDE_HAIKU_4_5, messages=[...]) result = await llm.completion(model=TEE_LLM.CLAUDE_HAIKU_4_5, prompt="Hello") @@ -182,7 +182,7 @@ async def _call_with_tee_retry( # ── Public API ────────────────────────────────────────────────────── - def ensure_opg_allowance( + def ensure_opg_approval( self, min_allowance: float, approve_amount: Optional[float] = None, @@ -196,7 +196,7 @@ def ensure_opg_allowance( Best for backend servers that call this on startup:: - llm.ensure_opg_allowance(min_allowance=5.0, approve_amount=100.0) + llm.ensure_opg_approval(min_allowance=5.0, approve_amount=100.0) Args: min_allowance: The minimum acceptable allowance in OPG. Must be @@ -217,7 +217,7 @@ def ensure_opg_allowance( """ if min_allowance < 0.1: raise ValueError("min_allowance must be at least 0.1.") - return ensure_opg_allowance(self._wallet_account, min_allowance, approve_amount) + return ensure_opg_approval(self._wallet_account, min_allowance, approve_amount) async def completion( self, diff --git a/src/opengradient/client/opg_token.py b/src/opengradient/client/opg_token.py index ce0308b1..efcca7be 100644 --- a/src/opengradient/client/opg_token.py +++ b/src/opengradient/client/opg_token.py @@ -144,7 +144,7 @@ def _get_web3_and_contract(): return w3, token, spender -def ensure_opg_allowance( +def ensure_opg_approval( wallet_account: LocalAccount, min_allowance: float, approve_amount: Optional[float] = None, @@ -160,7 +160,7 @@ def ensure_opg_allowance( # On startup — only sends a tx when allowance < 5 OPG, # then approves 100 OPG so subsequent restarts are free. - result = ensure_opg_allowance(wallet, min_allowance=5.0, approve_amount=100.0) + result = ensure_opg_approval(wallet, min_allowance=5.0, approve_amount=100.0) Args: wallet_account: The wallet account to check and approve from. diff --git a/stresstest/llm.py b/stresstest/llm.py index 45430b83..a234cee5 100644 --- a/stresstest/llm.py +++ b/stresstest/llm.py @@ -13,7 +13,7 @@ async def main(private_key: str): llm = og.LLM(private_key=private_key) - llm.ensure_opg_allowance(min_allowance=0.1) + llm.ensure_opg_approval(min_allowance=0.1) async def run_prompt(prompt: str): messages = [{"role": "user", "content": prompt}] diff --git a/tests/opg_token_test.py b/tests/opg_token_test.py index 66b8e65f..8558cd6b 100644 --- a/tests/opg_token_test.py +++ b/tests/opg_token_test.py @@ -4,7 +4,7 @@ import pytest from opengradient.client.opg_token import ( - ensure_opg_allowance, + ensure_opg_approval, ) OWNER_ADDRESS = "0x1234567890abcdef1234567890ABCDEF12345678" @@ -70,7 +70,7 @@ def _setup_approval_mocks(mock_web3, mock_wallet, contract): return approve_fn, tx_hash -# ── ensure_opg_allowance tests ────────────────────────────────────── +# ── ensure_opg_approval tests ────────────────────────────────────── class TestEnsureOpgAllowanceSkips: @@ -81,7 +81,7 @@ def test_above_minimum_skips(self, mock_wallet, mock_web3): min_base = int(5.0 * 10**18) _setup_allowance(mock_web3, min_base * 2) - result = ensure_opg_allowance(mock_wallet, min_allowance=5.0) + result = ensure_opg_approval(mock_wallet, min_allowance=5.0) assert result.tx_hash is None assert result.allowance_before == min_base * 2 @@ -91,7 +91,7 @@ def test_exact_minimum_skips(self, mock_wallet, mock_web3): min_base = int(5.0 * 10**18) _setup_allowance(mock_web3, min_base) - result = ensure_opg_allowance(mock_wallet, min_allowance=5.0) + result = ensure_opg_approval(mock_wallet, min_allowance=5.0) assert result.tx_hash is None @@ -107,7 +107,7 @@ def test_approves_default_with_greater_amount(self, mock_wallet, mock_web3): approve_base = int(10.0 * 10**18) # 2x of 5.0 contract.functions.allowance.return_value.call.side_effect = [0, 0, approve_base] - result = ensure_opg_allowance(mock_wallet, min_allowance=5.0) + result = ensure_opg_approval(mock_wallet, min_allowance=5.0) assert result.tx_hash == "0xabc123" # Verify approve was called with 10x amount @@ -122,7 +122,7 @@ def test_approves_custom_amount(self, mock_wallet, mock_web3): approve_base = int(100.0 * 10**18) contract.functions.allowance.return_value.call.side_effect = [0, 0, approve_base] - result = ensure_opg_allowance(mock_wallet, min_allowance=5.0, approve_amount=100.0) + result = ensure_opg_approval(mock_wallet, min_allowance=5.0, approve_amount=100.0) assert result.tx_hash == "0xabc123" args = contract.functions.approve.call_args[0] @@ -135,7 +135,7 @@ def test_no_tx_on_restart_when_above_min(self, mock_wallet, mock_web3): remaining = int(60.0 * 10**18) _setup_allowance(mock_web3, remaining) - result = ensure_opg_allowance(mock_wallet, min_allowance=5.0, approve_amount=100.0) + result = ensure_opg_approval(mock_wallet, min_allowance=5.0, approve_amount=100.0) assert result.tx_hash is None assert result.allowance_before == remaining @@ -153,7 +153,7 @@ def test_approve_amount_capped_to_balance(self, mock_wallet, mock_web3): # allowance calls: 1st for the check, 2nd in _send_approve_tx before, 3rd in post-tx poll contract.functions.allowance.return_value.call.side_effect = [0, 0, balance] - result = ensure_opg_allowance(mock_wallet, min_allowance=0.1) + result = ensure_opg_approval(mock_wallet, min_allowance=0.1) # Default approve_amount would be 0.2, but balance is only 0.1 — capped args = contract.functions.approve.call_args[0] @@ -165,7 +165,7 @@ def test_zero_balance_raises(self, mock_wallet, mock_web3): _setup_allowance(mock_web3, 0, balance=0) with pytest.raises(ValueError, match="has no OPG tokens"): - ensure_opg_allowance(mock_wallet, min_allowance=0.1) + ensure_opg_approval(mock_wallet, min_allowance=0.1) def test_no_cap_when_balance_sufficient(self, mock_wallet, mock_web3): """When balance >= approve_amount, no capping occurs.""" @@ -176,7 +176,7 @@ def test_no_cap_when_balance_sufficient(self, mock_wallet, mock_web3): contract.functions.allowance.return_value.call.side_effect = [0, 0, approve_base] - result = ensure_opg_allowance(mock_wallet, min_allowance=0.1) + result = ensure_opg_approval(mock_wallet, min_allowance=0.1) args = contract.functions.approve.call_args[0] assert args[1] == approve_base @@ -190,7 +190,7 @@ def test_approve_amount_less_than_min_raises(self, mock_wallet, mock_web3): _setup_allowance(mock_web3, 0) with pytest.raises(ValueError, match="approve_amount.*must be >= min_allowance"): - ensure_opg_allowance(mock_wallet, min_allowance=10.0, approve_amount=5.0) + ensure_opg_approval(mock_wallet, min_allowance=10.0, approve_amount=5.0) class TestAmountConversion: @@ -201,7 +201,7 @@ def test_fractional_amount(self, mock_wallet, mock_web3): expected_base = int(0.5 * 10**18) _setup_allowance(mock_web3, expected_base) - result = ensure_opg_allowance(mock_wallet, min_allowance=0.5) + result = ensure_opg_approval(mock_wallet, min_allowance=0.5) assert result.allowance_before == expected_base assert result.tx_hash is None @@ -211,7 +211,7 @@ def test_large_amount(self, mock_wallet, mock_web3): expected_base = int(1000.0 * 10**18) _setup_allowance(mock_web3, expected_base) - result = ensure_opg_allowance(mock_wallet, min_allowance=1000.0) + result = ensure_opg_approval(mock_wallet, min_allowance=1000.0) assert result.allowance_before == expected_base assert result.tx_hash is None diff --git a/tutorials/01-verifiable-ai-agent.md b/tutorials/01-verifiable-ai-agent.md index 9eea4363..b7737a86 100644 --- a/tutorials/01-verifiable-ai-agent.md +++ b/tutorials/01-verifiable-ai-agent.md @@ -36,7 +36,7 @@ export OG_PRIVATE_KEY="0x..." ## Step 1: Initialize and Create the LangChain Adapter Before making any LLM calls, you need to approve OPG token spending for the x402 -payment protocol. The `ensure_opg_allowance` method checks your wallet's current +payment protocol. The `ensure_opg_approval` method checks your wallet's current Permit2 allowance and only sends an on-chain transaction if the allowance drops below the threshold -- so it's safe to call every time. @@ -48,7 +48,7 @@ private_key = os.environ["OG_PRIVATE_KEY"] # Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). llm_client = og.LLM(private_key=private_key) -llm_client.ensure_opg_allowance(min_allowance=5) +llm_client.ensure_opg_approval(min_allowance=5) # Create the LangChain chat model backed by OpenGradient TEE. # The adapter creates its own internal LLM client. The approval above applies @@ -289,7 +289,7 @@ private_key = os.environ["OG_PRIVATE_KEY"] # Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). llm_client = og.LLM(private_key=private_key) -llm_client.ensure_opg_allowance(min_allowance=5) +llm_client.ensure_opg_approval(min_allowance=5) # Alpha client for on-chain model inference. alpha = og.Alpha(private_key=private_key) diff --git a/tutorials/02-streaming-multi-provider.md b/tutorials/02-streaming-multi-provider.md index 60f9b57c..e941137b 100644 --- a/tutorials/02-streaming-multi-provider.md +++ b/tutorials/02-streaming-multi-provider.md @@ -36,7 +36,7 @@ export OG_PRIVATE_KEY="0x..." Start with the simplest possible call -- send a message and get a response. Before making any LLM calls, ensure sufficient OPG token allowance for the x402 payment -protocol using `ensure_opg_allowance`. This only sends a transaction when the +protocol using `ensure_opg_approval`. This only sends a transaction when the current allowance drops below the threshold. ```python @@ -53,7 +53,7 @@ if not private_key: llm = og.LLM(private_key=private_key) # Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) async def main(): result = await llm.chat( @@ -274,7 +274,7 @@ if not private_key: llm = og.LLM(private_key=private_key) # Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) PROMPT = "Explain what a Trusted Execution Environment is in two sentences." diff --git a/tutorials/03-verified-tool-calling.md b/tutorials/03-verified-tool-calling.md index 99f58594..8ddead38 100644 --- a/tutorials/03-verified-tool-calling.md +++ b/tutorials/03-verified-tool-calling.md @@ -36,7 +36,7 @@ export OG_PRIVATE_KEY="0x..." ## Step 1: Initialize the Client Before making any LLM calls, ensure sufficient OPG token allowance for the x402 -payment protocol. The `ensure_opg_allowance` method only sends a transaction when +payment protocol. The `ensure_opg_approval` method only sends a transaction when the current allowance drops below the threshold. ```python @@ -54,7 +54,7 @@ if not private_key: llm = og.LLM(private_key=private_key) # Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) ``` ## Step 2: Define Local Tool Implementations @@ -317,7 +317,7 @@ if not private_key: llm = og.LLM(private_key=private_key) # Ensure sufficient OPG allowance for x402 payments (only sends tx when below threshold). -llm.ensure_opg_allowance(min_allowance=5) +llm.ensure_opg_approval(min_allowance=5) # ── Mock data ───────────────────────────────────────────────────────────── PORTFOLIO = {"ETH": {"amount": 5.0, "avg_cost": 1950.00}, From c8b2f1a2b3f39850889bdf2e220f7cbbc3cdede5 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Fri, 27 Mar 2026 11:55:03 -0400 Subject: [PATCH 21/24] wait for opg transfer --- integrationtest/llm/test_llm_chat.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/integrationtest/llm/test_llm_chat.py b/integrationtest/llm/test_llm_chat.py index 8130868d..6235f9cc 100644 --- a/integrationtest/llm/test_llm_chat.py +++ b/integrationtest/llm/test_llm_chat.py @@ -76,7 +76,7 @@ def _fund_account(funder_key: str, recipient_address: str): if opg_receipt.status != 1: raise RuntimeError(f"OPG transfer failed: {opg_hash.hex()}") - # Wait for the recipient balance to be visible on the RPC node + # Wait for the recipient balances to be visible on the RPC node for _ in range(5): if w3.eth.get_balance(recipient) > 0: break @@ -84,6 +84,13 @@ def _fund_account(funder_key: str, recipient_address: str): else: raise RuntimeError("Recipient ETH balance is still 0 after funding") + for _ in range(10): + if token.functions.balanceOf(recipient).call() > 0: + break + time.sleep(1) + else: + raise RuntimeError("Recipient OPG token balance is still 0 after funding") + @pytest.fixture(scope="module") def llm_client(): From 549e179d2183e9574ee00fa754eafb75d0d72108 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Fri, 27 Mar 2026 11:56:21 -0400 Subject: [PATCH 22/24] abi --- integrationtest/llm/test_llm_chat.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/integrationtest/llm/test_llm_chat.py b/integrationtest/llm/test_llm_chat.py index 6235f9cc..1241e016 100644 --- a/integrationtest/llm/test_llm_chat.py +++ b/integrationtest/llm/test_llm_chat.py @@ -20,6 +20,13 @@ "stateMutability": "nonpayable", "type": "function", }, + { + "inputs": [{"name": "account", "type": "address"}], + "name": "balanceOf", + "outputs": [{"name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, ] # Amount of OPG tokens to fund the test account with From b278e6a32784e8f39ea20b280a2db6c660b3ec75 Mon Sep 17 00:00:00 2001 From: kukac Date: Fri, 27 Mar 2026 12:04:45 -0400 Subject: [PATCH 23/24] Update tests/opg_token_test.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: kukac --- tests/opg_token_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/opg_token_test.py b/tests/opg_token_test.py index 8558cd6b..b237ccd5 100644 --- a/tests/opg_token_test.py +++ b/tests/opg_token_test.py @@ -110,7 +110,7 @@ def test_approves_default_with_greater_amount(self, mock_wallet, mock_web3): result = ensure_opg_approval(mock_wallet, min_allowance=5.0) assert result.tx_hash == "0xabc123" - # Verify approve was called with 10x amount + # Verify approve was called with 2x min_allowance args = contract.functions.approve.call_args[0] assert args[1] == approve_base From 514fa14336d57ee5e533ce127fad87414b79b3e7 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Fri, 27 Mar 2026 12:13:20 -0400 Subject: [PATCH 24/24] no src import --- tests/client_test.py | 24 ++++++++++-------------- tests/langchain_adapter_test.py | 10 +++------- tests/tee_connection_test.py | 32 ++++++++++++++++---------------- tests/tee_registry_test.py | 10 +++------- 4 files changed, 32 insertions(+), 44 deletions(-) diff --git a/tests/client_test.py b/tests/client_test.py index d7d70f8a..6829fc98 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -1,15 +1,11 @@ import json -import os -import sys from unittest.mock import MagicMock, mock_open, patch import pytest -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) - -from src.opengradient.client.llm import LLM -from src.opengradient.client.model_hub import ModelHub -from src.opengradient.types import ( +from opengradient.client.llm import LLM +from opengradient.client.model_hub import ModelHub +from opengradient.types import ( StreamChunk, x402SettlementMode, ) @@ -23,9 +19,9 @@ def mock_tee_registry(): """Mock the TEE registry so LLM.__init__ doesn't need a live registry.""" with ( - patch("src.opengradient.client.llm.TEERegistry") as mock_tee_registry, + patch("opengradient.client.llm.TEERegistry") as mock_tee_registry, patch( - "src.opengradient.client.tee_connection.build_ssl_context_from_der", + "opengradient.client.tee_connection.build_ssl_context_from_der", return_value=MagicMock(), ), ): @@ -41,7 +37,7 @@ def mock_tee_registry(): @pytest.fixture def mock_web3(): """Create a mock Web3 instance for Alpha.""" - with patch("src.opengradient.client.alpha.Web3") as mock: + with patch("opengradient.client.alpha.Web3") as mock: mock_instance = MagicMock() mock.return_value = mock_instance mock.HTTPProvider.return_value = MagicMock() @@ -94,8 +90,8 @@ class TestAuthentication: def test_login_to_hub_success(self): """Test successful login to hub.""" with ( - patch("src.opengradient.client.model_hub._FIREBASE_CONFIG", {"apiKey": "fake"}), - patch("src.opengradient.client.model_hub.firebase") as mock_firebase, + patch("opengradient.client.model_hub._FIREBASE_CONFIG", {"apiKey": "fake"}), + patch("opengradient.client.model_hub.firebase") as mock_firebase, ): mock_auth = MagicMock() mock_auth.sign_in_with_email_and_password.return_value = { @@ -112,8 +108,8 @@ def test_login_to_hub_success(self): def test_login_to_hub_failure(self): """Test login failure raises exception.""" with ( - patch("src.opengradient.client.model_hub._FIREBASE_CONFIG", {"apiKey": "fake"}), - patch("src.opengradient.client.model_hub.firebase") as mock_firebase, + patch("opengradient.client.model_hub._FIREBASE_CONFIG", {"apiKey": "fake"}), + patch("opengradient.client.model_hub.firebase") as mock_firebase, ): mock_auth = MagicMock() mock_auth.sign_in_with_email_and_password.side_effect = Exception("Invalid credentials") diff --git a/tests/langchain_adapter_test.py b/tests/langchain_adapter_test.py index e651ab49..284fab69 100644 --- a/tests/langchain_adapter_test.py +++ b/tests/langchain_adapter_test.py @@ -1,6 +1,4 @@ import json -import os -import sys from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -8,16 +6,14 @@ from langchain_core.messages.tool import ToolMessage from langchain_core.tools import tool -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) - -from src.opengradient.agents.og_langchain import OpenGradientChatModel, _extract_content, _parse_tool_call -from src.opengradient.types import TEE_LLM, TextGenerationOutput, x402SettlementMode +from opengradient.agents.og_langchain import OpenGradientChatModel, _extract_content, _parse_tool_call +from opengradient.types import TEE_LLM, TextGenerationOutput, x402SettlementMode @pytest.fixture def mock_llm_client(): """Create a mock LLM instance.""" - with patch("src.opengradient.agents.og_langchain.LLM") as MockLLM: + with patch("opengradient.agents.og_langchain.LLM") as MockLLM: mock_instance = MagicMock() mock_instance.chat = AsyncMock() MockLLM.return_value = mock_instance diff --git a/tests/tee_connection_test.py b/tests/tee_connection_test.py index 3d5379fe..20d02f7c 100644 --- a/tests/tee_connection_test.py +++ b/tests/tee_connection_test.py @@ -15,11 +15,11 @@ from cryptography.x509.oid import NameOID from x402 import x402Client -from src.opengradient.client.tee_connection import ( +from opengradient.client.tee_connection import ( ActiveTEE, RegistryTEEConnection, ) -from src.opengradient.client.tee_registry import build_ssl_context_from_der +from opengradient.client.tee_registry import build_ssl_context_from_der # ── Helpers ────────────────────────────────────────────────────────── @@ -44,11 +44,11 @@ def _make_registry_connection(*, registry=None, http_factory=None): factory = http_factory or FakeHTTPClient with ( patch( - "src.opengradient.client.tee_connection.x402HttpxClient", + "opengradient.client.tee_connection.x402HttpxClient", side_effect=factory, ), patch( - "src.opengradient.client.tee_connection.build_ssl_context_from_der", + "opengradient.client.tee_connection.build_ssl_context_from_der", return_value=MagicMock(spec=ssl.SSLContext), ), ): @@ -122,7 +122,7 @@ async def test_resolve_none_raises(self): mock_reg.get_llm_tee.return_value = None with patch( - "src.opengradient.client.tee_connection.x402HttpxClient", + "opengradient.client.tee_connection.x402HttpxClient", side_effect=FakeHTTPClient, ): with pytest.raises(ValueError, match="No active LLM proxy TEE"): @@ -133,7 +133,7 @@ async def test_resolve_exception_wraps_in_runtime_error(self): mock_reg.get_llm_tee.side_effect = Exception("rpc down") with patch( - "src.opengradient.client.tee_connection.x402HttpxClient", + "opengradient.client.tee_connection.x402HttpxClient", side_effect=FakeHTTPClient, ): with pytest.raises(RuntimeError, match="Failed to fetch LLM TEE"): @@ -157,11 +157,11 @@ async def test_builds_ssl_context_from_der(self): with ( patch( - "src.opengradient.client.tee_connection.x402HttpxClient", + "opengradient.client.tee_connection.x402HttpxClient", side_effect=FakeHTTPClient, ), patch( - "src.opengradient.client.tee_connection.build_ssl_context_from_der", + "opengradient.client.tee_connection.build_ssl_context_from_der", return_value=mock_ssl_ctx, ) as mock_build, ): @@ -187,11 +187,11 @@ def make_client(*args, **kwargs): with ( patch( - "src.opengradient.client.tee_connection.x402HttpxClient", + "opengradient.client.tee_connection.x402HttpxClient", side_effect=make_client, ), patch( - "src.opengradient.client.tee_connection.build_ssl_context_from_der", + "opengradient.client.tee_connection.build_ssl_context_from_der", return_value=MagicMock(spec=ssl.SSLContext), ), ): @@ -211,7 +211,7 @@ async def test_reconnect_swallows_close_failure(self): conn.get().http_client.aclose = AsyncMock(side_effect=OSError("already closed")) with patch( - "src.opengradient.client.tee_connection.x402HttpxClient", + "opengradient.client.tee_connection.x402HttpxClient", side_effect=FakeHTTPClient, ): await conn.reconnect() # should not raise @@ -232,11 +232,11 @@ def slow_connect(self): with ( patch.object(RegistryTEEConnection, "_connect", slow_connect), patch( - "src.opengradient.client.tee_connection.build_ssl_context_from_der", + "opengradient.client.tee_connection.build_ssl_context_from_der", return_value=MagicMock(spec=ssl.SSLContext), ), patch( - "src.opengradient.client.tee_connection.x402HttpxClient", + "opengradient.client.tee_connection.x402HttpxClient", side_effect=FakeHTTPClient, ), ): @@ -287,7 +287,7 @@ async def test_refresh_loop_skips_when_tee_still_active(self): with patch.object(conn, "reconnect", new_callable=AsyncMock) as mock_reconnect: with patch( - "src.opengradient.client.tee_connection.asyncio.sleep", + "opengradient.client.tee_connection.asyncio.sleep", side_effect=[None, asyncio.CancelledError], ): with pytest.raises(asyncio.CancelledError): @@ -305,7 +305,7 @@ async def test_refresh_loop_reconnects_when_tee_gone(self): with patch.object(conn, "reconnect", new_callable=AsyncMock) as mock_reconnect: with patch( - "src.opengradient.client.tee_connection.asyncio.sleep", + "opengradient.client.tee_connection.asyncio.sleep", side_effect=[None, asyncio.CancelledError], ): with pytest.raises(asyncio.CancelledError): @@ -323,7 +323,7 @@ async def test_refresh_loop_survives_registry_error(self): conn = _make_registry_connection(registry=mock_reg) with patch( - "src.opengradient.client.tee_connection.asyncio.sleep", + "opengradient.client.tee_connection.asyncio.sleep", side_effect=[None, None], ): with pytest.raises(asyncio.CancelledError): diff --git a/tests/tee_registry_test.py b/tests/tee_registry_test.py index 189ad26c..faa14205 100644 --- a/tests/tee_registry_test.py +++ b/tests/tee_registry_test.py @@ -1,13 +1,9 @@ -import os import ssl -import sys from unittest.mock import MagicMock, patch import pytest -sys.path.append(os.path.join(os.path.dirname(__file__), "..")) - -from src.opengradient.client.tee_registry import ( +from opengradient.client.tee_registry import ( TEE_TYPE_LLM_PROXY, TEE_TYPE_VALIDATOR, TEERegistry, @@ -69,8 +65,8 @@ def _make_self_signed_der() -> bytes: def mock_contract(): """Create a TEERegistry with a mocked Web3 contract.""" with ( - patch("src.opengradient.client.tee_registry.Web3") as mock_web3_cls, - patch("src.opengradient.client.tee_registry.get_abi") as mock_get_abi, + patch("opengradient.client.tee_registry.Web3") as mock_web3_cls, + patch("opengradient.client.tee_registry.get_abi") as mock_get_abi, ): mock_get_abi.return_value = [] mock_web3 = MagicMock()