metergraph-core 0.1.4__tar.gz → 0.2.0__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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metergraph-core
3
- Version: 0.1.4
4
- Summary: Reusable MeterGraph catalog and deterministic token-cost pricing engine
3
+ Version: 0.2.0
4
+ Summary: Reusable MeterGraph catalog and deterministic billing engine
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
@@ -12,14 +12,15 @@ Requires-Dist: pytest>=8; extra == "dev"
12
12
 
13
13
  # metergraph-core
14
14
 
15
- Reusable catalog and deterministic token-cost pricing engine for MeterGraph.
15
+ Reusable catalog and deterministic billing engine for MeterGraph.
16
16
 
17
17
  `metergraph-core` owns the public effective-dated model catalog and the pricing
18
18
  logic shared across MeterGraph systems: catalog parsing and validation, provider
19
19
  and model alias resolution, channel and region selection, input/output/cache/
20
20
  batch/long-context pricing rules, deterministic cost calculation with reason
21
21
  codes, stable logical price identifiers, and catalog version and content-hash
22
- reporting.
22
+ reporting. It also owns the pure decision that selects a qualified
23
+ gateway-reported charge or a catalog estimate as the effective call cost.
23
24
 
24
25
  It does not own HTTP routes, database access, migrations, authentication,
25
26
  tenancy, ingest, dashboard code, or any hosted-only concern, and it never reads
@@ -83,19 +84,51 @@ exists. It never substitutes a direct-provider price for a gateway price.
83
84
  `LoadedCatalog.pricing_verified_at` records when the bundled catalog was last
84
85
  checked against its linked provider sources.
85
86
 
87
+ ## Billing evidence
88
+
89
+ Servers can pass content-blind, already-extracted gateway fields through the
90
+ shared trust boundary and combine them with a catalog result:
91
+
92
+ ```python
93
+ from metergraph_core import normalize_gateway_evidence, resolve_billing
94
+
95
+ evidence = normalize_gateway_evidence({
96
+ "gateway": "openrouter",
97
+ "endpoint": "chat.completions",
98
+ "reported_cost_usd": "0.00482",
99
+ "reported_cost_source": "openrouter.usage.cost",
100
+ })
101
+ decision = resolve_billing(result, evidence)
102
+ print(decision.cost_usd, decision.cost_provenance)
103
+ ```
104
+
105
+ The initial qualified contract is OpenRouter Chat Completions. A finite,
106
+ non-negative `openrouter.usage.cost` value, including zero, takes precedence
107
+ over a catalog estimate. The decision retains both values and never adds the
108
+ separately reported upstream inference cost to the OpenRouter account charge.
109
+ Unsupported or malformed evidence falls back to the catalog result.
110
+
111
+ The billing module validates only gateway, endpoint, fixed source names, and
112
+ decimal cost values. It does not inspect provider response content or own SDK
113
+ capture, HTTP, timestamps, trace context, persistence, or tenant behavior.
114
+
86
115
  ## Public API
87
116
 
88
117
  ```python
89
118
  from metergraph_core import (
90
119
  Alias,
120
+ BillingDecision,
91
121
  CatalogError,
92
122
  CatalogSnapshot,
93
123
  CostResult,
124
+ GatewayBillingEvidence,
94
125
  LoadedCatalog,
95
126
  Price,
96
127
  ResolvedPrice,
97
128
  load_catalog,
129
+ normalize_gateway_evidence,
98
130
  parse_catalog,
131
+ resolve_billing,
99
132
  )
100
133
  ```
101
134
 
@@ -1,13 +1,14 @@
1
1
  # metergraph-core
2
2
 
3
- Reusable catalog and deterministic token-cost pricing engine for MeterGraph.
3
+ Reusable catalog and deterministic billing engine for MeterGraph.
4
4
 
5
5
  `metergraph-core` owns the public effective-dated model catalog and the pricing
6
6
  logic shared across MeterGraph systems: catalog parsing and validation, provider
7
7
  and model alias resolution, channel and region selection, input/output/cache/
8
8
  batch/long-context pricing rules, deterministic cost calculation with reason
9
9
  codes, stable logical price identifiers, and catalog version and content-hash
10
- reporting.
10
+ reporting. It also owns the pure decision that selects a qualified
11
+ gateway-reported charge or a catalog estimate as the effective call cost.
11
12
 
