metergraph 0.6.3__tar.gz → 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {metergraph-0.6.3/src/metergraph.egg-info → metergraph-0.6.4}/PKG-INFO +1 -1
  2. {metergraph-0.6.3 → metergraph-0.6.4}/pyproject.toml +1 -1
  3. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_capture.py +94 -26
  4. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_version.py +1 -1
  5. {metergraph-0.6.3 → metergraph-0.6.4/src/metergraph.egg-info}/PKG-INFO +1 -1
  6. {metergraph-0.6.3 → metergraph-0.6.4}/MANIFEST.in +0 -0
  7. {metergraph-0.6.3 → metergraph-0.6.4}/README.md +0 -0
  8. {metergraph-0.6.3 → metergraph-0.6.4}/setup.cfg +0 -0
  9. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/__init__.py +0 -0
  10. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_batch_first.py +0 -0
  11. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_config.py +0 -0
  12. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_context.py +0 -0
  13. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_failure_log.py +0 -0
  14. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_provider_batch.py +0 -0
  15. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_repo_config.py +0 -0
  16. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_session.py +0 -0
  17. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_template.py +0 -0
  18. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_track.py +0 -0
  19. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/_transport.py +0 -0
  20. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph/opentelemetry.py +0 -0
  21. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph.egg-info/SOURCES.txt +0 -0
  22. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph.egg-info/dependency_links.txt +0 -0
  23. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph.egg-info/requires.txt +0 -0
  24. {metergraph-0.6.3 → metergraph-0.6.4}/src/metergraph.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metergraph
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: Fire-and-forget LLM spend capture for Metergraph
5
5
  Author: Pioneer Square Labs
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "metergraph"
3
- version = "0.6.3"
3
+ version = "0.6.4"
4
4
  description = "Fire-and-forget LLM spend capture for Metergraph"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -884,6 +884,28 @@ class _StreamState:
884
884
  self.parts.append(text)
885
885
  return value
886
886
 
887
+ def safe_chunk(self, value: Any) -> Any:
888
+ """Record a provider chunk, but return the raw provider value if
889
+ MeterGraph's own normalization/bookkeeping fails. The caller passes the
890
+ chunk it already obtained from the provider, so a telemetry error here
891
+ never drops or corrupts iteration. Only MeterGraph's ``Exception`` is
892
+ swallowed; a cancellation propagating through ``chunk`` is not."""
893
+ try:
894
+ return self.chunk(value)
895
+ except Exception:
896
+ return value
897
+
898
+ def is_usage_only(self, value: Any) -> bool:
899
+ """Classify a usage-only chunk, failing open. A genuine usage-only chunk
900
+ is suppressed only when classification succeeds; if MeterGraph's own
901
+ classification raises on a pathological/proxied chunk, treat it as an
902
+ ordinary provider chunk so it is yielded, not dropped. Only MeterGraph's
903
+ ``Exception`` is swallowed here."""
904
+ try:
905
+ return _usage_only_chunk(value, self.call)
906
+ except Exception:
907
+ return False
908
+
887
909
  def finish(
888
910
  self, status: str | None = None, error: BaseException | None = None
889
911
  ) -> None:
@@ -933,6 +955,32 @@ class _StreamState:
933
955
  stream_chunks=self.chunks,
934
956
  )
935
957
 
958
+ def safe_finish(
959
+ self, status: str | None = None, error: BaseException | None = None
960
+ ) -> None:
961
+ """Shared fail-open finalization for every sync lifecycle path (entry
962
+ error, normal exhaustion, provider iteration error, context exit, and
963
+ close). MeterGraph's own telemetry ``Exception`` is swallowed so a
964
+ failed finish can never replace normal completion, the provider
965
+ iteration exception, or a close; the trace may be lost, but application
966
+ behavior is unchanged. Provider/application exceptions never reach
967
+ here."""
968
+ try:
969
+ self.finish(status=status, error=error)
970
+ except Exception:
971
+ pass
972
+
973
+ async def safe_finish_async(
974
+ self, status: str | None = None, error: BaseException | None = None
975
+ ) -> None:
976
+ """Async counterpart to :meth:`safe_finish`, shared by every async
977
+ lifecycle path (entry error, normal exhaustion, provider iteration
978
+ error, context exit, and aclose)."""
979
+ try:
980
+ await self.finish_async(status=status, error=error)
981
+ except Exception:
982
+ pass
983
+
936
984
 
