From 76b4a9e605cefbb9705523b66b6a99944e9e3914 Mon Sep 17 00:00:00 2001 From: Aniket Dixit Date: Sat, 25 Apr 2026 21:34:06 +0530 Subject: [PATCH 1/3] individual settlement updates --- pyproject.toml | 4 ++-- src/opengradient/client/llm.py | 24 +++++++++++++++++++++--- src/opengradient/types.py | 21 +++++++++++++++++---- uv.lock | 10 +++++----- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 07be43c..747e44b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,8 @@ dependencies = [ "langchain>=0.3.7", "openai>=1.58.1", "pydantic>=2.9.2", - "og-x402>=0.0.1.dev8", - "og-x402[extensions]>=0.0.1.dev8", + "og-x402>=0.0.2.dev1", + "og-x402[extensions]>=0.0.2.dev1", ] [project.optional-dependencies] diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index e7e5337..232efd7 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -28,6 +28,8 @@ DEFAULT_TEE_REGISTRY_ADDRESS = "0x4e72238852f3c918f4E4e57AeC9280dDB0c80248" X402_PROCESSING_HASH_HEADER = "x-processing-hash" +X402_DATA_SETTLEMENT_TX_HASH_HEADER = "x-settlement-tx-hash" +X402_DATA_SETTLEMENT_BLOB_ID_HEADER = "x-settlement-walrus-blob-id" X402_PLACEHOLDER_API_KEY = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" BASE_MAINNET_NETWORK = "eip155:8453" BASE_MAINNET_RPC = os.getenv("BASE_MAINNET_RPC", "https://base-rpc.publicnode.com") @@ -147,6 +149,14 @@ def _headers(self, settlement_mode: x402SettlementMode) -> Dict[str, str]: "X-SETTLEMENT-TYPE": settlement_mode.value, } + @staticmethod + def _data_settlement_transaction_hash(response: httpx.Response) -> Optional[str]: + return response.headers.get(X402_DATA_SETTLEMENT_TX_HASH_HEADER) + + @staticmethod + def _data_settlement_blob_id(response: httpx.Response) -> Optional[str]: + return response.headers.get(X402_DATA_SETTLEMENT_BLOB_ID_HEADER) + def _chat_payload(self, params: _ChatParams, messages: List[Dict], stream: bool = False) -> Dict: payload: Dict = { "model": params.model, @@ -285,7 +295,8 @@ async def _request() -> TextGenerationOutput: raw_body = await response.aread() result = json.loads(raw_body.decode()) return TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash=self._data_settlement_transaction_hash(response), + data_settlement_blob_id=self._data_settlement_blob_id(response), completion_output=result.get("completion"), tee_signature=result.get("tee_signature"), tee_timestamp=result.get("tee_timestamp"), @@ -337,7 +348,7 @@ async def chat( Returns: Union[TextGenerationOutput, AsyncGenerator[StreamChunk, None]]: - - If stream=False: TextGenerationOutput with chat_output, transaction_hash, finish_reason, and payment_hash + - If stream=False: TextGenerationOutput with chat_output, data settlement metadata, finish_reason, and payment_hash - If stream=True: Async generator yielding StreamChunk objects Raises: @@ -408,7 +419,8 @@ async def _request() -> TextGenerationOutput: ).strip() return TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash=self._data_settlement_transaction_hash(response), + data_settlement_blob_id=self._data_settlement_blob_id(response), finish_reason=choices[0].get("finish_reason"), chat_output=message, usage=result.get("usage"), @@ -447,6 +459,8 @@ async def _chat_tools_as_stream(self, params: _ChatParams, messages: List[Dict]) tee_id=result.tee_id, tee_endpoint=result.tee_endpoint, tee_payment_address=result.tee_payment_address, + data_settlement_transaction_hash=result.data_settlement_transaction_hash, + data_settlement_blob_id=result.data_settlement_blob_id, ) async def _chat_stream(self, params: _ChatParams, messages: List[Dict]) -> AsyncGenerator[StreamChunk, None]: @@ -535,6 +549,10 @@ async def _parse_sse_response(self, response, tee) -> AsyncGenerator[StreamChunk chunk = StreamChunk.from_sse_data(data) if chunk.is_final: + chunk.data_settlement_transaction_hash = ( + self._data_settlement_transaction_hash(response) + ) + chunk.data_settlement_blob_id = self._data_settlement_blob_id(response) chunk.tee_id = tee.tee_id chunk.tee_endpoint = tee.endpoint chunk.tee_payment_address = tee.payment_address diff --git a/src/opengradient/types.py b/src/opengradient/types.py index 569f7d8..6df36a3 100644 --- a/src/opengradient/types.py +++ b/src/opengradient/types.py @@ -237,6 +237,10 @@ class StreamChunk: tee_id: On-chain TEE registry ID of the enclave that served this request (final chunk only) tee_endpoint: Endpoint URL of the TEE that served this request (final chunk only) tee_payment_address: Payment address registered for the TEE (final chunk only) + data_settlement_transaction_hash: Transaction hash for the data settlement + transaction, present on the final chunk when available. + data_settlement_blob_id: Walrus blob ID for individual data settlement, + present on the final chunk when available. """ choices: List[StreamChoice] @@ -248,6 +252,8 @@ class StreamChunk: tee_id: Optional[str] = None tee_endpoint: Optional[str] = None tee_payment_address: Optional[str] = None + data_settlement_transaction_hash: Optional[str] = None + data_settlement_blob_id: Optional[str] = None @classmethod def from_sse_data(cls, data: Dict) -> "StreamChunk": @@ -400,8 +406,12 @@ class TextGenerationOutput: performed inside a TEE enclave. Attributes: - transaction_hash: Blockchain transaction hash. Set to - ``"external"`` for TEE-routed providers. + data_settlement_transaction_hash: Blockchain transaction hash for + the data settlement transaction. Set to ``"external"`` when the + provider does not return data settlement metadata. + data_settlement_blob_id: Walrus blob ID for individual data + settlement. ``None`` for private/batch settlement or when the + provider does not return it. finish_reason: Reason the model stopped generating (e.g. ``"stop"``, ``"tool_call"``, ``"error"``). Only populated for chat requests. @@ -416,8 +426,11 @@ class TextGenerationOutput: time. """ - transaction_hash: str - """Blockchain transaction hash. Set to ``"external"`` for TEE-routed providers.""" + data_settlement_transaction_hash: str + """Blockchain transaction hash for the data settlement transaction. Set to ``"external"`` when unavailable.""" + + data_settlement_blob_id: Optional[str] = None + """Walrus blob ID for individual data settlement. ``None`` when unavailable.""" finish_reason: Optional[str] = None """Reason the model stopped generating (e.g. ``"stop"``, ``"tool_call"``, ``"error"``). Only populated for chat requests.""" diff --git a/uv.lock b/uv.lock index 6703436..0dab8a8 100644 --- a/uv.lock +++ b/uv.lock @@ -1871,16 +1871,16 @@ wheels = [ [[package]] name = "og-x402" -version = "0.0.1.dev8" +version = "0.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nest-asyncio" }, { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/9e/1d718f3e0f7a6f6fd53c8a183c1794bc4aa15d986b0faa76139d5b04096b/og_x402-0.0.1.dev8.tar.gz", hash = "sha256:9d02c2c81112b7a612cd1aea03c09af75fc75d70766d042b5ddcc82ee7d8f98a", size = 1306960, upload-time = "2026-04-09T19:44:24.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/b7/247040fc716553938c030385167492130b2ba5a84264284c3e374aa7bf1b/og_x402-0.0.2.tar.gz", hash = "sha256:d361597576dd20200b7cd6e9bc6f3d06820cb06b38bdfd9e2ab38bfed6c3283f", size = 1312621, upload-time = "2026-04-20T09:23:00.588Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/0e/48facce5d73330d1cb79bbd67eda9c94b9786ea86f433338ee4423a6b1d0/og_x402-0.0.1.dev8-py3-none-any.whl", hash = "sha256:2b5b9601a6d312f9b1cf68967eaf98229eb203c54ca403e46994d6eed2488ccc", size = 1387989, upload-time = "2026-04-09T19:44:23.174Z" }, + { url = "https://files.pythonhosted.org/packages/96/64/6bb6a4da2be2260d52a4e32099541bf75c8c0928dd47454681ccbae87d36/og_x402-0.0.2-py3-none-any.whl", hash = "sha256:e5ad5f257c1d65eb4d4c5ef5a53f404a1e69ff92a5cffd714440ac23fe95de61", size = 1392895, upload-time = "2026-04-20T09:22:58.285Z" }, ] [package.optional-dependencies] @@ -1950,8 +1950,8 @@ requires-dist = [ { name = "langgraph", marker = "extra == 'dev'" }, { name = "mypy", marker = "extra == 'dev'" }, { name = "numpy", specifier = ">=1.26.4" }, - { name = "og-x402", specifier = ">=0.0.1.dev8" }, - { name = "og-x402", extras = ["extensions"], specifier = ">=0.0.1.dev8" }, + { name = "og-x402", specifier = ">=0.0.2.dev1" }, + { name = "og-x402", extras = ["extensions"], specifier = ">=0.0.2.dev1" }, { name = "openai", specifier = ">=1.58.1" }, { name = "pdoc3", marker = "extra == 'dev'", specifier = "==0.10.0" }, { name = "pydantic", specifier = ">=2.9.2" }, From 71c605ae8e6a725d7ba14b3c1445aaf7536bb3fd Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Mon, 27 Apr 2026 14:04:00 -0400 Subject: [PATCH 2/3] fix compile --- examples/llm_multi_turn_conversation.py | 4 ++-- examples/llm_tool_calling.py | 2 +- src/opengradient/cli.py | 13 +++++++++++-- src/opengradient/client/twins.py | 2 +- tests/langchain_adapter_test.py | 18 +++++++++--------- tests/llm_test.py | 3 ++- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/examples/llm_multi_turn_conversation.py b/examples/llm_multi_turn_conversation.py index 3d944be..a16c092 100644 --- a/examples/llm_multi_turn_conversation.py +++ b/examples/llm_multi_turn_conversation.py @@ -42,7 +42,7 @@ async def chat_turn( model: TEE_LLM model to use. Returns: - Tuple of (assistant_reply, updated_history, transaction_hash). + Tuple of (assistant_reply, updated_history, data_settlement_transaction_hash). """ history = add_user_message(history, user_input) @@ -58,7 +58,7 @@ async def chat_turn( reply = str(result.chat_output["content"]) history = add_assistant_message(history, reply) - return reply, history, result.transaction_hash + return reply, history, result.data_settlement_transaction_hash async def main(): diff --git a/examples/llm_tool_calling.py b/examples/llm_tool_calling.py index b763298..8c131e7 100644 --- a/examples/llm_tool_calling.py +++ b/examples/llm_tool_calling.py @@ -68,7 +68,7 @@ async def main(): print(f"Finish reason: {result.finish_reason}") print(f"Chat output: {result.chat_output}") - print(f"Transaction hash: {result.transaction_hash}") + print(f"Data settlement transaction hash: {result.data_settlement_transaction_hash}") asyncio.run(main()) diff --git a/src/opengradient/cli.py b/src/opengradient/cli.py index 4886aa9..c48ce7b 100644 --- a/src/opengradient/cli.py +++ b/src/opengradient/cli.py @@ -415,7 +415,11 @@ def completion( ) print_llm_completion_result( - model_cid, completion_output.transaction_hash, completion_output.completion_output, is_vanilla=False, result=completion_output + model_cid, + completion_output.data_settlement_transaction_hash, + completion_output.completion_output, + is_vanilla=False, + result=completion_output, ) except Exception as e: @@ -603,7 +607,12 @@ def chat( print_streaming_chat_result(model_cid, result, is_tee=True) else: print_llm_chat_result( - model_cid, result.transaction_hash, result.finish_reason, result.chat_output, is_vanilla=False, result=result + model_cid, + result.data_settlement_transaction_hash, + result.finish_reason, + result.chat_output, + is_vanilla=False, + result=result, ) except Exception as e: diff --git a/src/opengradient/client/twins.py b/src/opengradient/client/twins.py index 94bafcc..797fe9f 100644 --- a/src/opengradient/client/twins.py +++ b/src/opengradient/client/twins.py @@ -79,7 +79,7 @@ def chat( raise RuntimeError(f"Invalid response: 'choices' missing or empty in {result}") return TextGenerationOutput( - transaction_hash="", + data_settlement_transaction_hash="", finish_reason=choices[0].get("finish_reason"), chat_output=choices[0].get("message"), payment_hash=None, diff --git a/tests/langchain_adapter_test.py b/tests/langchain_adapter_test.py index ecc450a..b08e205 100644 --- a/tests/langchain_adapter_test.py +++ b/tests/langchain_adapter_test.py @@ -78,7 +78,7 @@ def test_identifying_params(self, model): class TestGenerate: def test_text_response(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="stop", chat_output={"role": "assistant", "content": "Hello there!"}, ) @@ -91,7 +91,7 @@ def test_text_response(self, model, mock_llm_client): async def test_async_text_response(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="stop", chat_output={"role": "assistant", "content": "Hello async!"}, ) @@ -103,7 +103,7 @@ async def test_async_text_response(self, model, mock_llm_client): def test_tool_call_response_flat_format(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="tool_call", chat_output={ "role": "assistant", @@ -129,7 +129,7 @@ def test_tool_call_response_flat_format(self, model, mock_llm_client): def test_tool_call_response_nested_format(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="tool_call", chat_output={ "role": "assistant", @@ -158,7 +158,7 @@ def test_tool_call_response_nested_format(self, model, mock_llm_client): def test_content_as_list_of_blocks(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="stop", chat_output={ "role": "assistant", @@ -172,7 +172,7 @@ def test_content_as_list_of_blocks(self, model, mock_llm_client): def test_empty_chat_output(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="stop", chat_output=None, ) @@ -185,7 +185,7 @@ def test_empty_chat_output(self, model, mock_llm_client): class TestMessageConversion: def test_converts_all_message_types(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="stop", chat_output={"role": "assistant", "content": "ok"}, ) @@ -224,7 +224,7 @@ def test_unsupported_message_type_raises(self, model): def test_passes_correct_params_to_client(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="stop", chat_output={"role": "assistant", "content": "ok"}, ) @@ -299,7 +299,7 @@ def test_bind_tool_choice(self, model): def test_tools_used_in_generate(self, model, mock_llm_client): mock_llm_client.chat.return_value = TextGenerationOutput( - transaction_hash="external", + data_settlement_transaction_hash="external", finish_reason="stop", chat_output={"role": "assistant", "content": "ok"}, ) diff --git a/tests/llm_test.py b/tests/llm_test.py index 5309f28..f93add6 100644 --- a/tests/llm_test.py +++ b/tests/llm_test.py @@ -95,6 +95,7 @@ class _FakeStreamResponse: def __init__(self, status_code: int, chunks: List[bytes]): self.status_code = status_code self._chunks = chunks + self.headers: Dict[str, str] = {} async def aiter_raw(self): for chunk in self._chunks: @@ -169,7 +170,7 @@ async def test_returns_completion_output(self, fake_http): assert result.completion_output == "Hello world" assert result.tee_signature == "sig-abc" assert result.tee_timestamp == "2025-01-01T00:00:00Z" - assert result.transaction_hash == "external" + assert result.data_settlement_transaction_hash is None assert result.tee_id == "test-tee-id" assert result.tee_payment_address == "0xTestPayment" From 95bf9524ab935d79f6cc9220165ab2e5498c17aa Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Mon, 27 Apr 2026 14:08:27 -0400 Subject: [PATCH 3/3] fix checks: --- examples/llm_multi_turn_conversation.py | 2 +- src/opengradient/client/llm.py | 22 ++++++---------------- src/opengradient/client/twins.py | 1 - src/opengradient/types.py | 16 ++++++---------- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/examples/llm_multi_turn_conversation.py b/examples/llm_multi_turn_conversation.py index a16c092..50c2fd6 100644 --- a/examples/llm_multi_turn_conversation.py +++ b/examples/llm_multi_turn_conversation.py @@ -31,7 +31,7 @@ async def chat_turn( history: list, user_input: str, model: og.TEE_LLM = og.TEE_LLM.GEMINI_2_5_FLASH, -) -> tuple[str, list, str]: +) -> tuple[str, list, str | None]: """ Execute a single conversation turn. diff --git a/src/opengradient/client/llm.py b/src/opengradient/client/llm.py index 232efd7..0c01281 100644 --- a/src/opengradient/client/llm.py +++ b/src/opengradient/client/llm.py @@ -149,14 +149,6 @@ def _headers(self, settlement_mode: x402SettlementMode) -> Dict[str, str]: "X-SETTLEMENT-TYPE": settlement_mode.value, } - @staticmethod - def _data_settlement_transaction_hash(response: httpx.Response) -> Optional[str]: - return response.headers.get(X402_DATA_SETTLEMENT_TX_HASH_HEADER) - - @staticmethod - def _data_settlement_blob_id(response: httpx.Response) -> Optional[str]: - return response.headers.get(X402_DATA_SETTLEMENT_BLOB_ID_HEADER) - def _chat_payload(self, params: _ChatParams, messages: List[Dict], stream: bool = False) -> Dict: payload: Dict = { "model": params.model, @@ -295,8 +287,8 @@ async def _request() -> TextGenerationOutput: raw_body = await response.aread() result = json.loads(raw_body.decode()) return TextGenerationOutput( - data_settlement_transaction_hash=self._data_settlement_transaction_hash(response), - data_settlement_blob_id=self._data_settlement_blob_id(response), + data_settlement_transaction_hash=response.headers.get(X402_DATA_SETTLEMENT_TX_HASH_HEADER), + data_settlement_blob_id=response.headers.get(X402_DATA_SETTLEMENT_BLOB_ID_HEADER), completion_output=result.get("completion"), tee_signature=result.get("tee_signature"), tee_timestamp=result.get("tee_timestamp"), @@ -419,8 +411,8 @@ async def _request() -> TextGenerationOutput: ).strip() return TextGenerationOutput( - data_settlement_transaction_hash=self._data_settlement_transaction_hash(response), - data_settlement_blob_id=self._data_settlement_blob_id(response), + data_settlement_transaction_hash=response.headers.get(X402_DATA_SETTLEMENT_TX_HASH_HEADER), + data_settlement_blob_id=response.headers.get(X402_DATA_SETTLEMENT_BLOB_ID_HEADER), finish_reason=choices[0].get("finish_reason"), chat_output=message, usage=result.get("usage"), @@ -549,10 +541,8 @@ async def _parse_sse_response(self, response, tee) -> AsyncGenerator[StreamChunk chunk = StreamChunk.from_sse_data(data) if chunk.is_final: - chunk.data_settlement_transaction_hash = ( - self._data_settlement_transaction_hash(response) - ) - chunk.data_settlement_blob_id = self._data_settlement_blob_id(response) + chunk.data_settlement_transaction_hash = response.headers.get(X402_DATA_SETTLEMENT_TX_HASH_HEADER) + chunk.data_settlement_blob_id = response.headers.get(X402_DATA_SETTLEMENT_BLOB_ID_HEADER) chunk.tee_id = tee.tee_id chunk.tee_endpoint = tee.endpoint chunk.tee_payment_address = tee.payment_address diff --git a/src/opengradient/client/twins.py b/src/opengradient/client/twins.py index 797fe9f..01afcb3 100644 --- a/src/opengradient/client/twins.py +++ b/src/opengradient/client/twins.py @@ -79,7 +79,6 @@ def chat( raise RuntimeError(f"Invalid response: 'choices' missing or empty in {result}") return TextGenerationOutput( - data_settlement_transaction_hash="", finish_reason=choices[0].get("finish_reason"), chat_output=choices[0].get("message"), payment_hash=None, diff --git a/src/opengradient/types.py b/src/opengradient/types.py index 6df36a3..548699d 100644 --- a/src/opengradient/types.py +++ b/src/opengradient/types.py @@ -407,8 +407,8 @@ class TextGenerationOutput: Attributes: data_settlement_transaction_hash: Blockchain transaction hash for - the data settlement transaction. Set to ``"external"`` when the - provider does not return data settlement metadata. + the data settlement transaction. ``None`` when the provider + does not return data settlement metadata. data_settlement_blob_id: Walrus blob ID for individual data settlement. ``None`` for private/batch settlement or when the provider does not return it. @@ -426,8 +426,8 @@ class TextGenerationOutput: time. """ - data_settlement_transaction_hash: str - """Blockchain transaction hash for the data settlement transaction. Set to ``"external"`` when unavailable.""" + data_settlement_transaction_hash: Optional[str] = None + """Blockchain transaction hash for the data settlement transaction. ``None`` when unavailable.""" data_settlement_blob_id: Optional[str] = None """Walrus blob ID for individual data settlement. ``None`` when unavailable.""" @@ -593,13 +593,9 @@ class ResponseFormat: def __post_init__(self) -> None: valid_types = ("text", "json_object", "json_schema") if self.type not in valid_types: - raise ValueError( - f"ResponseFormat.type must be one of {valid_types}, got '{self.type}'" - ) + raise ValueError(f"ResponseFormat.type must be one of {valid_types}, got '{self.type}'") if self.type == "json_schema" and not self.json_schema: - raise ValueError( - "ResponseFormat.json_schema is required when type='json_schema'" - ) + raise ValueError("ResponseFormat.json_schema is required when type='json_schema'") def to_dict(self) -> Dict: """Serialise to a JSON-compatible dict for the TEE gateway request payload."""