diff --git a/packages/paperclip-runner/src/drivers/acpx/hermes-distributions.ts b/packages/paperclip-runner/src/drivers/acpx/hermes-distributions.ts index 876624b472..b92812aa14 100644 --- a/packages/paperclip-runner/src/drivers/acpx/hermes-distributions.ts +++ b/packages/paperclip-runner/src/drivers/acpx/hermes-distributions.ts @@ -1,5 +1,5 @@ /** Reviewed native closures. Setup cannot adopt a digest from downloaded files. */ export const HERMES_CLOSURES: Readonly> = Object.freeze({ - "darwin-arm64": "26371563768eb6578b80019f86d4a49cb55e36fd1593021cf9eac2cba945180c", - "linux-x64": "06a4e2d0005f0600125b8b3032cb200d7d5ff21280ecd6c3b80f29d29a3491a8", + "darwin-arm64": "fdf1369e1713fcf6dbaf43bb4a08e2cb819120bfef47d6047adfe6f7f47778a1", + "linux-x64": "62f0071c5be0b5d07088b0d1f9d280db64390f7aeab395a526059afaa3573135", }); diff --git a/packages/paperclip-runner/src/providers/hermes/billing.py b/packages/paperclip-runner/src/providers/hermes/billing.py index 15fe1b868f..26c65c59df 100644 --- a/packages/paperclip-runner/src/providers/hermes/billing.py +++ b/packages/paperclip-runner/src/providers/hermes/billing.py @@ -80,13 +80,15 @@ class TurnBilling: with self._lock: self._closed = True completed = [r for r in self._requests if r.valid and r.done] - priced = [r for r in completed if r.cost is not None] + # A verified usage frame can precede a transport interruption. + # Keep that known charge without certifying the request's totals. + priced = [r for r in self._requests if r.cost_verified] tokenized = [r for r in completed if r.tokens is not None] scope_complete = owned_work_complete and not self._overflow and not self._children cost = sum(r.cost for r in priced) if cost > 1000000000000000: raise ValueError("Hermes reported turn cost exceeds its receipt bound") - complete = scope_complete and len(priced) == len(self._requests) + complete = scope_complete and len(completed) == len(self._requests) and len(priced) == len(self._requests) token_complete = scope_complete and len(tokenized) == len(self._requests) totals = tuple(sum(r.tokens[i] for r in tokenized) for i in range(4)) if any(value > 9007199254740991 for value in totals): @@ -107,6 +109,7 @@ class WireReceipt: self.done = False self.tokens = None self.cost = None + self.cost_verified = False self._seen_usage = False self._buffer = bytearray() self._data = [] @@ -117,9 +120,11 @@ class WireReceipt: value = json.loads(data, parse_float=Decimal) except (ValueError, UnicodeError): self.valid = False + self.cost_verified = False return if not isinstance(value, dict): self.valid = False + self.cost_verified = False return usage = value.get("usage") if usage is not None: @@ -128,13 +133,16 @@ class WireReceipt: self.valid = False self._seen_usage = True self.tokens, self.cost = tokens, cost + self.cost_verified = self.valid and cost is not None if value.get("error") is not None: self.valid = False + self.cost_verified = False def json(self, body): with self.owner._lock: if len(body) > MAX_BODY: self.valid = False + self.cost_verified = False else: self._document(body) self.done = True diff --git a/packages/paperclip-runner/src/providers/hermes/test_billing.py b/packages/paperclip-runner/src/providers/hermes/test_billing.py index 501d1a4de9..66bd96fc22 100644 --- a/packages/paperclip-runner/src/providers/hermes/test_billing.py +++ b/packages/paperclip-runner/src/providers/hermes/test_billing.py @@ -80,7 +80,19 @@ class Accounting(unittest.TestCase): self.assertFalse(ledger.finish()[0]["complete"]) ledger = TurnBilling(); receipt = ledger.begin() receipt.json(document()); receipt.json(document("0.01")) - self.assertFalse(ledger.finish()[0]["complete"]) + billed, _ = ledger.finish() + self.assertFalse(billed["complete"]) + self.assertEqual(billed["reportedRequestCount"], 0) + self.assertEqual(billed["amountUsdExact"], "0.000000000") + + def test_verified_frame_before_an_unfinished_response_is_a_known_subtotal(self): + ledger = TurnBilling(); receipt = ledger.begin() + receipt.feed(b"data: " + document() + b"\n\n") + billed, tokens = ledger.finish() + self.assertEqual(billed["reportedRequestCount"], 1) + self.assertEqual(billed["amountUsdExact"], "0.004200000") + self.assertFalse(billed["complete"]) + self.assertIsNone(tokens) def test_truncation_and_oversized_frames_are_unpriced(self): for wire in (b"data: " + document() + b"\n", b"data: " + b"x" * (MAX_FRAME + 1)): @@ -130,6 +142,31 @@ class Transport(unittest.TestCase): self.assertFalse(billed["complete"]) self.assertIsNone(tokens) + def test_interrupted_stream_keeps_its_verified_charge_before_a_successful_retry(self): + class InterruptedStream(httpx.SyncByteStream): + def __iter__(self): + yield b"data: " + document() + b"\n\n" + raise httpx.ReadError("synthetic interrupted stream") + + calls = [] + def reply(request): + calls.append(request) + if len(calls) == 1: + return httpx.Response(200, headers={"content-type": "text/event-stream"}, stream=InterruptedStream()) + return httpx.Response(200, content=document()) + + with httpx.Client(transport=httpx.MockTransport(reply)) as client, OpenAI(http_client=client, api_key="synthetic-fixture-only", base_url="https://openrouter.ai/api/v1") as sdk: + with self.assertRaises(httpx.ReadError): + with sdk.chat.completions.create(model="fixture-model", messages=[], stream=True) as stream: + list(stream) + sdk.chat.completions.create(model="fixture-model", messages=[]) + billed, tokens = self.ledger.finish() + self.assertEqual(billed["requestCount"], 2) + self.assertEqual(billed["reportedRequestCount"], 2) + self.assertEqual(billed["amountUsdExact"], "0.008400000") + self.assertFalse(billed["complete"]) + self.assertIsNone(tokens) + def test_account_or_endpoint_changes_cannot_mint_billing_for_the_selected_account(self): for url, key in [("https://other.test/v1/chat/completions", "synthetic-fixture-only"), ("https://openrouter.ai/api/v1/chat/completions", "another-fixture-account"),