mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
fix: bill PPQ.AI BYOK upstream_inference_cost + BYOK fee
PPQ.AI (BYOK) requests were billed at ~5% of their true cost because _resolve_usd_cost fell through to usage.cost (a small BYOK routing fee) instead of using cost_details.upstream_inference_cost (the real inference cost). The proxy operator absorbed the inference cost. The fix adds a BYOK-specific branch in _resolve_usd_cost: when is_byok is true and cost_details.upstream_inference_cost is present, bill upstream_inference_cost + byok_fee — what PPQ actually deducts from the balance. Non-BYOK providers (e.g. OpenRouter) are unaffected because their usage.cost already equals upstream_inference_cost. Regression tests mirror the live glm-5.2-fast request from GitHub issue #615, asserting the corrected billing (940,274 msats vs the old 45,202 msats — a 20.8× undercharge). Closes #615
This commit is contained in:
@@ -264,7 +264,17 @@ def _coerce_usd(value: object) -> float:
|
|||||||
def _resolve_usd_cost(usage_data: dict, response_data: dict) -> float:
|
def _resolve_usd_cost(usage_data: dict, response_data: dict) -> float:
|
||||||
"""Resolve USD cost with clear priority order.
|
"""Resolve USD cost with clear priority order.
|
||||||
|
|
||||||
Priority: cost_details.total_cost → total_cost → cost (in both usage and response).
|
Priority:
|
||||||
|
|
||||||
|
1. ``cost_details.total_cost``
|
||||||
|
2. ``cost_details.upstream_inference_cost`` (BYOK — see below)
|
||||||
|
3. ``total_cost`` → ``cost`` (in both usage and response)
|
||||||
|
|
||||||
|
**BYOK path (PPQ.AI):** when ``is_byok`` is true the ``usage.cost`` field
|
||||||
|
is only a small (~5 %) routing fee, not the inference cost. The real cost
|
||||||
|
lives in ``cost_details.upstream_inference_cost`` and the provider's
|
||||||
|
balance is debited by ``upstream_inference_cost + byok_fee``. Billing just
|
||||||
|
the fee under-charges by ~20×.
|
||||||
"""
|
"""
|
||||||
cost_details = usage_data.get("cost_details")
|
cost_details = usage_data.get("cost_details")
|
||||||
if isinstance(cost_details, dict):
|
if isinstance(cost_details, dict):
|
||||||
@@ -272,6 +282,18 @@ def _resolve_usd_cost(usage_data: dict, response_data: dict) -> float:
|
|||||||
if cost > 0:
|
if cost > 0:
|
||||||
return cost
|
return cost
|
||||||
|
|
||||||
|
# PPQ.AI BYOK: upstream_inference_cost is the real inference cost;
|
||||||
|
# usage.cost is only a ~5 % BYOK routing fee. Bill the sum — what PPQ
|
||||||
|
# actually deducts from the balance. For non-BYOK providers (e.g.
|
||||||
|
# OpenRouter) usage.cost already equals upstream_inference_cost, so we
|
||||||
|
# fall through to the normal ``cost`` lookup below.
|
||||||
|
upstream_cost = _coerce_usd(
|
||||||
|
cost_details.get("upstream_inference_cost")
|
||||||
|
)
|
||||||
|
if upstream_cost > 0 and usage_data.get("is_byok"):
|
||||||
|
byok_fee = _coerce_usd(usage_data.get("cost"))
|
||||||
|
return upstream_cost + byok_fee
|
||||||
|
|
||||||
for source in [usage_data, response_data]:
|
for source in [usage_data, response_data]:
|
||||||
if not isinstance(source, dict):
|
if not isinstance(source, dict):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -532,6 +532,72 @@ async def test_openrouter_upstream_inference_cost_components_are_used() -> None:
|
|||||||
assert result.input_msats + result.output_msats == result.total_msats == 4471
|
assert result.input_msats + result.output_msats == result.total_msats == 4471
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PPQ.AI BYOK: upstream_inference_cost + BYOK fee billing
|
||||||
|
#
|
||||||
|
# PPQ.AI (bring-your-own-key) returns a small ~5 % BYOK routing fee in
|
||||||
|
# ``usage.cost`` and the real inference cost in
|
||||||
|
# ``cost_details.upstream_inference_cost``. The old code billed only the fee,
|
||||||
|
# under-charging by ~20×. The fix bills ``upstream_inference_cost + byok_fee``
|
||||||
|
# — what PPQ actually deducts from the balance.
|
||||||
|
# Payload numbers are from a live ``glm-5.2-fast`` request (GitHub issue #615).
|
||||||
|
# ============================================================================
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ppq_byok_bills_upstream_inference_cost_plus_fee() -> None:
|
||||||
|
"""PPQ.AI BYOK must bill upstream_inference_cost + byok_fee, not just the
|
||||||
|
fee. Mirrors the live request from GitHub issue #615."""
|
||||||
|
response = {
|
||||||
|
"model": "glm-5.2-fast",
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 164371, # includes 159301 cached
|
||||||
|
"completion_tokens": 99,
|
||||||
|
"cost": 0.002260057305, # ~5% BYOK routing fee
|
||||||
|
"is_byok": True,
|
||||||
|
"prompt_tokens_details": {"cached_tokens": 159301},
|
||||||
|
"cost_details": {
|
||||||
|
"upstream_inference_cost": 0.04475361,
|
||||||
|
"upstream_inference_prompt_cost": 0.04410021,
|
||||||
|
"upstream_inference_completions_cost": 0.0006534,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result = await calculate_cost(response, max_cost=100000)
|
||||||
|
|
||||||
|
assert isinstance(result, CostData)
|
||||||
|
# The fix bills upstream_inference_cost + byok_fee (~0.047 USD → ~940k
|
||||||
|
# msats), not the fee alone (~0.0023 USD → ~45k msats). ~20× correction.
|
||||||
|
assert result.total_msats == 940274
|
||||||
|
assert result.input_msats + result.output_msats == result.total_msats
|
||||||
|
assert result.input_msats == 926546
|
||||||
|
assert result.output_msats == 13728
|
||||||
|
assert result.total_usd == pytest.approx(0.047013667305)
|
||||||
|
# Token normalisation (OpenAI dialect: cached included in prompt_tokens)
|
||||||
|
assert result.input_tokens == 5070 # 164371 - 159301
|
||||||
|
assert result.cache_read_input_tokens == 159301
|
||||||
|
assert result.output_tokens == 99
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ppq_byok_fee_only_would_undercharge() -> None:
|
||||||
|
"""Sanity check: billing only usage.cost (the BYOK fee) under-charges by
|
||||||
|
~20×. This documents the regression the fix prevents."""
|
||||||
|
response = {
|
||||||
|
"model": "glm-5.2-fast",
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 164371,
|
||||||
|
"completion_tokens": 99,
|
||||||
|
"cost": 0.002260057305, # BYOK fee only — no upstream_inference_cost
|
||||||
|
"is_byok": True,
|
||||||
|
"prompt_tokens_details": {"cached_tokens": 159301},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result = await calculate_cost(response, max_cost=100000)
|
||||||
|
|
||||||
|
assert isinstance(result, CostData)
|
||||||
|
# Without upstream_inference_cost, only the fee is billed — the old bug.
|
||||||
|
assert result.total_msats == 45202
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Test 13: Missing Usage Block
|
# Test 13: Missing Usage Block
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user