Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion app/services/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,56 @@ def _run(prompt: str, *, system: str = _SYSTEM) -> str:

try:
provider = get_provider()
return provider.complete(
content = provider.complete(
[
{"role": "system", "content": system},
{"role": "user", "content": prompt},
]
)
_record_analysis_usage(provider)
return content
except LLMProviderError as exc:
return f"[analysis unavailable: {exc}]"


def _record_analysis_usage(provider) -> None:
"""Record one GitHub-analysis completion in the central usage ledger.

``complete()`` returns text only, so token counts are recorded as estimated
zeroes rather than guessed. Accounting is best-effort and must never break
analysis; anonymous calls and calls outside an app context are not ledgered
because they have no owner.
"""
from flask import has_app_context

if not has_app_context():
return

from flask_login import current_user

if not getattr(current_user, "is_authenticated", False):
return

from app.services import usage_ledger

model = getattr(provider, "model", None)
if not model:
models = getattr(provider, "models", ()) or ()
model = models[0] if models else None
try:
usage_ledger.record_usage(
source=usage_ledger.SOURCE_ANALYSIS,
user=current_user,
provider=getattr(provider, "name", None),
model=model,
tokens_estimated=True,
status=usage_ledger.STATUS_SUCCESS,
)
except Exception:
# Never let accounting change the analysis result.
return


def _clip(text: str, limit: int = MAX_CONTEXT_CHARS) -> str:
if len(text) <= limit:
return text
Expand Down
46 changes: 46 additions & 0 deletions tests/test_analysis_usage_ledger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""GitHub analysis must record one usage-ledger row per LLM call (#366)."""

from __future__ import annotations

from types import SimpleNamespace

from flask_login import login_user

from app.models import UsageRecord
from app.services import analysis, budgets


class FakeProvider:
name = "mock"
model = "mock-1"
models = ("mock-1",)

def complete(self, messages, *, stream=False):
return "analysis result"

def chat(self, messages, **kwargs):
raise AssertionError("analysis must keep using the provider's complete() path")


def test_github_analysis_records_a_ledger_row(app, db, make_user, monkeypatch):
user = make_user(username="analyst", email="analyst@example.com")
monkeypatch.setattr(analysis, "get_provider", lambda: FakeProvider())
monkeypatch.setattr(
budgets,
"check_budgets",
lambda current_user: SimpleNamespace(allowed=True, message=""),
)
monkeypatch.setattr(budgets, "after_usage_recorded", lambda record: None)

with app.test_request_context("/"):
login_user(user)
assert analysis._run("review this change") == "analysis result"

row = UsageRecord.query.order_by(UsageRecord.id.desc()).first()
assert row is not None
assert row.source == "analysis"
assert row.user_id == user.id
assert row.provider == "mock"
assert row.model == "mock-1"
assert row.total_tokens == 0
assert row.tokens_estimated is True
Loading