937
985
  class SyncStream:
938
986
  def __init__(self, stream: Any, call: CallState) -> None:
@@ -951,16 +999,17 @@ class SyncStream:
951
999
  self._state.iterator = iter(self._state.stream)
952
1000
  while True:
953
1001
  try:
954
- value = self._state.chunk(next(self._state.iterator))
955
- if _usage_only_chunk(value, self._state.call):
956
- continue
957
- return value
1002
+ provider_chunk = next(self._state.iterator)
958
1003
  except StopIteration:
959
- self._state.finish()
1004
+ self._state.safe_finish()
960
1005
  raise
961
1006
  except BaseException as exc:
962
- self._state.finish(error=exc)
1007
+ self._state.safe_finish(error=exc)
963
1008
  raise
1009
+ value = self._state.safe_chunk(provider_chunk)
1010
+ if self._state.is_usage_only(value):
1011
+ continue
1012
+ return value
964
1013
 
965
1014
  def __enter__(self) -> "SyncStream":
966
1015
  enter = getattr(self._state.manager, "__enter__", None)
@@ -968,15 +1017,15 @@ class SyncStream:
968
1017
  try:
969
1018
  self._state.use_entered_stream(enter())
970
1019
  except BaseException as exc:
971
- self._state.finish(error=exc)
1020
+ self._state.safe_finish(error=exc)
972
1021
  raise
973
1022
  return self
974
1023
 
975
1024
  def __exit__(self, exc_type, exc, tb) -> Any:
976
1025
  if exc:
977
- self._state.finish(error=exc)
1026
+ self._state.safe_finish(error=exc)
978
1027
  elif not self._state.call.done:
979
- self._state.finish(status=None if self._state.entered else "abandoned")
1028
+ self._state.safe_finish(status=None if self._state.entered else "abandoned")
980
1029
  exit_fn = getattr(self._state.manager, "__exit__", None)
981
1030
  return exit_fn(exc_type, exc, tb) if exit_fn else None
982
1031
 
@@ -985,7 +1034,7 @@ class SyncStream:
985
1034
  if close:
986
1035
  close()
987
1036
  if not self._state.call.done:
988
- self._state.finish(status="abandoned")
1037
+ self._state.safe_finish(status="abandoned")
989
1038
 
990
1039
  def __del__(self) -> None:
991
1040
  try:
@@ -1012,16 +1061,17 @@ class AsyncStream:
1012
1061
  self._state.iterator = self._state.stream.__aiter__()
1013
1062
  while True:
1014
1063
  try:
1015
- value = self._state.chunk(await self._state.iterator.__anext__())
1016
- if _usage_only_chunk(value, self._state.call):
1017
- continue
1018
- return value
1064
+ provider_chunk = await self._state.iterator.__anext__()
1019
1065
  except StopAsyncIteration:
1020
- await self._state.finish_async()
1066
+ await self._state.safe_finish_async()
1021
1067
  raise
1022
1068
  except BaseException as exc:
1023
- await self._state.finish_async(error=exc)
1069
+ await self._state.safe_finish_async(error=exc)
1024
1070
  raise
1071
+ value = self._state.safe_chunk(provider_chunk)
1072
+ if self._state.is_usage_only(value):
1073
+ continue
1074
+ return value
1025
1075
 
1026
1076
  async def __aenter__(self) -> "AsyncStream":
1027
1077
  enter = getattr(self._state.manager, "__aenter__", None)
@@ -1029,15 +1079,15 @@ class AsyncStream:
1029
1079
  try:
1030
1080
  self._state.use_entered_stream(await enter())
1031
1081
  except BaseException as exc:
1032
- await self._state.finish_async(error=exc)
1082
+ await self._state.safe_finish_async(error=exc)
1033
1083
  raise
1034
1084
  return self
1035
1085
 
1036
1086
  async def __aexit__(self, exc_type, exc, tb) -> Any:
1037
1087
  if exc:
