|
1 | | -import asyncio |
2 | 1 | import os |
3 | | -import unittest |
| 2 | +import time |
4 | 3 |
|
| 4 | +import pytest |
5 | 5 | from eth_account import Account |
6 | 6 | from web3 import Web3 |
7 | 7 |
|
@@ -76,71 +76,74 @@ def _fund_account(funder_key: str, recipient_address: str): |
76 | 76 | if opg_receipt.status != 1: |
77 | 77 | raise RuntimeError(f"OPG transfer failed: {opg_hash.hex()}") |
78 | 78 |
|
| 79 | + # Wait for the recipient balance to be visible on the RPC node |
| 80 | + for _ in range(10): |
| 81 | + if w3.eth.get_balance(recipient) > 0: |
| 82 | + break |
| 83 | + time.sleep(1) |
| 84 | + else: |
| 85 | + raise RuntimeError("Recipient ETH balance is still 0 after funding") |
79 | 86 |
|
80 | | -class TestLLMChat(unittest.TestCase): |
81 | | - @classmethod |
82 | | - def setUpClass(cls): |
83 | | - """Create a fresh account and fund it from the funder wallet.""" |
84 | | - funder_key = os.environ.get("PRIVATE_KEY") |
85 | | - if not funder_key: |
86 | | - raise ValueError("PRIVATE_KEY environment variable is not set") |
87 | | - |
88 | | - # Generate a fresh ephemeral account |
89 | | - cls._test_account = Account.create() |
90 | | - print(f"\nTest account: {cls._test_account.address}") |
91 | | - |
92 | | - # Fund it with ETH + OPG from the funder |
93 | | - _fund_account(funder_key, cls._test_account.address) |
94 | | - print("Account funded with ETH and OPG") |
95 | | - |
96 | | - # Initialize LLM client with the fresh account |
97 | | - cls._llm = og.LLM(private_key=cls._test_account.key.hex()) |
98 | | - cls._llm.ensure_opg_approval(opg_amount=OPG_FUND_AMOUNT) |
99 | | - print("Permit2 approval complete") |
100 | | - |
101 | | - def test_chat(self): |
102 | | - async def run(): |
103 | | - messages = [ |
104 | | - {"role": "user", "content": "What is the capital of France? Reply in one word."}, |
105 | | - ] |
106 | | - |
107 | | - result = await self._llm.chat( |
108 | | - model=og.TEE_LLM.GEMINI_2_5_FLASH, |
109 | | - messages=messages, |
110 | | - max_tokens=50, |
111 | | - x402_settlement_mode=og.x402SettlementMode.INDIVIDUAL_FULL, |
112 | | - ) |
113 | | - |
114 | | - self.assertIsNotNone(result) |
115 | | - self.assertIn("Paris", result.chat_output["content"]) |
116 | | - |
117 | | - asyncio.run(run()) |
118 | | - |
119 | | - def test_chat_streaming(self): |
120 | | - async def run(): |
121 | | - messages = [ |
122 | | - {"role": "user", "content": "What is 2 + 2? Reply with just the number."}, |
123 | | - ] |
124 | | - |
125 | | - stream = await self._llm.chat( |
126 | | - model=og.TEE_LLM.GEMINI_2_5_FLASH, |
127 | | - messages=messages, |
128 | | - max_tokens=50, |
129 | | - x402_settlement_mode=og.x402SettlementMode.INDIVIDUAL_FULL, |
130 | | - stream=True, |
131 | | - ) |
132 | | - |
133 | | - chunks = [] |
134 | | - async for chunk in stream: |
135 | | - if chunk.choices[0].delta.content: |
136 | | - chunks.append(chunk.choices[0].delta.content) |
137 | | - |
138 | | - full_response = "".join(chunks) |
139 | | - self.assertTrue(len(chunks) > 0, "Expected at least one streamed chunk") |
140 | | - self.assertIn("4", full_response) |
141 | | - |
142 | | - asyncio.run(run()) |
143 | | - |
144 | | - |
145 | | -if __name__ == "__main__": |
146 | | - unittest.main() |
| 87 | + |
| 88 | +@pytest.fixture(scope="module") |
| 89 | +def llm_client(): |
| 90 | + """Create a fresh account, fund it, and return an initialized LLM client.""" |
| 91 | + funder_key = os.environ.get("PRIVATE_KEY") |
| 92 | + if not funder_key: |
| 93 | + pytest.skip("PRIVATE_KEY environment variable is not set") |
| 94 | + |
| 95 | + account = Account.create() |
| 96 | + print(f"\nTest account: {account.address}") |
| 97 | + |
| 98 | + _fund_account(funder_key, account.address) |
| 99 | + print("Account funded with ETH and OPG") |
| 100 | + |
| 101 | + llm = og.LLM(private_key=account.key.hex()) |
| 102 | + llm.ensure_opg_approval(opg_amount=OPG_FUND_AMOUNT) |
| 103 | + print("Permit2 approval complete") |
| 104 | + |
| 105 | + # Wait for the approval to propagate on-chain |
| 106 | + time.sleep(2) |
| 107 | + |
| 108 | + return llm |
| 109 | + |
| 110 | + |
| 111 | +@pytest.mark.asyncio(loop_scope="module") |
| 112 | +async def test_chat(llm_client): |
| 113 | + messages = [ |
| 114 | + {"role": "user", "content": "What is the capital of France? Reply in one word."}, |
| 115 | + ] |
| 116 | + |
| 117 | + result = await llm_client.chat( |
| 118 | + model=og.TEE_LLM.GEMINI_2_5_FLASH, |
| 119 | + messages=messages, |
| 120 | + max_tokens=50, |
| 121 | + x402_settlement_mode=og.x402SettlementMode.INDIVIDUAL_FULL, |
| 122 | + ) |
| 123 | + |
| 124 | + assert result is not None |
| 125 | + assert "Paris" in result.chat_output["content"] |
| 126 | + |
| 127 | + |
| 128 | +@pytest.mark.asyncio(loop_scope="module") |
| 129 | +async def test_chat_streaming(llm_client): |
| 130 | + messages = [ |
| 131 | + {"role": "user", "content": "What is 2 + 2? Reply with just the number."}, |
| 132 | + ] |
| 133 | + |
| 134 | + stream = await llm_client.chat( |
| 135 | + model=og.TEE_LLM.GEMINI_2_5_FLASH, |
| 136 | + messages=messages, |
| 137 | + max_tokens=50, |
| 138 | + x402_settlement_mode=og.x402SettlementMode.INDIVIDUAL_FULL, |
| 139 | + stream=True, |
| 140 | + ) |
| 141 | + |
| 142 | + chunks = [] |
| 143 | + async for chunk in stream: |
| 144 | + if chunk.choices[0].delta.content: |
| 145 | + chunks.append(chunk.choices[0].delta.content) |
| 146 | + |
| 147 | + full_response = "".join(chunks) |
| 148 | + assert len(chunks) > 0, "Expected at least one streamed chunk" |
| 149 | + assert "4" in full_response |
0 commit comments