Commit 2df68a1
authored
fix: 0.8.3 — close silent zero-billing across langgraph + init-ordering (#42)
* fix: 0.8.3 — close silent zero-billing across langgraph + init-ordering
Three coordinated defenses against the same class of bug the 0.8.2
audit closed on the httpx path: llm_call events reaching the backend
without a model field were silently recorded as ≈$0 (backend
unwrap_or('default') + DEFAULT_RATE). 0.8.3 closes the langgraph
callback path and the init-ordering hazard, and promotes the
missing-model wire failure from WARN to fail-LOUD.
1. langgraph callback path (instrumentation/langgraph.py)
_extract_model_from_response now consults response.llm_output
FIRST — that's where langchain-openai 1.x puts the
date-suffixed model id (e.g. 'gpt-4.1-mini-2025-04-14'). The
previous chain led with response_metadata, which langchain 1.x
leaves empty on the AIMessage inside generations[0][0].message.
Without this promotion every OpenAI-via-LangChain 1.x call
silently zero-billed. Also adds a 'any key containing model'
sweep inside llm_output so non-OpenAI wrappers (proxies,
custom chat models) still get attributed.
2. Init-ordering hazard (instrumentation/auto.py)
patch_httpx's class-level __init__ wrap only catches Clients
created AFTER it is installed. Users that build
ChatOpenAI(...) before nullrun.init(api_key=...) get a
pre-existing httpx.Client that the patch never sees — those
clients keep the unpatched transport and emit nothing. We now
sweep gc.get_objects() once at patch install time and wrap
any pre-existing Client/AsyncClient whose transport isn't
already a NullRun*Transport. Idempotent via the existing
class-level marker.
3. Fail-LOUD wire tag (runtime.py)
runtime.track() now escalates the missing-model warning from
logger.warning to logger.error, bumps a runtime counter
(dropped_llm_call_no_model) for dashboards, and tags the wire
event with __missing_model: True so the backend's into_track_request
gate can reject with HTTP 422 instead of silently recording a
zero-cost call. The event is still sent (not fail-CLOSED) so the
backend can audit the rejection; the flag is wire-private and
stripped before persisting.
tests/contract/test_llm_call_model_wire.py pins all three
invariants: 7 unit tests for _extract_model_from_response
(every known langchain shape + non-OpenAI wrappers + empty-string
fallthrough), 3 tests for track()'s missing-model wire tagging
(ERROR + counter + __missing_model flag + non-llm_call silence),
and 2 tests for the eager-wrap sweep (pre-existing Client gets
wrapped, idempotent on re-patch).
* style(auto): sort stdlib imports (ruff I001)
`import gc` was inserted between `hashlib` and `json`; ruff's
isort rule wants `gc` before `hashlib` alphabetically.1 parent d4884a7 commit 2df68a1
5 files changed
Lines changed: 508 additions & 31 deletions
File tree
- src/nullrun
- instrumentation
- tests/contract
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
1038 | 1039 | | |
1039 | 1040 | | |
1040 | 1041 | | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
1041 | 1074 | | |
1042 | 1075 | | |
1043 | 1076 | | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
| 1116 | + | |
| 1117 | + | |
| 1118 | + | |
| 1119 | + | |
| 1120 | + | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
1044 | 1128 | | |
1045 | 1129 | | |
1046 | 1130 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
718 | 718 | | |
719 | 719 | | |
720 | 720 | | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
721 | 738 | | |
722 | 739 | | |
723 | | - | |
724 | | - | |
725 | | - | |
726 | | - | |
727 | | - | |
728 | | - | |
729 | | - | |
730 | | - | |
731 | | - | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
732 | 753 | | |
733 | | - | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
734 | 785 | | |
735 | 786 | | |
736 | 787 | | |
737 | 788 | | |
738 | 789 | | |
739 | 790 | | |
740 | | - | |
| 791 | + | |
741 | 792 | | |
742 | 793 | | |
743 | 794 | | |
| |||
751 | 802 | | |
752 | 803 | | |
753 | 804 | | |
754 | | - | |
755 | | - | |
756 | | - | |
757 | | - | |
758 | | - | |
759 | | - | |
760 | | - | |
761 | 805 | | |
762 | 806 | | |
763 | 807 | | |
764 | 808 | | |
765 | 809 | | |
766 | 810 | | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
767 | 824 | | |
768 | 825 | | |
769 | 826 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1430 | 1430 | | |
1431 | 1431 | | |
1432 | 1432 | | |
1433 | | - | |
1434 | | - | |
1435 | | - | |
1436 | | - | |
1437 | | - | |
1438 | | - | |
1439 | | - | |
1440 | | - | |
1441 | | - | |
1442 | | - | |
1443 | | - | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
| 1441 | + | |
| 1442 | + | |
| 1443 | + | |
| 1444 | + | |
| 1445 | + | |
| 1446 | + | |
| 1447 | + | |
| 1448 | + | |
| 1449 | + | |
| 1450 | + | |
| 1451 | + | |
| 1452 | + | |
| 1453 | + | |
| 1454 | + | |
| 1455 | + | |
| 1456 | + | |
1444 | 1457 | | |
1445 | | - | |
| 1458 | + | |
1446 | 1459 | | |
1447 | | - | |
| 1460 | + | |
1448 | 1461 | | |
1449 | 1462 | | |
| 1463 | + | |
| 1464 | + | |
1450 | 1465 | | |
1451 | 1466 | | |
1452 | 1467 | | |
| |||
Whitespace-only changes.
0 commit comments