diff --git a/cli/mcp_metering.py b/cli/mcp_metering.py index 2851d49..2f3f466 100644 --- a/cli/mcp_metering.py +++ b/cli/mcp_metering.py @@ -22,6 +22,10 @@ def _account_id() -> str: return os.environ.get("NUNCHI_ACCOUNT_ID", "").strip() +def _user_id() -> str: + return os.environ.get("NUNCHI_USER_ID", "").strip() + + def _subscription_id() -> str: return os.environ.get("NUNCHI_SUBSCRIPTION_ID", "").strip() @@ -72,6 +76,8 @@ def upload_rows(rows: list[dict[str, Any]]) -> dict[str, Any]: body = { "account_id": account_id, "accountId": account_id, + "user_id": _user_id() or None, + "userId": _user_id() or None, "subscription_id": _subscription_id() or None, "subscriptionId": _subscription_id() or None, "plan_id": _plan_id(), @@ -118,6 +124,11 @@ def report_inference_cost( cached_tokens: Optional[int] = None, cache_savings_usd: Optional[Any] = None, model: Optional[str] = None, + provider: Optional[str] = None, + requested_model: Optional[str] = None, + route: Optional[str] = None, + decision_call_id: Optional[str] = None, + price_source: Optional[str] = None, ) -> dict[str, Any]: if not metering_enabled(): return {"ok": False, "skipped": "metering_not_configured"} @@ -128,12 +139,28 @@ def report_inference_cost( "input_tokens": int(input_tokens or 0), "output_tokens": int(output_tokens or 0), "ts": ts, + "account_id": _account_id(), + "subscription_id": _subscription_id() or None, + "plan_id": _plan_id(), } + if _user_id(): + row["user_id"] = _user_id() if cached_tokens is not None: row["cached_tokens"] = int(cached_tokens) if cache_savings_usd is not None: row["cache_savings_usd"] = cache_savings_usd if model: row["model"] = model + row["resolved_model"] = model + if provider: + row["provider"] = provider + if requested_model: + row["requested_model"] = requested_model + if route: + row["route"] = route + if decision_call_id: + row["decision_call_id"] = decision_call_id + if price_source: + row["pricing_snapshot_source"] = price_source row_id = _row_id("cost", row) return upload_rows([{"row_id": row_id, "metric_type": "cost", "row": row}]) diff --git a/modules/cost_metering.py b/modules/cost_metering.py index a1c5879..d4023cf 100644 --- a/modules/cost_metering.py +++ b/modules/cost_metering.py @@ -297,6 +297,11 @@ def record_llm_call( cached_tokens=cached_tokens, cache_savings_usd=str(cache_savings) if cache_savings is not None else None, model=resolved_model, + provider=provider, + requested_model=requested_model, + route=route, + decision_call_id=decision_call_id, + price_source=row["pricing_snapshot_source"], ) except Exception: pass diff --git a/tests/test_mcp_metering.py b/tests/test_mcp_metering.py index a4691d8..96732fd 100644 --- a/tests/test_mcp_metering.py +++ b/tests/test_mcp_metering.py @@ -1,4 +1,4 @@ -from cli.mcp_metering import _tool_bucket, report_tool_call, upload_rows +from cli.mcp_metering import _tool_bucket, report_inference_cost, report_tool_call, upload_rows def test_tool_bucket(): @@ -42,3 +42,58 @@ def fake_urlopen(req, timeout=10): body = __import__("json").loads(captured["body"]) assert body["accountId"] == "acct_1" assert body["rows"][0]["metric_type"] == "mcp_tool" + + +def test_report_inference_cost_includes_subscription_identity(monkeypatch): + captured = {} + + class FakeResponse: + def read(self): + return b'{"ok": true}' + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def fake_urlopen(req, timeout=10): + captured["body"] = req.data.decode("utf-8") + return FakeResponse() + + monkeypatch.setenv("NUNCHI_METERING_URL", "https://pair.example/api/metering/usage") + monkeypatch.setenv("NUNCHI_METERING_TOKEN", "token") + monkeypatch.setenv("NUNCHI_USER_ID", "user_1") + monkeypatch.setenv("NUNCHI_ACCOUNT_ID", "acct_1") + monkeypatch.setenv("NUNCHI_SUBSCRIPTION_ID", "sub_1") + monkeypatch.setenv("NUNCHI_PLAN_ID", "hosted-mcp-inference-starter") + monkeypatch.setattr("cli.mcp_metering.urllib.request.urlopen", fake_urlopen) + + result = report_inference_cost( + inference_usd="0.0042", + input_tokens=12, + output_tokens=2, + cached_tokens=3, + cache_savings_usd="0.0001", + model="openai/gpt-4.1-mini", + provider="openrouter", + requested_model="openrouter/auto", + route="openrouter/auto", + decision_call_id="decision-1", + price_source="openrouter:usage.cost", + ) + + assert result["ok"] is True + body = __import__("json").loads(captured["body"]) + row = body["rows"][0]["row"] + assert body["accountId"] == "acct_1" + assert body["subscriptionId"] == "sub_1" + assert row["user_id"] == "user_1" + assert row["account_id"] == "acct_1" + assert row["subscription_id"] == "sub_1" + assert row["plan_id"] == "hosted-mcp-inference-starter" + assert row["inference_usd"] == "0.0042" + assert row["provider"] == "openrouter" + assert row["requested_model"] == "openrouter/auto" + assert row["resolved_model"] == "openai/gpt-4.1-mini" + assert row["decision_call_id"] == "decision-1"