1038
- await self._state.finish_async(error=exc)
1088
+ await self._state.safe_finish_async(error=exc)
1039
1089
  elif not self._state.call.done:
1040
- await self._state.finish_async(
1090
+ await self._state.safe_finish_async(
1041
1091
  status=None if self._state.entered else "abandoned"
1042
1092
  )
1043
1093
  exit_fn = getattr(self._state.manager, "__aexit__", None)
@@ -1048,7 +1098,7 @@ class AsyncStream:
1048
1098
  if close:
1049
1099
  await close()
1050
1100
  if not self._state.call.done:
1051
- await self._state.finish_async(status="abandoned")
1101
+ await self._state.safe_finish_async(status="abandoned")
1052
1102
 
1053
1103
  def __del__(self) -> None:
1054
1104
  try:
@@ -1359,13 +1409,19 @@ def _patch(
1359
1409
  args = (first, *args[1:])
1360
1410
  else:
1361
1411
  kwargs = {**kwargs, "stream_options": {"include_usage": True}}
1362
- request = _request(args, kwargs)
1363
- capture_provider = _gateway_provider(request) if gateway else provider
1364
- call = runtime.call_state(capture_provider, endpoint, request)
1412
+ try:
1413
+ request = _request(args, kwargs)
1414
+ capture_provider = _gateway_provider(request) if gateway else provider
1415
+ call = runtime.call_state(capture_provider, endpoint, request)
1416
+ except Exception:
1417
+ # Capture-state creation is telemetry-only. If it fails, invoke the
1418
+ # provider exactly once and return/await its native result with no
1419
+ # MeterGraph wrapping — the caller observes an uninstrumented call.
1420
+ return original(*args, **kwargs)
1365
1421
  try:
1366
1422
  result = original(*args, **kwargs)
1367
1423
  except BaseException as exc:
1368
- call.finish(error=exc)
1424
+ _safe_finish(call, error=exc)
1369
1425
  raise
1370
1426
 
1371
1427
  if inspect.isawaitable(result):
@@ -1374,7 +1430,7 @@ def _patch(
1374
1430
  try:
1375
1431
  resolved = await result
1376
1432
  except BaseException as exc:
1377
- call.finish(error=exc)
1433
+ _safe_finish(call, error=exc)
1378
1434
  raise
1379
1435
  return _finish_or_stream(resolved, call, endpoint, request)
1380
1436
 
@@ -1389,6 +1445,18 @@ def _patch(
1389
1445
  return True
1390
1446
 
1391
1447
 
1448
+ def _safe_finish(call: CallState, response: Any = None, **kwargs: Any) -> None:
1449
+ """Fail-open finalization for a non-streaming call. MeterGraph's own
1450
+ telemetry ``Exception`` is swallowed so a failed ``CallState.finish`` can
1451
+ never replace the provider result or the provider exception the caller must
1452
+ observe. Only telemetry runs here — the provider result/exception is passed
1453
+ in, never produced — so a cancellation is never wrongly swallowed."""
1454
+ try:
1455
+ call.finish(response, **kwargs)
1456
+ except Exception:
1457
+ pass
1458
+
1459
+
1392
1460
  def _finish_or_stream(
1393
1461
  result: Any, call: CallState, endpoint: str, request: Mapping[str, Any]
1394
1462
  ):
@@ -1403,7 +1471,7 @@ def _finish_or_stream(
1403
1471
  or (hasattr(result, "__enter__") and hasattr(result, "__exit__"))
1404
1472
  ):
1405
1473
  return SyncStream(result, call)
1406
- call.finish(result)
1474
+ _safe_finish(call, result)
1407
1475
  return result
1408
1476
 
1409
1477
 
@@ -8,4 +8,4 @@ import importlib.metadata
8
8
  try:
9
9
  SDK_VERSION = importlib.metadata.version("metergraph")
10
10
  except Exception:
11
- SDK_VERSION = "0.6.3"
11
+ SDK_VERSION = "0.6.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metergraph
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: Fire-and-forget LLM spend capture for Metergraph
5
5
  Author: Pioneer Square Labs
6
6
  License-Expression: Apache-2.0
File without changes
File without changes
File without changes