diff --git a/Makefile b/Makefile index 6eea7f5..465e0d3 100644 --- a/Makefile +++ b/Makefile @@ -68,11 +68,17 @@ completion: --prompt "Hello, how are you?" \ --max-tokens 50 -chat: +chat-og-evm: python -m opengradient.cli chat \ - --model $(MODEL) --mode TEE \ + --model $(MODEL) \ + --messages '[{"role":"user","content":"Tell me a fun fact"}]' \ + --max-tokens 150 --network og-evm + +chat-base-testnet: + python -m opengradient.cli chat \ + --model $(MODEL) \ --messages '[{"role":"user","content":"Tell me a fun fact"}]' \ - --max-tokens 150 + --max-tokens 150 --network base-testnet chat-stream: python -m opengradient.cli chat \ diff --git a/examples/README.md b/examples/README.md index 39ef8f6..29e91d5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,6 +48,18 @@ python examples/upload_model.py ## x402 LLM Examples +#### `x402_permit2.py` +Grants Permit2 approval so x402 payments can spend OPG on Base Sepolia. + +```bash +python examples/x402_permit2.py +``` + +**What it does:** +- Sends an `approve(PERMIT2, amount)` transaction for OPG +- Uses `OG_PRIVATE_KEY` to sign and submit the transaction +- Prints allowance before and after approval + #### `run_x402_llm.py` Runs LLM inference with x402 transaction processing. @@ -187,4 +199,3 @@ Browse available models on the [OpenGradient Model Hub](https://hub.opengradient - Run `opengradient --help` for CLI command reference - Visit our [documentation](https://docs.opengradient.ai/) for detailed guides - Check the main [README](../README.md) for SDK overview - diff --git a/examples/run_x402_llm.py b/examples/run_x402_llm.py index 8926de2..97affc7 100644 --- a/examples/run_x402_llm.py +++ b/examples/run_x402_llm.py @@ -12,11 +12,16 @@ import os import opengradient as og +from x402_permit2 import check_permit2_approval + +network = "base-testnet" client = og.Client( private_key=os.environ.get("OG_PRIVATE_KEY"), ) +check_permit2_approval(client.wallet_address, network) + messages = [ {"role": "user", "content": "What is Python?"}, {"role": "assistant", "content": "Python is a high-level programming language."}, @@ -27,6 +32,6 @@ model=og.TEE_LLM.GPT_4_1_2025_04_14, messages=messages, x402_settlement_mode=og.x402SettlementMode.SETTLE_METADATA, + network=network ) -print(f"Response: {result.chat_output['content']}") -print(f"Payment hash: {result.payment_hash}") +print(f"Response: {result.chat_output['content']}") \ No newline at end of file diff --git a/examples/run_x402_llm_stream.py b/examples/run_x402_llm_stream.py index b8f1127..cee1602 100644 --- a/examples/run_x402_llm_stream.py +++ b/examples/run_x402_llm_stream.py @@ -1,13 +1,20 @@ import os import opengradient as og +from x402_permit2 import check_permit2_approval + +network = "base-testnet" client = og.Client( private_key=os.environ.get("OG_PRIVATE_KEY"), ) +check_permit2_approval(client.wallet_address, network) + messages = [ - {"role": "user", "content": "Describe to me the 7 network layers?"}, + {"role": "user", "content": "What is Python?"}, + {"role": "assistant", "content": "Python is a high-level programming language."}, + {"role": "user", "content": "What makes it good for beginners?"}, ] stream = client.llm.chat( @@ -15,7 +22,8 @@ messages=messages, x402_settlement_mode=og.x402SettlementMode.SETTLE_METADATA, stream=True, - max_tokens=1000, + max_tokens=300, + network=network, ) for chunk in stream: diff --git a/examples/x402_permit2.py b/examples/x402_permit2.py new file mode 100644 index 0000000..070fb6e --- /dev/null +++ b/examples/x402_permit2.py @@ -0,0 +1,141 @@ +import argparse +import os +from typing import Optional + +import opengradient as og +from x402v2.mechanisms.evm.constants import PERMIT2_ADDRESS +from web3 import Web3 + +BASE_OPG_ADDRESS = "0x240b09731D96979f50B2C649C9CE10FcF9C7987F" +BASE_SEPOLIA_RPC = "https://sepolia.base.org" +BASE_TESTNET = "base-testnet" +MAX_UINT256 = (1 << 256) - 1 + +ERC20_ABI = [ + { + "inputs": [ + {"name": "owner", "type": "address"}, + {"name": "spender", "type": "address"}, + ], + "name": "allowance", + "outputs": [{"name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"name": "spender", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], + "name": "approve", + "outputs": [{"name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function", + }, +] + + +def _get_base_sepolia_web3() -> Web3: + return Web3(Web3.HTTPProvider(BASE_SEPOLIA_RPC)) + + +def get_permit2_allowance(client_address: str) -> int: + w3 = _get_base_sepolia_web3() + token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI) + return token.functions.allowance( + Web3.to_checksum_address(client_address), + Web3.to_checksum_address(PERMIT2_ADDRESS), + ).call() + + +def check_permit2_approval(client_address: str, network: str) -> None: + """Raise an error if Permit2 approval is missing for base-testnet.""" + if network != BASE_TESTNET: + return + + allowance = get_permit2_allowance(client_address) + print(f"Current OPG Permit2 allowance: {allowance}") + + if allowance == 0: + raise RuntimeError( + f"ERROR: No Permit2 approval found for address {client_address}. " + f"Approve Permit2 ({PERMIT2_ADDRESS}) to spend OPG ({BASE_OPG_ADDRESS}) " + "on Base Sepolia before using x402 payments." + ) + + +def grant_permit2_approval( + private_key: str, + amount: int = MAX_UINT256, + gas_multiplier: float = 1.2, + nonce: Optional[int] = None, +) -> str: + """Send ERC-20 approve(spender=Permit2, amount=amount) for OPG on Base Sepolia.""" + w3 = _get_base_sepolia_web3() + + account = w3.eth.account.from_key(private_key) + token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI) + + tx_nonce = nonce + if tx_nonce is None: + tx_nonce = w3.eth.get_transaction_count(account.address, "pending") + + approve_fn = token.functions.approve(Web3.to_checksum_address(PERMIT2_ADDRESS), amount) + estimated_gas = approve_fn.estimate_gas({"from": account.address}) + + tx = approve_fn.build_transaction( + { + "from": account.address, + "nonce": tx_nonce, + "gas": int(estimated_gas * gas_multiplier), + "gasPrice": w3.eth.gas_price, + "chainId": w3.eth.chain_id, + } + ) + + signed = account.sign_transaction(tx) + tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt.status != 1: + raise RuntimeError(f"Permit2 approval transaction failed: {tx_hash.hex()}") + + return tx_hash.hex() + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Grant Permit2 approval to spend OPG on Base Sepolia.") + parser.add_argument( + "--amount", + type=int, + default=MAX_UINT256, + help="Approval amount in base units (default: max uint256).", + ) + return parser.parse_args() + + +def main() -> None: + args = _parse_args() + + private_key = os.environ.get("OG_PRIVATE_KEY") + if not private_key: + raise RuntimeError("OG_PRIVATE_KEY is not set.") + + client = og.Client(private_key=private_key) + wallet_address = client.wallet_address + + before = get_permit2_allowance(wallet_address) + print(f"Wallet: {wallet_address}") + print(f"Permit2: {PERMIT2_ADDRESS}") + print(f"OPG Token: {BASE_OPG_ADDRESS}") + print(f"Allowance before: {before}") + + tx_hash = grant_permit2_approval(private_key=private_key, amount=args.amount) + after = get_permit2_allowance(wallet_address) + + print(f"Approval transaction hash: {tx_hash}") + print(f"Allowance after: {after}") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 7317ef7..00d1148 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "openai>=1.58.1", "pydantic>=2.9.2", "og-test-x402==0.0.9", + "og-test-v2-x402==0.0.6" ] [project.scripts] diff --git a/requirements.txt b/requirements.txt index 58b585b..6bb305c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ requests>=2.32.3 langchain>=0.3.7 openai>=1.58.1 pydantic>=2.9.2 -og-test-x402==0.0.9 \ No newline at end of file +og-test-x402==0.0.9 +og-test-v2-x402==0.0.6 \ No newline at end of file diff --git a/src/opengradient/cli.py b/src/opengradient/cli.py index 72ffec5..7b8c208 100644 --- a/src/opengradient/cli.py +++ b/src/opengradient/cli.py @@ -20,7 +20,7 @@ DEFAULT_OG_FAUCET_URL, DEFAULT_RPC_URL, ) -from .types import InferenceMode, x402SettlementMode +from .types import InferenceMode, x402Network, x402SettlementMode OG_CONFIG_FILE = Path.home() / ".opengradient_config.json" @@ -74,6 +74,11 @@ def convert(self, value, param, ctx): "settle-metadata": x402SettlementMode.SETTLE_METADATA, } +x402Networks = { + "og-evm": x402Network.OG_EVM, + "base-testnet": x402Network.BASE_TESTNET, +} + def initialize_config(ctx): """Interactively initialize OpenGradient config""" @@ -461,6 +466,12 @@ def print_llm_completion_result(model_cid, tx_hash, llm_output, is_vanilla=True) default="settle-batch", help="Settlement mode for x402 payments: settle (hashes only), settle-batch (batched, default), settle-metadata (full data)", ) +@click.option( + "--network", + type=click.Choice(x402Networks.keys()), + default="og-evm", + help="x402 network to use for TEE chat payments: og-evm (default) or base-testnet", +) @click.option("--stream", is_flag=True, default=False, help="Stream the output from the LLM") @click.pass_context def chat( @@ -475,6 +486,7 @@ def chat( tools_file: Optional[Path], tool_choice: Optional[str], x402_settlement_mode: Optional[str], + network: str, stream: bool, ): """ @@ -561,6 +573,7 @@ def chat( tools=parsed_tools, tool_choice=tool_choice, x402_settlement_mode=x402SettlementModes[x402_settlement_mode], + network=x402Networks[network], stream=stream, ) diff --git a/src/opengradient/client/client.py b/src/opengradient/client/client.py index 472bff7..5cf6063 100644 --- a/src/opengradient/client/client.py +++ b/src/opengradient/client/client.py @@ -50,6 +50,7 @@ def __init__( email: Optional[str] = None, password: Optional[str] = None, twins_api_key: Optional[str] = None, + wallet_address: str = None, rpc_url: str = DEFAULT_RPC_URL, api_url: str = DEFAULT_API_URL, contract_address: str = DEFAULT_INFERENCE_CONTRACT_ADDRESS, @@ -79,6 +80,7 @@ def __init__( # Create namespaces self.model_hub = ModelHub(hub_user=hub_user) + self.wallet_address = wallet_account.address self.llm = LLM( wallet_account=wallet_account, diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index 2d13224..9e6901e 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -2,15 +2,26 @@ import asyncio import json -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, AsyncGenerator import httpx from eth_account.account import LocalAccount from x402.clients.base import x402Client from x402.clients.httpx import x402HttpxClient +from x402v2 import x402Client as x402Clientv2 +from x402v2.http import x402HTTPClient as x402HTTPClientv2 +from x402v2.http.clients import x402HttpxClient as x402HttpxClientv2 +from x402v2.mechanisms.evm import EthAccountSigner as EthAccountSignerv2 +from x402v2.mechanisms.evm.exact import ExactEvmServerScheme as ExactEvmServerSchemev2 +from x402v2.mechanisms.evm.upto import UptoEvmServerScheme as UptoEvmServerSchemev2 +from x402v2.mechanisms.evm.exact.register import register_exact_evm_client as register_exact_evm_clientv2 +from x402v2.mechanisms.evm.upto.register import register_upto_evm_client as register_upto_evm_clientv2 +from eth_account import Account + from ..defaults import ( DEFAULT_NETWORK_FILTER, + DEFAULT_OPENGRADIENT_V2_LLM_SERVER_URL ) from ..types import ( TEE_LLM, @@ -18,12 +29,14 @@ TextGenerationOutput, TextGenerationStream, x402SettlementMode, + x402Network ) from .exceptions import OpenGradientError from .x402_auth import X402Auth X402_PROCESSING_HASH_HEADER = "x-processing-hash" X402_PLACEHOLDER_API_KEY = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" +BASE_TESTNET_NETWORK = "eip155:84532" TIMEOUT = httpx.Timeout( timeout=90.0, @@ -152,13 +165,9 @@ async def make_request(): # Read the response content content = await response.aread() result = json.loads(content.decode()) - payment_hash = "" - - if X402_PROCESSING_HASH_HEADER in response.headers: - payment_hash = response.headers[X402_PROCESSING_HASH_HEADER] return TextGenerationOutput( - transaction_hash="external", completion_output=result.get("completion"), payment_hash=payment_hash + transaction_hash="external", completion_output=result.get("completion"), ) except Exception as e: @@ -182,6 +191,7 @@ def chat( tool_choice: Optional[str] = None, x402_settlement_mode: Optional[x402SettlementMode] = x402SettlementMode.SETTLE_BATCH, stream: bool = False, + network: Optional[x402Network] = x402Network.OG_EVM, ) -> Union[TextGenerationOutput, TextGenerationStream]: """ Perform inference on an LLM model using chat via TEE. @@ -220,6 +230,7 @@ def chat( tools=tools, tool_choice=tool_choice, x402_settlement_mode=x402_settlement_mode, + network=network, ) else: # Non-streaming @@ -232,6 +243,7 @@ def chat( tools=tools, tool_choice=tool_choice, x402_settlement_mode=x402_settlement_mode, + network=network ) def _tee_llm_chat( @@ -244,6 +256,7 @@ def _tee_llm_chat( tools: Optional[List[Dict]] = None, tool_choice: Optional[str] = None, x402_settlement_mode: x402SettlementMode = x402SettlementMode.SETTLE_BATCH, + network: Optional[x402Network] = x402Network.OG_EVM, ) -> TextGenerationOutput: """ Route chat request to OpenGradient TEE LLM server with x402 payments. @@ -282,13 +295,59 @@ async def make_request(): endpoint = "/v1/chat/completions" response = await client.post(endpoint, json=payload, headers=headers, timeout=60) - # Read the response content content = await response.aread() result = json.loads(content.decode()) - payment_hash = "" - if X402_PROCESSING_HASH_HEADER in response.headers: - payment_hash = response.headers[X402_PROCESSING_HASH_HEADER] + choices = result.get("choices") + if not choices: + raise OpenGradientError(f"Invalid response: 'choices' missing or empty in {result}") + + return TextGenerationOutput( + transaction_hash="external", + finish_reason=choices[0].get("finish_reason"), + chat_output=choices[0].get("message"), + ) + + except Exception as e: + raise OpenGradientError(f"TEE LLM chat request failed: {str(e)}") + + + async def make_request_v2(): + x402_client = x402Clientv2() + register_exact_evm_clientv2(x402_client, EthAccountSignerv2(self._wallet_account),networks=[BASE_TESTNET_NETWORK]) + register_upto_evm_clientv2(x402_client, EthAccountSignerv2(self._wallet_account),networks=[BASE_TESTNET_NETWORK]) + + # Security Fix: verify=True enabled + async with x402HttpxClientv2( + x402_client + ) as client: + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {X402_PLACEHOLDER_API_KEY}", + "X-SETTLEMENT-TYPE": x402_settlement_mode, + } + + payload = { + "model": model, + "messages": messages, + "max_tokens": max_tokens, + "temperature": temperature, + } + + if stop_sequence: + payload["stop"] = stop_sequence + + if tools: + payload["tools"] = tools + payload["tool_choice"] = tool_choice or "auto" + + try: + # Non-streaming with x402 + endpoint = "/v1/chat/completions" + response = await client.post(DEFAULT_OPENGRADIENT_V2_LLM_SERVER_URL+endpoint, json=payload, headers=headers, timeout=60) + + content = await response.aread() + result = json.loads(content.decode()) choices = result.get("choices") if not choices: @@ -298,14 +357,18 @@ async def make_request(): transaction_hash="external", finish_reason=choices[0].get("finish_reason"), chat_output=choices[0].get("message"), - payment_hash=payment_hash, ) except Exception as e: raise OpenGradientError(f"TEE LLM chat request failed: {str(e)}") try: - return asyncio.run(make_request()) + if network == x402Network.OG_EVM: + return asyncio.run(make_request()) + elif network == x402Network.BASE_TESTNET: + return asyncio.run(make_request_v2()) + else: + raise OpenGradientError(f"Invalid network: {network}") except OpenGradientError: raise except Exception as e: @@ -321,6 +384,7 @@ def _tee_llm_chat_stream_sync( tools: Optional[List[Dict]] = None, tool_choice: Optional[str] = None, x402_settlement_mode: x402SettlementMode = x402SettlementMode.SETTLE_BATCH, + network: Optional[x402Network] = x402Network.OG_EVM, ): """ Sync streaming using threading bridge - TRUE real-time streaming. @@ -352,6 +416,7 @@ async def _stream(): tools=tools, tool_choice=tool_choice, x402_settlement_mode=x402_settlement_mode, + network=network, ): queue.put(chunk) # Put chunk immediately except Exception as e: @@ -405,76 +470,106 @@ async def _tee_llm_chat_stream_async( tools: Optional[List[Dict]] = None, tool_choice: Optional[str] = None, x402_settlement_mode: x402SettlementMode = x402SettlementMode.SETTLE_BATCH, + network: Optional[x402Network] = x402Network.OG_EVM, ): """ Internal async streaming implementation for TEE LLM with x402 payments. Yields StreamChunk objects as they arrive from the server. """ - async with httpx.AsyncClient( - base_url=self._og_llm_streaming_server_url, - headers={"Authorization": f"Bearer {X402_PLACEHOLDER_API_KEY}"}, - timeout=TIMEOUT, - limits=LIMITS, - http2=False, - follow_redirects=False, - auth=X402Auth(account=self._wallet_account, network_filter=DEFAULT_NETWORK_FILTER), # type: ignore - verify=True, - ) as client: - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {X402_PLACEHOLDER_API_KEY}", - "X-SETTLEMENT-TYPE": x402_settlement_mode, - } - - payload = { - "model": model, - "messages": messages, - "max_tokens": max_tokens, - "temperature": temperature, - "stream": True, - } - - if stop_sequence: - payload["stop"] = stop_sequence - if tools: - payload["tools"] = tools - payload["tool_choice"] = tool_choice or "auto" - - async with client.stream( - "POST", - "/v1/chat/completions", - json=payload, - headers=headers, - ) as response: - buffer = b"" - async for chunk in response.aiter_raw(): - if not chunk: + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {X402_PLACEHOLDER_API_KEY}", + "X-SETTLEMENT-TYPE": x402_settlement_mode, + } + + payload = { + "model": model, + "messages": messages, + "max_tokens": max_tokens, + "temperature": temperature, + "stream": True, + } + + if stop_sequence: + payload["stop"] = stop_sequence + if tools: + payload["tools"] = tools + payload["tool_choice"] = tool_choice or "auto" + + async def _parse_sse_response(response) -> AsyncGenerator[StreamChunk, None]: + status_code = getattr(response, "status_code", None) + if status_code is not None and status_code >= 400: + body = await response.aread() + body_text = body.decode("utf-8", errors="replace") + raise OpenGradientError(f"TEE LLM streaming request failed with status {status_code}: {body_text}") + + buffer = b"" + async for chunk in response.aiter_raw(): + if not chunk: + continue + + buffer += chunk + + while b"\n" in buffer: + line_bytes, buffer = buffer.split(b"\n", 1) + + if not line_bytes.strip(): continue - buffer += chunk - - # Process complete lines from buffer - while b"\n" in buffer: - line_bytes, buffer = buffer.split(b"\n", 1) - - if not line_bytes.strip(): - continue + try: + line = line_bytes.decode("utf-8").strip() + except UnicodeDecodeError: + continue - try: - line = line_bytes.decode("utf-8").strip() - except UnicodeDecodeError: - continue + if not line.startswith("data: "): + continue - if not line.startswith("data: "): - continue + data_str = line[6:] + if data_str.strip() == "[DONE]": + return - data_str = line[6:] - if data_str.strip() == "[DONE]": - return + try: + data = json.loads(data_str) + yield StreamChunk.from_sse_data(data) + except json.JSONDecodeError: + continue - try: - data = json.loads(data_str) - yield StreamChunk.from_sse_data(data) - except json.JSONDecodeError: - continue + if network == x402Network.OG_EVM: + async with httpx.AsyncClient( + base_url=self._og_llm_streaming_server_url, + headers={"Authorization": f"Bearer {X402_PLACEHOLDER_API_KEY}"}, + timeout=TIMEOUT, + limits=LIMITS, + http2=False, + follow_redirects=False, + auth=X402Auth(account=self._wallet_account, network_filter=DEFAULT_NETWORK_FILTER), # type: ignore + verify=True, + ) as client: + async with client.stream( + "POST", + "/v1/chat/completions", + json=payload, + headers=headers, + ) as response: + async for parsed_chunk in _parse_sse_response(response): + yield parsed_chunk + + elif network == x402Network.BASE_TESTNET: + x402_client = x402Clientv2() + register_exact_evm_clientv2(x402_client, EthAccountSignerv2(self._wallet_account), networks=[BASE_TESTNET_NETWORK]) + register_upto_evm_clientv2(x402_client, EthAccountSignerv2(self._wallet_account), networks=[BASE_TESTNET_NETWORK]) + + async with x402HttpxClientv2(x402_client) as client: + endpoint = "/v1/chat/completions" + async with client.stream( + "POST", + DEFAULT_OPENGRADIENT_V2_LLM_SERVER_URL + endpoint, + json=payload, + headers=headers, + timeout=60, + ) as response: + async for parsed_chunk in _parse_sse_response(response): + yield parsed_chunk + else: + raise OpenGradientError(f"Invalid network: {network}") diff --git a/src/opengradient/defaults.py b/src/opengradient/defaults.py index cd796da..143aee0 100644 --- a/src/opengradient/defaults.py +++ b/src/opengradient/defaults.py @@ -8,4 +8,6 @@ DEFAULT_BLOCKCHAIN_EXPLORER = "https://explorer.opengradient.ai/tx/" DEFAULT_OPENGRADIENT_LLM_SERVER_URL = "https://llmogevm.opengradient.ai" DEFAULT_OPENGRADIENT_LLM_STREAMING_SERVER_URL = "https://llmogevm.opengradient.ai" +DEFAULT_OPENGRADIENT_V2_LLM_SERVER_URL = "https://llm.opengradient.ai" +DEFAULT_OPENGRADIENT_V2_LLM_STREAMING_SERVER_URL = "https://llm.opengradient.ai" DEFAULT_NETWORK_FILTER = "og-evm" diff --git a/src/opengradient/types.py b/src/opengradient/types.py index fa89c98..2a7b3f1 100644 --- a/src/opengradient/types.py +++ b/src/opengradient/types.py @@ -69,6 +69,10 @@ class CandleType(IntEnum): CLOSE = 3 VOLUME = 4 +class x402Network(str, Enum): + OG_EVM = "og-evm" + BASE_TESTNET = "base-testnet" + @dataclass class HistoricalInputQuery: diff --git a/tutorials/03-verified-tool-calling.md b/tutorials/03-verified-tool-calling.md index 61e1481..b1a978c 100644 --- a/tutorials/03-verified-tool-calling.md +++ b/tutorials/03-verified-tool-calling.md @@ -223,7 +223,6 @@ def run_agent(client: og.Client, user_query: str) -> str: return f"Error: {e}" print(f" Finish reason: {result.finish_reason}") - print(f" Payment hash: {result.payment_hash}") # -- The model wants to call one or more tools -- # "tool_calls" finish reason follows the OpenAI convention and is used