12
13
  It does not own HTTP routes, database access, migrations, authentication,
13
14
  tenancy, ingest, dashboard code, or any hosted-only concern, and it never reads
@@ -71,19 +72,51 @@ exists. It never substitutes a direct-provider price for a gateway price.
71
72
  `LoadedCatalog.pricing_verified_at` records when the bundled catalog was last
72
73
  checked against its linked provider sources.
73
74
 
75
+ ## Billing evidence
76
+
77
+ Servers can pass content-blind, already-extracted gateway fields through the
78
+ shared trust boundary and combine them with a catalog result:
79
+
80
+ ```python
81
+ from metergraph_core import normalize_gateway_evidence, resolve_billing
82
+
83
+ evidence = normalize_gateway_evidence({
84
+ "gateway": "openrouter",
85
+ "endpoint": "chat.completions",
86
+ "reported_cost_usd": "0.00482",
87
+ "reported_cost_source": "openrouter.usage.cost",
88
+ })
89
+ decision = resolve_billing(result, evidence)
90
+ print(decision.cost_usd, decision.cost_provenance)
91
+ ```
92
+
93
+ The initial qualified contract is OpenRouter Chat Completions. A finite,
94
+ non-negative `openrouter.usage.cost` value, including zero, takes precedence
95
+ over a catalog estimate. The decision retains both values and never adds the
96
+ separately reported upstream inference cost to the OpenRouter account charge.
97
+ Unsupported or malformed evidence falls back to the catalog result.
98
+
99
+ The billing module validates only gateway, endpoint, fixed source names, and
100
+ decimal cost values. It does not inspect provider response content or own SDK
101
+ capture, HTTP, timestamps, trace context, persistence, or tenant behavior.
102
+
74
103
  ## Public API
75
104
 
76
105
  ```python
77
106
  from metergraph_core import (
78
107
  Alias,
108
+ BillingDecision,
79
109
  CatalogError,
80
110
  CatalogSnapshot,
81
111
  CostResult,
112
+ GatewayBillingEvidence,
82
113
  LoadedCatalog,
83
114
  Price,
84
115
  ResolvedPrice,
85
116
  load_catalog,
117
+ normalize_gateway_evidence,
86
118
  parse_catalog,
119
+ resolve_billing,
87
120
  )
88
121
  ```
89
122
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "metergraph-core"
3
- version = "0.1.4"
4
- description = "Reusable MeterGraph catalog and deterministic token-cost pricing engine"
3
+ version = "0.2.0"
4
+ description = "Reusable MeterGraph catalog and deterministic billing engine"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
7
7
  requires-python = ">=3.10"
@@ -8,6 +8,12 @@ from .catalog import (
8
8
  ResolvedPrice,
9
9
  direct_channel_for_provider,
10
10
  )
