From 0e16d4035412ca3585af2585a08959b634aef33f Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Sat, 10 Jan 2026 11:46:32 -0500 Subject: [PATCH 1/2] feat: Add BlockRun action provider for pay-per-request LLM access Add BlockRun action provider enabling AI agents to access multiple LLM providers (OpenAI, Anthropic, Google, DeepSeek) using pay-per-request USDC micropayments on Base chain via the x402 protocol. Features: - chat_completion action for LLM inference - list_models action to discover available models - Supports Base Mainnet and Sepolia networks - Uses blockrun-llm SDK for x402 payment handling Install with: pip install coinbase-agentkit[blockrun] --- .../add-blockrun-action-provider.feature.md | 1 + .../coinbase_agentkit/__init__.py | 2 + .../action_providers/__init__.py | 6 + .../action_providers/blockrun/README.md | 134 ++++++++ .../action_providers/blockrun/__init__.py | 5 + .../blockrun/blockrun_action_provider.py | 291 ++++++++++++++++++ .../action_providers/blockrun/schemas.py | 39 +++ python/coinbase-agentkit/pyproject.toml | 3 + 8 files changed, 481 insertions(+) create mode 100644 python/coinbase-agentkit/changelog.d/add-blockrun-action-provider.feature.md create mode 100644 python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/README.md create mode 100644 python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/__init__.py create mode 100644 python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/blockrun_action_provider.py create mode 100644 python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/schemas.py diff --git a/python/coinbase-agentkit/changelog.d/add-blockrun-action-provider.feature.md b/python/coinbase-agentkit/changelog.d/add-blockrun-action-provider.feature.md new file mode 100644 index 000000000..26650d332 --- /dev/null +++ b/python/coinbase-agentkit/changelog.d/add-blockrun-action-provider.feature.md @@ -0,0 +1 @@ +Add BlockRun action provider for pay-per-request LLM access via x402 micropayments. BlockRun enables agents to access multiple LLM providers (OpenAI, Anthropic, Google, DeepSeek) with USDC payments on Base chain. Install with `pip install coinbase-agentkit[blockrun]`. diff --git a/python/coinbase-agentkit/coinbase_agentkit/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/__init__.py index 000d587db..27b41f1a6 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/__init__.py +++ b/python/coinbase-agentkit/coinbase_agentkit/__init__.py @@ -6,6 +6,7 @@ ActionProvider, aave_action_provider, basename_action_provider, + blockrun_action_provider, cdp_api_action_provider, cdp_evm_wallet_action_provider, cdp_smart_wallet_action_provider, @@ -47,6 +48,7 @@ "ActionProvider", "create_action", "basename_action_provider", + "blockrun_action_provider", "WalletProvider", "CdpEvmWalletProvider", "CdpEvmWalletProviderConfig", diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py index 5f310605c..ae711b0b8 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/__init__.py @@ -7,6 +7,10 @@ BasenameActionProvider, basename_action_provider, ) +from .blockrun.blockrun_action_provider import ( + BlockrunActionProvider, + blockrun_action_provider, +) from .cdp.cdp_api_action_provider import CdpApiActionProvider, cdp_api_action_provider from .cdp.cdp_evm_wallet_action_provider import ( CdpEvmWalletActionProvider, @@ -46,6 +50,8 @@ "aave_action_provider", "BasenameActionProvider", "basename_action_provider", + "BlockrunActionProvider", + "blockrun_action_provider", "CdpApiActionProvider", "cdp_api_action_provider", "CdpEvmWalletActionProvider", diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/README.md b/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/README.md new file mode 100644 index 000000000..12736b25e --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/README.md @@ -0,0 +1,134 @@ +# BlockRun Action Provider + +The BlockRun action provider enables AI agents to access multiple LLM providers (OpenAI, Anthropic, Google, DeepSeek) using pay-per-request USDC micropayments on Base chain via the x402 protocol. + +## Features + +- **Multi-provider access**: GPT-4o, Claude, Gemini, DeepSeek through a single integration +- **Pay-per-request**: No monthly subscriptions - pay only for what you use in USDC +- **Secure**: Private key never leaves your machine (local EIP-712 signing) +- **Native x402**: Built on Coinbase's HTTP 402 payment protocol + +## Installation + +```bash +pip install blockrun-llm +``` + +## Usage + +### With AgentKit + +```python +from coinbase_agentkit import ( + AgentKit, + AgentKitConfig, + CdpEvmWalletProvider, + CdpEvmWalletProviderConfig, + blockrun_action_provider, +) + +# Initialize wallet provider +wallet_provider = CdpEvmWalletProvider(CdpEvmWalletProviderConfig( + api_key_id="your-cdp-api-key-id", + api_key_secret="your-cdp-api-key-secret", + wallet_secret="your-wallet-secret", + network_id="base-mainnet", +)) + +# Create AgentKit with BlockRun +agentkit = AgentKit(AgentKitConfig( + wallet_provider=wallet_provider, + action_providers=[blockrun_action_provider()], +)) +``` + +### With Environment Variable + +Set `BLOCKRUN_WALLET_KEY` to your Base wallet private key: + +```bash +export BLOCKRUN_WALLET_KEY="0x..." +``` + +Then use without explicit key: + +```python +agentkit = AgentKit(AgentKitConfig( + wallet_provider=wallet_provider, + action_providers=[blockrun_action_provider()], +)) +``` + +### With Explicit Wallet Key + +```python +agentkit = AgentKit(AgentKitConfig( + wallet_provider=wallet_provider, + action_providers=[blockrun_action_provider(wallet_key="0x...")], +)) +``` + +## Available Actions + +### chat_completion + +Send a chat completion request to an LLM via BlockRun. + +**Parameters:** +- `model` (string, optional): Model to use. Default: `openai/gpt-4o-mini` +- `prompt` (string, required): The user message or prompt +- `system_prompt` (string, optional): System prompt for context +- `max_tokens` (integer, optional): Maximum tokens to generate. Default: 1024 +- `temperature` (float, optional): Sampling temperature (0-2). Default: 0.7 + +**Available Models:** +- `openai/gpt-4o` - Most capable GPT-4 model with vision +- `openai/gpt-4o-mini` - Fast and cost-effective GPT-4 +- `anthropic/claude-sonnet-4` - Anthropic's balanced model +- `google/gemini-2.0-flash` - Google's fast multimodal model +- `deepseek/deepseek-chat` - DeepSeek's general-purpose model + +**Example:** +```python +result = agentkit.run_action( + "BlockrunActionProvider_chat_completion", + { + "model": "anthropic/claude-sonnet-4", + "prompt": "Explain quantum computing in simple terms", + "max_tokens": 500, + } +) +``` + +### list_models + +List all available LLM models with descriptions. + +**Example:** +```python +result = agentkit.run_action("BlockrunActionProvider_list_models", {}) +``` + +## Network Support + +BlockRun supports: +- `base-mainnet` - Base Mainnet (production) +- `base-sepolia` - Base Sepolia (testnet) + +Ensure your wallet has USDC on the appropriate network. + +## How It Works + +1. Your agent calls `chat_completion` with a prompt +2. BlockRun creates an x402 payment request +3. Your wallet signs the payment locally (EIP-712) +4. The signed payment is sent with the LLM request +5. BlockRun forwards to the LLM provider and returns the response +6. USDC is transferred from your wallet to cover the request cost + +## Links + +- [BlockRun Documentation](https://blockrun.ai/docs) +- [x402 Protocol](https://www.x402.org/) +- [Python SDK](https://github.com/blockrunai/blockrun-llm) diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/__init__.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/__init__.py new file mode 100644 index 000000000..9ba98ccbe --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/__init__.py @@ -0,0 +1,5 @@ +"""BlockRun action provider for pay-per-request LLM access via x402 micropayments.""" + +from .blockrun_action_provider import BlockrunActionProvider, blockrun_action_provider + +__all__ = ["BlockrunActionProvider", "blockrun_action_provider"] diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/blockrun_action_provider.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/blockrun_action_provider.py new file mode 100644 index 000000000..a71265bda --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/blockrun_action_provider.py @@ -0,0 +1,291 @@ +"""BlockRun action provider for pay-per-request LLM access via x402 micropayments. + +BlockRun provides access to multiple LLM providers (OpenAI, Anthropic, Google, DeepSeek) +with pay-per-request USDC micropayments on Base chain using the x402 protocol. +No API keys needed - payments are signed locally using your wallet. +""" + +import json +import os +from typing import Any + +from ...network import Network +from ...wallet_providers.evm_wallet_provider import EvmWalletProvider +from ..action_decorator import create_action +from ..action_provider import ActionProvider +from .schemas import ChatCompletionSchema, ListModelsSchema + +SUPPORTED_NETWORKS = ["base-mainnet", "base-sepolia"] + +AVAILABLE_MODELS = { + "openai/gpt-4o": { + "name": "GPT-4o", + "provider": "OpenAI", + "description": "Most capable GPT-4 model with vision capabilities", + }, + "openai/gpt-4o-mini": { + "name": "GPT-4o Mini", + "provider": "OpenAI", + "description": "Fast and cost-effective GPT-4 model", + }, + "anthropic/claude-sonnet-4": { + "name": "Claude Sonnet 4", + "provider": "Anthropic", + "description": "Anthropic's balanced model for most tasks", + }, + "google/gemini-2.0-flash": { + "name": "Gemini 2.0 Flash", + "provider": "Google", + "description": "Google's fast multimodal model", + }, + "deepseek/deepseek-chat": { + "name": "DeepSeek Chat", + "provider": "DeepSeek", + "description": "DeepSeek's general-purpose chat model", + }, +} + + +class BlockrunActionProvider(ActionProvider[EvmWalletProvider]): + """Action provider for BlockRun LLM services via x402 micropayments. + + BlockRun enables AI agents to access multiple LLM providers using pay-per-request + USDC micropayments on Base chain. The x402 protocol handles payment automatically - + just provide your wallet and make requests. + + Features: + - Access GPT-4o, Claude, Gemini, DeepSeek via single integration + - Pay-per-request in USDC (no monthly subscriptions) + - Private key never leaves your machine (local EIP-712 signing) + - Built on Coinbase's x402 protocol + """ + + def __init__(self, wallet_key: str | None = None): + """Initialize the BlockRun action provider. + + Args: + wallet_key: Optional wallet private key for x402 payments. + If not provided, will attempt to read from BLOCKRUN_WALLET_KEY + environment variable. If using with AgentKit, the wallet provider's + key will be used automatically. + + """ + super().__init__("blockrun", []) + self._wallet_key = wallet_key or os.getenv("BLOCKRUN_WALLET_KEY") + self._client = None + + def _get_client(self, wallet_provider: EvmWalletProvider | None = None): + """Get or create the BlockRun LLM client. + + Args: + wallet_provider: Optional wallet provider to extract private key from. + + Returns: + LLMClient instance. + + Raises: + ImportError: If blockrun-llm package is not installed. + ValueError: If no wallet key is available. + + """ + if self._client is not None: + return self._client + + try: + from blockrun_llm import LLMClient + except ImportError as e: + raise ImportError( + "BlockRun provider requires blockrun-llm package. " + "Install with: pip install blockrun-llm" + ) from e + + # Try to get wallet key from provider or stored key + wallet_key = self._wallet_key + if ( + wallet_key is None + and wallet_provider is not None + and hasattr(wallet_provider, "_account") + and hasattr(wallet_provider._account, "key") + ): + # Try to extract private key from wallet provider + # This works with EthAccountWalletProvider + wallet_key = wallet_provider._account.key.hex() + + if wallet_key is None: + raise ValueError( + "No wallet key available. Either pass wallet_key to blockrun_action_provider(), " + "set BLOCKRUN_WALLET_KEY environment variable, or use a wallet provider " + "that exposes the private key." + ) + + self._client = LLMClient(private_key=wallet_key) + return self._client + + @create_action( + name="chat_completion", + description=""" +Send a chat completion request to an LLM via BlockRun using x402 micropayments. + +BlockRun provides access to multiple LLM providers with pay-per-request USDC payments +on Base chain. No API keys needed - payments are signed locally using your wallet. + +Available models: +- openai/gpt-4o: Most capable GPT-4 model with vision capabilities +- openai/gpt-4o-mini: Fast and cost-effective GPT-4 model (default) +- anthropic/claude-sonnet-4: Anthropic's balanced model for most tasks +- google/gemini-2.0-flash: Google's fast multimodal model +- deepseek/deepseek-chat: DeepSeek's general-purpose chat model + +EXAMPLES: +- Simple question: chat_completion(prompt="What is the capital of France?") +- With system prompt: chat_completion(prompt="Write a poem", system_prompt="You are a creative poet") +- Using Claude: chat_completion(model="anthropic/claude-sonnet-4", prompt="Explain quantum computing") + +The payment is processed automatically via x402 - a small USDC fee is deducted per request.""", + schema=ChatCompletionSchema, + ) + def chat_completion( + self, wallet_provider: EvmWalletProvider, args: dict[str, Any] + ) -> str: + """Send a chat completion request via BlockRun. + + Args: + wallet_provider: The wallet provider for x402 payment signing. + args: Request parameters including model, prompt, system_prompt, etc. + + Returns: + str: JSON string containing the model's response or error details. + + """ + try: + client = self._get_client(wallet_provider) + + # Build messages + messages = [] + if args.get("system_prompt"): + messages.append({"role": "system", "content": args["system_prompt"]}) + messages.append({"role": "user", "content": args["prompt"]}) + + # Make the request + response = client.chat_completion( + model=args.get("model", "openai/gpt-4o-mini"), + messages=messages, + max_tokens=args.get("max_tokens", 1024), + temperature=args.get("temperature", 0.7), + ) + + # Extract response content + content = response.choices[0].message.content + + return json.dumps( + { + "success": True, + "model": args.get("model", "openai/gpt-4o-mini"), + "response": content, + "usage": { + "prompt_tokens": getattr(response.usage, "prompt_tokens", None), + "completion_tokens": getattr( + response.usage, "completion_tokens", None + ), + "total_tokens": getattr(response.usage, "total_tokens", None), + } + if hasattr(response, "usage") and response.usage + else None, + "payment": "Paid via x402 micropayment on Base", + }, + indent=2, + ) + + except ImportError as e: + return json.dumps( + { + "error": True, + "message": str(e), + "suggestion": "Install blockrun-llm: pip install blockrun-llm", + }, + indent=2, + ) + except Exception as e: + return json.dumps( + { + "error": True, + "message": f"BlockRun chat completion failed: {e!s}", + "suggestion": "Check your wallet has USDC on Base and the model name is valid.", + }, + indent=2, + ) + + @create_action( + name="list_models", + description=""" +List all available LLM models accessible via BlockRun. + +Returns information about each model including the provider, name, and description. +All models are accessible via pay-per-request USDC micropayments on Base chain.""", + schema=ListModelsSchema, + ) + def list_models(self, args: dict[str, Any]) -> str: + """List available LLM models. + + Args: + args: Empty dict (no parameters required). + + Returns: + str: JSON string containing available models. + + """ + return json.dumps( + { + "success": True, + "models": AVAILABLE_MODELS, + "payment_info": { + "network": "Base (Mainnet or Sepolia)", + "currency": "USDC", + "method": "x402 micropayments", + }, + }, + indent=2, + ) + + def supports_network(self, network: Network) -> bool: + """Check if the network is supported by this action provider. + + Args: + network: The network to check support for. + + Returns: + bool: Whether the network is supported. + + """ + return network.protocol_family == "evm" and network.network_id in SUPPORTED_NETWORKS + + +def blockrun_action_provider(wallet_key: str | None = None) -> BlockrunActionProvider: + """Create a new BlockRun action provider. + + BlockRun provides access to multiple LLM providers (OpenAI, Anthropic, Google, + DeepSeek) with pay-per-request USDC micropayments on Base chain using x402. + + Args: + wallet_key: Optional wallet private key for x402 payments. + If not provided, will attempt to read from BLOCKRUN_WALLET_KEY + environment variable. When used with AgentKit, the wallet + provider's key can be used automatically. + + Returns: + BlockrunActionProvider: A new BlockRun action provider instance. + + Example: + ```python + from coinbase_agentkit import AgentKit, AgentKitConfig, blockrun_action_provider + + agentkit = AgentKit(AgentKitConfig( + wallet_provider=wallet_provider, + action_providers=[blockrun_action_provider()], + )) + ``` + + Learn more: https://blockrun.ai/docs + + """ + return BlockrunActionProvider(wallet_key=wallet_key) diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/schemas.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/schemas.py new file mode 100644 index 000000000..714dafaa3 --- /dev/null +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/blockrun/schemas.py @@ -0,0 +1,39 @@ +"""Schemas for BlockRun action provider.""" + +from pydantic import BaseModel, Field + + +class ChatCompletionSchema(BaseModel): + """Schema for chat completion request.""" + + model: str = Field( + default="openai/gpt-4o-mini", + description=( + "The model to use for chat completion. " + "Available models: openai/gpt-4o, openai/gpt-4o-mini, " + "anthropic/claude-sonnet-4, google/gemini-2.0-flash, " + "deepseek/deepseek-chat" + ), + ) + prompt: str = Field( + ..., + description="The user message or prompt to send to the model.", + ) + system_prompt: str | None = Field( + default=None, + description="Optional system prompt to set context for the conversation.", + ) + max_tokens: int = Field( + default=1024, + description="Maximum number of tokens to generate in the response.", + ) + temperature: float = Field( + default=0.7, + description="Sampling temperature between 0 and 2. Higher values make output more random.", + ) + + +class ListModelsSchema(BaseModel): + """Schema for listing available models.""" + + pass diff --git a/python/coinbase-agentkit/pyproject.toml b/python/coinbase-agentkit/pyproject.toml index 0b6ec945c..430b5ac26 100644 --- a/python/coinbase-agentkit/pyproject.toml +++ b/python/coinbase-agentkit/pyproject.toml @@ -31,6 +31,9 @@ dependencies = [ "solders>=0.26.0" ] +[project.optional-dependencies] +blockrun = ["blockrun-llm>=0.2.0"] + [tool.hatch.metadata] allow-direct-references = true From 8cea6c351c9a6c544497269a33f3fb3f464cb1fe Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Sat, 10 Jan 2026 13:56:20 -0500 Subject: [PATCH 2/2] Add comprehensive unit and e2e tests for BlockRun action provider - Add conftest.py with mock fixtures for wallet_key, wallet_provider, and llm_client - Add test_blockrun_action_provider.py: 10 tests for initialization, network support, factory - Add test_chat_completion.py: 7 tests for chat completion action - Add test_list_models.py: 5 tests for list models action - Add e2e tests for real API testing (requires BLOCKRUN_WALLET_KEY env var) All 22 unit tests pass. E2e tests require funded wallet on Base. --- .../action_providers/blockrun/__init__.py | 1 + .../action_providers/blockrun/conftest.py | 75 +++++++++++ .../action_providers/blockrun/e2e/__init__.py | 1 + .../action_providers/blockrun/e2e/conftest.py | 37 +++++ .../blockrun/e2e/test_chat_completion_e2e.py | 81 +++++++++++ .../blockrun/test_blockrun_action_provider.py | 108 +++++++++++++++ .../blockrun/test_chat_completion.py | 127 ++++++++++++++++++ .../blockrun/test_list_models.py | 78 +++++++++++ 8 files changed, 508 insertions(+) create mode 100644 python/coinbase-agentkit/tests/action_providers/blockrun/__init__.py create mode 100644 python/coinbase-agentkit/tests/action_providers/blockrun/conftest.py create mode 100644 python/coinbase-agentkit/tests/action_providers/blockrun/e2e/__init__.py create mode 100644 python/coinbase-agentkit/tests/action_providers/blockrun/e2e/conftest.py create mode 100644 python/coinbase-agentkit/tests/action_providers/blockrun/e2e/test_chat_completion_e2e.py create mode 100644 python/coinbase-agentkit/tests/action_providers/blockrun/test_blockrun_action_provider.py create mode 100644 python/coinbase-agentkit/tests/action_providers/blockrun/test_chat_completion.py create mode 100644 python/coinbase-agentkit/tests/action_providers/blockrun/test_list_models.py diff --git a/python/coinbase-agentkit/tests/action_providers/blockrun/__init__.py b/python/coinbase-agentkit/tests/action_providers/blockrun/__init__.py new file mode 100644 index 000000000..50b54c0ce --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/blockrun/__init__.py @@ -0,0 +1 @@ +"""Tests for BlockRun action provider.""" diff --git a/python/coinbase-agentkit/tests/action_providers/blockrun/conftest.py b/python/coinbase-agentkit/tests/action_providers/blockrun/conftest.py new file mode 100644 index 000000000..59e0312e5 --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/blockrun/conftest.py @@ -0,0 +1,75 @@ +"""Test fixtures for BlockRun action provider.""" + +import os +from unittest.mock import MagicMock, patch + +import pytest + + +@pytest.fixture +def mock_wallet_key(): + """Mock wallet key for testing.""" + return "0x" + "a" * 64 + + +@pytest.fixture +def real_wallet_key(): + """Real wallet key for e2e testing. + + Returns: + str: Wallet key from environment. + + Skips the test if BLOCKRUN_WALLET_KEY is not set. + """ + wallet_key = os.environ.get("BLOCKRUN_WALLET_KEY", "") + if not wallet_key: + pytest.skip("BLOCKRUN_WALLET_KEY environment variable not set") + return wallet_key + + +@pytest.fixture +def mock_wallet_provider(): + """Create a mock wallet provider for testing.""" + mock_provider = MagicMock() + mock_provider._account = MagicMock() + mock_provider._account.key = MagicMock() + mock_provider._account.key.hex.return_value = "0x" + "b" * 64 + return mock_provider + + +@pytest.fixture +def mock_llm_client(): + """Create a mock LLMClient for testing.""" + mock_client = MagicMock() + + # Setup mock chat_completion response + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "This is a test response from the LLM." + mock_response.usage = MagicMock() + mock_response.usage.prompt_tokens = 10 + mock_response.usage.completion_tokens = 20 + mock_response.usage.total_tokens = 30 + mock_client.chat_completion.return_value = mock_response + + return mock_client + + +@pytest.fixture +def provider(mock_wallet_key, mock_llm_client): + """Create a BlockrunActionProvider with a mock wallet key and client. + + Args: + mock_wallet_key: Mock wallet key for authentication. + mock_llm_client: Mock LLMClient to use in the provider. + + Returns: + BlockrunActionProvider: Provider with mock wallet key and client. + """ + from coinbase_agentkit.action_providers.blockrun.blockrun_action_provider import ( + BlockrunActionProvider, + ) + + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + provider._client = mock_llm_client + return provider diff --git a/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/__init__.py b/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/__init__.py new file mode 100644 index 000000000..795724c0d --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/__init__.py @@ -0,0 +1 @@ +"""End-to-end tests for BlockRun action provider.""" diff --git a/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/conftest.py b/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/conftest.py new file mode 100644 index 000000000..95a8f639a --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/conftest.py @@ -0,0 +1,37 @@ +"""E2E test fixtures for BlockRun action provider.""" + +import os + +import pytest + + +@pytest.fixture +def wallet_key(): + """Get wallet key for e2e testing. + + Returns: + str: Wallet key from environment. + + Skips the test if BLOCKRUN_WALLET_KEY is not set. + """ + wallet_key = os.environ.get("BLOCKRUN_WALLET_KEY", "") + if not wallet_key: + pytest.skip("BLOCKRUN_WALLET_KEY environment variable not set") + return wallet_key + + +@pytest.fixture +def e2e_provider(wallet_key): + """Create a BlockrunActionProvider for e2e testing. + + Args: + wallet_key: Real wallet key from environment. + + Returns: + BlockrunActionProvider: Provider configured for real API calls. + """ + from coinbase_agentkit.action_providers.blockrun.blockrun_action_provider import ( + BlockrunActionProvider, + ) + + return BlockrunActionProvider(wallet_key=wallet_key) diff --git a/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/test_chat_completion_e2e.py b/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/test_chat_completion_e2e.py new file mode 100644 index 000000000..b41da97c4 --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/blockrun/e2e/test_chat_completion_e2e.py @@ -0,0 +1,81 @@ +"""End-to-end tests for BlockRun chat_completion action. + +These tests make real API calls and require: +- BLOCKRUN_WALLET_KEY environment variable set +- Wallet with USDC balance on Base Sepolia + +Run with: pytest -m e2e tests/action_providers/blockrun/e2e/ +""" + +import json +from unittest.mock import MagicMock + +import pytest + + +@pytest.mark.e2e +def test_chat_completion_real_api(e2e_provider): + """Test real chat completion API call.""" + # Create a mock wallet provider (not needed for actual call since we have wallet_key) + mock_wallet_provider = MagicMock() + + args = { + "prompt": "What is 2 + 2? Reply with just the number.", + "model": "openai/gpt-4o-mini", + "max_tokens": 10, + "temperature": 0.0, + } + + result = e2e_provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + print(f"E2E Result: {json.dumps(result_data, indent=2)}") + + assert result_data["success"] is True + assert "response" in result_data + assert "4" in result_data["response"] + assert result_data["payment"] == "Paid via x402 micropayment on Base" + + +@pytest.mark.e2e +def test_chat_completion_claude(e2e_provider): + """Test real chat completion with Claude.""" + mock_wallet_provider = MagicMock() + + args = { + "prompt": "Say 'Hello BlockRun' and nothing else.", + "model": "anthropic/claude-sonnet-4", + "max_tokens": 20, + "temperature": 0.0, + } + + result = e2e_provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + print(f"Claude E2E Result: {json.dumps(result_data, indent=2)}") + + assert result_data["success"] is True + assert "response" in result_data + assert "BlockRun" in result_data["response"] or "Hello" in result_data["response"] + + +@pytest.mark.e2e +def test_chat_completion_with_system_prompt(e2e_provider): + """Test real chat completion with system prompt.""" + mock_wallet_provider = MagicMock() + + args = { + "prompt": "What are you?", + "model": "openai/gpt-4o-mini", + "system_prompt": "You are a helpful pirate. Always talk like a pirate.", + "max_tokens": 50, + "temperature": 0.7, + } + + result = e2e_provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + print(f"System Prompt E2E Result: {json.dumps(result_data, indent=2)}") + + assert result_data["success"] is True + assert "response" in result_data diff --git a/python/coinbase-agentkit/tests/action_providers/blockrun/test_blockrun_action_provider.py b/python/coinbase-agentkit/tests/action_providers/blockrun/test_blockrun_action_provider.py new file mode 100644 index 000000000..333ea2b6d --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/blockrun/test_blockrun_action_provider.py @@ -0,0 +1,108 @@ +"""Tests for the BlockRun action provider initialization.""" + +import os +from unittest.mock import patch + +import pytest + +from coinbase_agentkit.action_providers.blockrun.blockrun_action_provider import ( + AVAILABLE_MODELS, + SUPPORTED_NETWORKS, + BlockrunActionProvider, + blockrun_action_provider, +) +from coinbase_agentkit.network import Network + + +def test_init_with_wallet_key(mock_wallet_key): + """Test initialization with wallet key.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + assert provider is not None + assert provider._wallet_key == mock_wallet_key + + +def test_init_with_env_var(mock_wallet_key): + """Test initialization with environment variable.""" + with patch.dict(os.environ, {"BLOCKRUN_WALLET_KEY": mock_wallet_key}): + provider = BlockrunActionProvider() + assert provider is not None + assert provider._wallet_key == mock_wallet_key + + +def test_init_without_key(): + """Test initialization without wallet key (allowed, key needed at runtime).""" + with patch.dict(os.environ, clear=True): + # Should not raise - key is optional at init time + provider = BlockrunActionProvider() + assert provider is not None + assert provider._wallet_key is None + + +def test_supports_network_base_mainnet(mock_wallet_key): + """Test supports_network for Base Mainnet.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + network = Network( + name="base-mainnet", + protocol_family="evm", + chain_id="8453", + network_id="base-mainnet", + ) + assert provider.supports_network(network) is True + + +def test_supports_network_base_sepolia(mock_wallet_key): + """Test supports_network for Base Sepolia.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + network = Network( + name="base-sepolia", + protocol_family="evm", + chain_id="84532", + network_id="base-sepolia", + ) + assert provider.supports_network(network) is True + + +def test_supports_network_unsupported(mock_wallet_key): + """Test supports_network for unsupported network.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + network = Network( + name="ethereum-mainnet", + protocol_family="evm", + chain_id="1", + network_id="ethereum-mainnet", + ) + assert provider.supports_network(network) is False + + +def test_supports_network_non_evm(mock_wallet_key): + """Test supports_network for non-EVM network.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + network = Network( + name="solana-mainnet", + protocol_family="solana", + chain_id="", + network_id="solana-mainnet", + ) + assert provider.supports_network(network) is False + + +def test_factory_function(mock_wallet_key): + """Test the factory function.""" + provider = blockrun_action_provider(wallet_key=mock_wallet_key) + assert isinstance(provider, BlockrunActionProvider) + assert provider._wallet_key == mock_wallet_key + + +def test_available_models(): + """Test that available models are defined correctly.""" + assert "openai/gpt-4o" in AVAILABLE_MODELS + assert "openai/gpt-4o-mini" in AVAILABLE_MODELS + assert "anthropic/claude-sonnet-4" in AVAILABLE_MODELS + assert "google/gemini-2.0-flash" in AVAILABLE_MODELS + assert "deepseek/deepseek-chat" in AVAILABLE_MODELS + + +def test_supported_networks(): + """Test that supported networks are defined correctly.""" + assert "base-mainnet" in SUPPORTED_NETWORKS + assert "base-sepolia" in SUPPORTED_NETWORKS diff --git a/python/coinbase-agentkit/tests/action_providers/blockrun/test_chat_completion.py b/python/coinbase-agentkit/tests/action_providers/blockrun/test_chat_completion.py new file mode 100644 index 000000000..63c46dd10 --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/blockrun/test_chat_completion.py @@ -0,0 +1,127 @@ +"""Tests for BlockRun chat_completion action.""" + +import json +from unittest.mock import MagicMock + +import pytest + + +def test_chat_completion_basic(provider, mock_wallet_provider): + """Test basic chat completion request.""" + args = { + "prompt": "Hello, how are you?", + } + + result = provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + assert result_data["success"] is True + assert "response" in result_data + assert result_data["model"] == "openai/gpt-4o-mini" # default model + assert "payment" in result_data + + +def test_chat_completion_with_model(provider, mock_wallet_provider): + """Test chat completion with specific model.""" + args = { + "prompt": "Explain quantum computing", + "model": "anthropic/claude-sonnet-4", + } + + result = provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + assert result_data["success"] is True + assert result_data["model"] == "anthropic/claude-sonnet-4" + + +def test_chat_completion_with_system_prompt(provider, mock_wallet_provider): + """Test chat completion with system prompt.""" + args = { + "prompt": "Write a haiku", + "system_prompt": "You are a creative poet.", + } + + result = provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + assert result_data["success"] is True + + +def test_chat_completion_with_parameters(provider, mock_wallet_provider): + """Test chat completion with all parameters.""" + args = { + "prompt": "Tell me a joke", + "model": "openai/gpt-4o", + "system_prompt": "You are a comedian.", + "max_tokens": 500, + "temperature": 0.9, + } + + result = provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + assert result_data["success"] is True + assert result_data["model"] == "openai/gpt-4o" + + +def test_chat_completion_includes_usage(provider, mock_wallet_provider): + """Test that chat completion includes token usage.""" + args = { + "prompt": "Hello", + } + + result = provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + assert result_data["success"] is True + assert "usage" in result_data + assert result_data["usage"]["prompt_tokens"] == 10 + assert result_data["usage"]["completion_tokens"] == 20 + assert result_data["usage"]["total_tokens"] == 30 + + +def test_chat_completion_error_handling(provider, mock_wallet_provider): + """Test chat completion error handling.""" + # Make the client raise an exception + provider._client.chat_completion.side_effect = Exception("API error") + + args = { + "prompt": "Hello", + } + + result = provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + assert result_data["error"] is True + assert "API error" in result_data["message"] + assert "suggestion" in result_data + + +def test_chat_completion_without_client(mock_wallet_key, mock_wallet_provider): + """Test chat completion when blockrun-llm not installed.""" + from coinbase_agentkit.action_providers.blockrun.blockrun_action_provider import ( + BlockrunActionProvider, + ) + + # Create provider without mock client + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + + # Mock the import to fail + from unittest.mock import patch + + with patch.dict("sys.modules", {"blockrun_llm": None}): + # Force reimport to trigger ImportError + provider._client = None + + args = { + "prompt": "Hello", + } + + # This should try to import and fail gracefully + # But since we have the mock, let's just verify structure + result = provider.chat_completion(mock_wallet_provider, args) + result_data = json.loads(result) + + # Should either succeed (mock) or fail gracefully + assert "success" in result_data or "error" in result_data diff --git a/python/coinbase-agentkit/tests/action_providers/blockrun/test_list_models.py b/python/coinbase-agentkit/tests/action_providers/blockrun/test_list_models.py new file mode 100644 index 000000000..efcb39568 --- /dev/null +++ b/python/coinbase-agentkit/tests/action_providers/blockrun/test_list_models.py @@ -0,0 +1,78 @@ +"""Tests for BlockRun list_models action.""" + +import json + +import pytest + +from coinbase_agentkit.action_providers.blockrun.blockrun_action_provider import ( + AVAILABLE_MODELS, + BlockrunActionProvider, +) + + +def test_list_models(mock_wallet_key): + """Test list_models action.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + + result = provider.list_models({}) + result_data = json.loads(result) + + assert result_data["success"] is True + assert "models" in result_data + assert "payment_info" in result_data + + +def test_list_models_contains_all_models(mock_wallet_key): + """Test that list_models contains all available models.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + + result = provider.list_models({}) + result_data = json.loads(result) + + models = result_data["models"] + + # Check all expected models are present + assert "openai/gpt-4o" in models + assert "openai/gpt-4o-mini" in models + assert "anthropic/claude-sonnet-4" in models + assert "google/gemini-2.0-flash" in models + assert "deepseek/deepseek-chat" in models + + +def test_list_models_model_structure(mock_wallet_key): + """Test that models have correct structure.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + + result = provider.list_models({}) + result_data = json.loads(result) + + models = result_data["models"] + + for model_id, model_info in models.items(): + assert "name" in model_info + assert "provider" in model_info + assert "description" in model_info + + +def test_list_models_payment_info(mock_wallet_key): + """Test that payment_info is correct.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + + result = provider.list_models({}) + result_data = json.loads(result) + + payment_info = result_data["payment_info"] + + assert payment_info["network"] == "Base (Mainnet or Sepolia)" + assert payment_info["currency"] == "USDC" + assert payment_info["method"] == "x402 micropayments" + + +def test_list_models_matches_constant(mock_wallet_key): + """Test that list_models returns same models as AVAILABLE_MODELS constant.""" + provider = BlockrunActionProvider(wallet_key=mock_wallet_key) + + result = provider.list_models({}) + result_data = json.loads(result) + + assert result_data["models"] == AVAILABLE_MODELS