forked from retoor/devplacepy
feat: add issue_usage tracking and metrics for AI ticket enhancement and planning
Add `issue_usage` table with columns for user_uid, tokens, cost, and latency metrics, including a unique index on user_uid. Wire `accumulate_usage` into `enhance_ticket` and `generate_plan` to capture per-request AI usage, persist totals via `add_issue_usage` in both `IssueCreateService` and `PlanningReportService`, and expose aggregated usage as metric cards through `IssueTrackerService.collect_metrics`. Update planning API docs summary to reflect the new phased implementation document format.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.services.openai_gateway.usage import (
|
||||
USAGE_FIELDS,
|
||||
accumulate_usage,
|
||||
new_usage_totals,
|
||||
parse_usage_headers,
|
||||
usage_metric_cards,
|
||||
)
|
||||
|
||||
GATEWAY_HEADERS = {
|
||||
"X-Gateway-Cost-USD": "0.00010000",
|
||||
"X-Gateway-Model": "molodetz",
|
||||
"X-Gateway-Prompt-Tokens": "500",
|
||||
"X-Gateway-Completion-Tokens": "100",
|
||||
"X-Gateway-Total-Tokens": "600",
|
||||
"X-Gateway-Upstream-Latency-Ms": "400",
|
||||
"X-Gateway-Total-Latency-Ms": "450",
|
||||
}
|
||||
|
||||
|
||||
class FakeResp:
|
||||
def __init__(self, headers=None):
|
||||
self.headers = headers if headers is not None else {}
|
||||
|
||||
|
||||
def test_new_usage_totals_is_zeroed():
|
||||
totals = new_usage_totals()
|
||||
assert set(totals) == set(USAGE_FIELDS)
|
||||
assert all(value == 0 for value in totals.values())
|
||||
|
||||
|
||||
def test_accumulate_usage_sums_headers():
|
||||
totals = new_usage_totals()
|
||||
accumulate_usage(totals, FakeResp(GATEWAY_HEADERS))
|
||||
accumulate_usage(totals, FakeResp(GATEWAY_HEADERS))
|
||||
assert totals["calls"] == 2
|
||||
assert totals["prompt_tokens"] == 1000
|
||||
assert totals["completion_tokens"] == 200
|
||||
assert totals["total_tokens"] == 1200
|
||||
assert abs(totals["cost_usd"] - 0.0002) < 1e-9
|
||||
assert totals["upstream_latency_ms"] == 800.0
|
||||
assert totals["total_latency_ms"] == 900.0
|
||||
|
||||
|
||||
def test_accumulate_usage_is_noop_without_headers_or_totals():
|
||||
totals = new_usage_totals()
|
||||
accumulate_usage(None, FakeResp(GATEWAY_HEADERS))
|
||||
accumulate_usage(totals, FakeResp({}))
|
||||
accumulate_usage(totals, object())
|
||||
assert totals["calls"] == 0
|
||||
|
||||
|
||||
def test_parse_usage_headers_requires_cost_header():
|
||||
assert parse_usage_headers({}) is None
|
||||
assert parse_usage_headers(None) is None
|
||||
parsed = parse_usage_headers(GATEWAY_HEADERS)
|
||||
assert parsed["calls"] == 1
|
||||
assert parsed["total_tokens"] == 600
|
||||
|
||||
|
||||
def test_usage_metric_cards_labels_and_formatting():
|
||||
usage = {
|
||||
"calls": 4,
|
||||
"total_tokens": 9200,
|
||||
"prompt_tokens": 6000,
|
||||
"completion_tokens": 3200,
|
||||
"cost_usd": 0.0492,
|
||||
"avg_tokens": 2300.0,
|
||||
"avg_cost_usd": 0.0123,
|
||||
"avg_upstream_latency_ms": 4200.0,
|
||||
"avg_tokens_per_second": 190.5,
|
||||
}
|
||||
cards = usage_metric_cards(usage)
|
||||
labels = [card["label"] for card in cards]
|
||||
assert labels == [
|
||||
"AI calls",
|
||||
"Total tokens",
|
||||
"Prompt tokens",
|
||||
"Completion tokens",
|
||||
"Total cost",
|
||||
"Avg tokens/call",
|
||||
"Avg cost/call",
|
||||
"Avg latency",
|
||||
"Avg tokens/sec",
|
||||
]
|
||||
by_label = {card["label"]: card["value"] for card in cards}
|
||||
assert by_label["AI calls"] == 4
|
||||
assert by_label["Total cost"] == "$0.0492"
|
||||
assert by_label["Avg cost/call"] == "$0.012300"
|
||||
assert by_label["Avg latency"] == "4200ms"
|
||||
Reference in New Issue
Block a user