11
+ from .billing import (
12
+ BillingDecision,
13
+ GatewayBillingEvidence,
14
+ normalize_gateway_evidence,
15
+ resolve_billing,
16
+ )
11
17
  from .loader import (
12
18
  CatalogError,
13
19
  LoadedCatalog,
@@ -17,13 +23,17 @@ from .loader import (
17
23
 
18
24
  __all__ = [
19
25
  "Alias",
26
+ "BillingDecision",
20
27
  "CatalogError",
21
28
  "CatalogSnapshot",
22
29
  "CostResult",
23
30
  "LoadedCatalog",
31
+ "GatewayBillingEvidence",
24
32
  "Price",
25
33
  "ResolvedPrice",
26
34
  "direct_channel_for_provider",
27
35
  "load_catalog",
36
+ "normalize_gateway_evidence",
28
37
  "parse_catalog",
38
+ "resolve_billing",
29
39
  ]
@@ -0,0 +1,135 @@
1
+ """Pure selection of effective LLM cost from catalog and gateway evidence."""
2
+
3
+ from dataclasses import dataclass
4
+ from decimal import Decimal, InvalidOperation
5
+ from typing import Any, Mapping
6
+
7
+ from .catalog import CostResult
8
+
9
+ _OPENROUTER = "openrouter"
10
+ _CHAT_COMPLETIONS = "chat.completions"
11
+ _OPENROUTER_COST_SOURCE = "openrouter.usage.cost"
12
+ _OPENROUTER_UPSTREAM_COST_SOURCE = (
13
+ "openrouter.usage.cost_details.upstream_inference_cost"
14
+ )
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class GatewayBillingEvidence:
19
+ """Validated gateway billing fields from one content-blind call row."""
20
+
21
+ gateway: str | None
22
+ endpoint: str | None
23
+ reported_cost_usd: Decimal | None
24
+ reported_cost_source: str | None
25
+ reported_upstream_cost_usd: Decimal | None
26
+ reported_upstream_cost_source: str | None
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class BillingDecision:
31
+ """Effective cost plus the independent evidence used to select it."""
32
+
33
+ cost_usd: Decimal | None
34
+ cost_status: str
35
+ cost_provenance: str
36
+ reported_cost_usd: Decimal | None
37
+ reported_upstream_cost_usd: Decimal | None
38
+ catalog_cost_usd: Decimal | None
39
+ catalog_price_id: str | None
40
+ catalog_reasons: tuple[str, ...]
41
+ cost_discrepancy_status: str | None = None
42
+
43
+
44
+ def _text(value: Any, *, limit: int = 128) -> str | None:
45
+ if not isinstance(value, str):
46
+ return None
47
+ stripped = value.strip()
48
+ return stripped[:limit] if stripped else None
49
+
50
+
51
+ def _non_negative_decimal(value: Any) -> Decimal | None:
52
+ if value is None or isinstance(value, bool):
53
+ return None
54
+ try:
55
+ result = Decimal(str(value))
56
+ except (InvalidOperation, ValueError, TypeError):
57
+ return None
58
+ if not result.is_finite() or result < 0:
59
+ return None
60
+ return result
61
+
62
+
63
+ def normalize_gateway_evidence(row: Mapping[str, Any]) -> GatewayBillingEvidence:
64
+ """Allowlist and validate gateway billing evidence from a call mapping."""
65
+
66
+ gateway = _text(row.get("gateway"), limit=64)
67
+ if gateway is not None:
68
+ gateway = gateway.lower()
69
+ endpoint = _text(row.get("endpoint"), limit=64)
70
+ reported_source = _text(row.get("reported_cost_source"))
71
+ upstream_source = _text(row.get("reported_upstream_cost_source"))
72
+
73
+ return GatewayBillingEvidence(
74
+ gateway=gateway,
75
+ endpoint=endpoint,
76
+ reported_cost_usd=_non_negative_decimal(row.get("reported_cost_usd")),
77
+ reported_cost_source=reported_source,
78
+ reported_upstream_cost_usd=_non_negative_decimal(
79
+ row.get("reported_upstream_cost_usd")
80
+ ),
81
+ reported_upstream_cost_source=upstream_source,
82
+ )
83
+
84
+
85
+ def _is_openrouter_chat_completions(evidence: GatewayBillingEvidence) -> bool:
86
+ return (
87
+ evidence.gateway == _OPENROUTER
88
+ and evidence.endpoint == _CHAT_COMPLETIONS
89
+ )
90
+
91
+
92
+ def resolve_billing(
93
+ catalog_result: CostResult,
94
+ evidence: GatewayBillingEvidence,
95
+ ) -> BillingDecision:
96
+ """Select effective cost without combining independent reported amounts."""
97
+
98
+ qualified_openrouter = _is_openrouter_chat_completions(evidence)
99
+ reported_cost = (
100
+ evidence.reported_cost_usd
101
+ if qualified_openrouter
102
+ and evidence.reported_cost_source == _OPENROUTER_COST_SOURCE
103
+ else None
104
+ )
105
+ upstream_cost = (
106
+ evidence.reported_upstream_cost_usd
107
+ if qualified_openrouter
108
+ and evidence.reported_upstream_cost_source
109
+ == _OPENROUTER_UPSTREAM_COST_SOURCE
110
+ else None
111
+ )
112
+
113
+ if reported_cost is not None:
114
+ cost_usd = reported_cost
115
+ cost_status = "priced"
116
+ provenance = "gateway_reported"
117
+ elif catalog_result.cost_usd is not None:
118
+ cost_usd = catalog_result.cost_usd
119
+ cost_status = catalog_result.status
120
+ provenance = "catalog"
121
+ else:
122
+ cost_usd = None
123
+ cost_status = catalog_result.status
124
+ provenance = "none"
125
+
126
+ return BillingDecision(
127
+ cost_usd=cost_usd,
128
+ cost_status=cost_status,
129
+ cost_provenance=provenance,
130
+ reported_cost_usd=reported_cost,
131
+ reported_upstream_cost_usd=upstream_cost,
132
+ catalog_cost_usd=catalog_result.cost_usd,
133
+ catalog_price_id=catalog_result.price_id,
134
+ catalog_reasons=catalog_result.reasons,
135
+ )
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metergraph-core
3
- Version: 0.1.4
4
- Summary: Reusable MeterGraph catalog and deterministic token-cost pricing engine
3
+ Version: 0.2.0
4
+ Summary: Reusable MeterGraph catalog and deterministic billing engine
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
@@ -12,14 +12,15 @@ Requires-Dist: pytest>=8; extra == "dev"
12
12
 
13
13
  # metergraph-core
14
14
 
15
- Reusable catalog and deterministic token-cost pricing engine for MeterGraph.
15
+ Reusable catalog and deterministic billing engine for MeterGraph.
16
16
 
17
17
  `metergraph-core` owns the public effective-dated model catalog and the pricing
18
18
  logic shared across MeterGraph systems: catalog parsing and validation, provider
19
19
  and model alias resolution, channel and region selection, input/output/cache/
20
20
  batch/long-context pricing rules, deterministic cost calculation with reason
21
21
  codes, stable logical price identifiers, and catalog version and content-hash
22
- reporting.
22
+ reporting. It also owns the pure decision that selects a qualified
23
+ gateway-reported charge or a catalog estimate as the effective call cost.
23
24
 
24
25
  It does not own HTTP routes, database access, migrations, authentication,
25
26
  tenancy, ingest, dashboard code, or any hosted-only concern, and it never reads
@@ -83,19 +84,51 @@ exists. It never substitutes a direct-provider price for a gateway price.
83
84
  `LoadedCatalog.pricing_verified_at` records when the bundled catalog was last
84
85
  checked against its linked provider sources.
85
86
 
87
+ ## Billing evidence
88
+
89
+ Servers can pass content-blind, already-extracted gateway fields through the
90
+ shared trust boundary and combine them with a catalog result:
91
+
92
+ ```python
93
+ from metergraph_core import normalize_gateway_evidence, resolve_billing
94
+
95
+ evidence = normalize_gateway_evidence({
96
+ "gateway": "openrouter",
97
+ "endpoint": "chat.completions",
98
+ "reported_cost_usd": "0.00482",
99
+ "reported_cost_source": "openrouter.usage.cost",
100
+ })
101
+ decision = resolve_billing(result, evidence)
102
+ print(decision.cost_usd, decision.cost_provenance)
103
+ ```
104
+
105
+ The initial qualified contract is OpenRouter Chat Completions. A finite,
106
+ non-negative `openrouter.usage.cost` value, including zero, takes precedence
107
+ over a catalog estimate. The decision retains both values and never adds the
108
+ separately reported upstream inference cost to the OpenRouter account charge.
109
+ Unsupported or malformed evidence falls back to the catalog result.
110
+
111
+ The billing module validates only gateway, endpoint, fixed source names, and
112
+ decimal cost values. It does not inspect provider response content or own SDK
113
+ capture, HTTP, timestamps, trace context, persistence, or tenant behavior.
114
+
86
115
  ## Public API
87
116
 
88
117
  ```python
89
118
  from metergraph_core import (
90
119
  Alias,
120
+ BillingDecision,
91
121
  CatalogError,
92
122
  CatalogSnapshot,
93
123
  CostResult,
124
+ GatewayBillingEvidence,
94
125
  LoadedCatalog,
95
126
  Price,
96
127
  ResolvedPrice,
97
128
  load_catalog,
129
+ normalize_gateway_evidence,
98
130
  parse_catalog,
131
+ resolve_billing,
99
132
  )
100
133
  ```
101
134
 
@@ -2,6 +2,7 @@ MANIFEST.in
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/metergraph_core/__init__.py
5
+ src/metergraph_core/billing.py
5
6
  src/metergraph_core/catalog.py
6
7
  src/metergraph_core/loader.py
7
8
  src/metergraph_core.egg-info/PKG-INFO