llmapi-tracker 1.0.0__tar.gz → 1.0.2__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.
Files changed (53) hide show
  1. {llmapi_tracker-1.0.0/llmapi_tracker.egg-info → llmapi_tracker-1.0.2}/PKG-INFO +21 -12
  2. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/README.md +17 -11
  3. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/database.py +0 -2
  4. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/frontend.py +5 -3
  5. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/launcher.py +5 -1
  6. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/main.py +0 -2
  7. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/models.py +18 -7
  8. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/paths.py +0 -1
  9. llmapi_tracker-1.0.2/backend/app/routes/__init__.py +0 -0
  10. llmapi_tracker-1.0.2/backend/app/routes/analytics.py +171 -0
  11. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/routes/models.py +4 -1
  12. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/routes/pricing.py +3 -1
  13. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/routes/projects.py +3 -1
  14. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/routes/usage.py +1 -1
  15. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/schemas.py +19 -19
  16. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/services/usage_service.py +10 -4
  17. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2/llmapi_tracker.egg-info}/PKG-INFO +21 -12
  18. llmapi_tracker-1.0.2/pyproject.toml +83 -0
  19. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/tests/test_analytics_routes.py +0 -2
  20. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/tests/test_paths.py +4 -3
  21. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/tests/test_routes.py +16 -6
  22. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/tests/test_tracker.py +3 -1
  23. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/tests/test_usage_service.py +17 -7
  24. llmapi_tracker-1.0.0/backend/app/routes/__init__.py +0 -1
  25. llmapi_tracker-1.0.0/backend/app/routes/analytics.py +0 -91
  26. llmapi_tracker-1.0.0/pyproject.toml +0 -45
  27. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/LICENSE +0 -0
  28. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/__init__.py +0 -0
  29. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/__init__.py +0 -0
  30. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/routes/providers.py +0 -0
  31. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/services/__init__.py +0 -0
  32. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/services/cost_service.py +0 -0
  33. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/static/assets/index-3YVy8dav.css +0 -0
  34. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/static/assets/index-jlcpybVb.js +0 -0
  35. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/backend/app/static/index.html +0 -0
  36. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/llmapi_tracker.egg-info/SOURCES.txt +0 -0
  37. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/llmapi_tracker.egg-info/dependency_links.txt +0 -0
  38. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/llmapi_tracker.egg-info/entry_points.txt +0 -0
  39. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/llmapi_tracker.egg-info/requires.txt +0 -0
  40. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/llmapi_tracker.egg-info/top_level.txt +0 -0
  41. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/__init__.py +0 -0
  42. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/client.py +0 -0
  43. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/models.py +0 -0
  44. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/pricing.py +0 -0
  45. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/providers/__init__.py +0 -0
  46. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/providers/gemini.py +0 -0
  47. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/providers/openai.py +0 -0
  48. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/py.typed +0 -0
  49. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/tracker.py +0 -0
  50. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/sdk/api_tracker/transport.py +0 -0
  51. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/setup.cfg +0 -0
  52. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/tests/test_cost_service.py +0 -0
  53. {llmapi_tracker-1.0.0 → llmapi_tracker-1.0.2}/tests/test_provider_model_routes.py +0 -0
@@ -1,9 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llmapi-tracker
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Local-first API usage and cost tracking platform
5
5
  Author: API Tracker
6
6
  License: MIT
7
+ Project-URL: Homepage, https://github.com/Lns9777/API-Usage-Tracker
8
+ Project-URL: Repository, https://github.com/Lns9777/API-Usage-Tracker
9
+ Project-URL: Issues, https://github.com/Lns9777/API-Usage-Tracker/issues
7
10
  Requires-Python: >=3.10
8
11
  Description-Content-Type: text/markdown
9
12
  License-File: LICENSE
@@ -27,24 +30,24 @@ Requires-Dist: build>=1.2; extra == "dev"
27
30
  Requires-Dist: twine>=5.1; extra == "dev"
28
31
  Dynamic: license-file
29
32
 
30
- # API Tracker
33
+ # llmapi-tracker
31
34
 
32
35
  ## What API Tracker Is
33
36
 
34
- API Tracker is a local-first API usage and cost observability tool. It records request volume, token usage, latency, status, and versioned pricing for models from providers such as OpenAI and Gemini.
37
+ `llmapi-tracker` is a local-first API usage and cost observability platform for LLM applications. It records request counts, token usage, latency, errors, and versioned pricing for provider models such as OpenAI and Gemini.
35
38
 
36
- The project is built to keep data local by default. It stores usage and pricing in a local SQLite database and does not persist prompts, responses, or raw API keys in the application database.
39
+ The project is designed to keep data local by default. It stores usage and pricing in a local SQLite database and does not persist prompts, responses, or raw API keys in the application database.
37
40
 
38
41
  ## Features
39
42
 
40
43
  - Local FastAPI backend with SQLite storage
41
44
  - Versioned pricing records for historical cost tracking
42
45
  - Usage capture for input, output, thinking, cached, and total tokens
43
- - Cost calculation per request and across analytics views
46
+ - Request-level and analytics-level cost calculation
44
47
  - React + Vite dashboard for usage, analytics, providers, models, and pricing
45
48
  - SDK wrappers for OpenAI and Gemini
46
49
  - Packaged launcher command that starts the local app and opens the browser
47
- - Open-source repository setup with tests, CI, and release automation
50
+ - Open-source repository with tests, CI, and release automation
48
51
 
49
52
  ## Installation
50
53
 
@@ -65,10 +68,16 @@ npm install
65
68
 
66
69
  ### Package install
67
70
 
68
- If you install this project as a package, the launcher command is:
71
+ ```bash
72
+ pip install llmapi-tracker
73
+ ```
74
+
75
+ If you want optional provider SDK support:
69
76
 
70
77
  ```bash
71
- api-tracker
78
+ pip install "llmapi-tracker[openai]"
79
+ pip install "llmapi-tracker[gemini]"
80
+ pip install "llmapi-tracker[all]"
72
81
  ```
73
82
 
74
83
  ## Starting the Application
@@ -88,7 +97,7 @@ npm run dev
88
97
 
89
98
  ### Packaged app
90
99
 
91
- After building the frontend assets and packaging them into the backend static directory, run:
100
+ After the frontend assets are built and packaged into the backend static directory:
92
101
 
93
102
  ```bash
94
103
  api-tracker
@@ -102,7 +111,7 @@ The launcher starts the backend server and opens the browser to the local dashbo
102
111
 
103
112
  ```python
104
113
  import os
105
- from sdk.api_tracker import APITracker
114
+ from api_tracker import APITracker
106
115
 
107
116
  tracker = APITracker(
108
117
  project="Pharmacy-AI",
@@ -201,7 +210,7 @@ python scripts/verify_frontend_assets.py
201
210
 
202
211
  ## Contributing
203
212
 
204
- This repository is intended to be open source and contributor friendly.
213
+ `llmapi-tracker` is intended to be open source and contributor friendly.
205
214
 
206
215
  Contribution files:
207
216
 
@@ -221,5 +230,5 @@ Contribution expectations:
221
230
 
222
231
  ## License
223
232
 
224
- API Tracker is released under the MIT License. See [`LICENSE`](L:/Personal/APITracker/LICENSE).
233
+ `llmapi-tracker` is released under the MIT License. See [`LICENSE`](L:/Personal/APITracker/LICENSE).
225
234
 
@@ -1,21 +1,21 @@
1
- # API Tracker
1
+ # llmapi-tracker
2
2
 
3
3
  ## What API Tracker Is
4
4
 
5
- API Tracker is a local-first API usage and cost observability tool. It records request volume, token usage, latency, status, and versioned pricing for models from providers such as OpenAI and Gemini.
5
+ `llmapi-tracker` is a local-first API usage and cost observability platform for LLM applications. It records request counts, token usage, latency, errors, and versioned pricing for provider models such as OpenAI and Gemini.
6
6
 
7
- The project is built to keep data local by default. It stores usage and pricing in a local SQLite database and does not persist prompts, responses, or raw API keys in the application database.
7
+ The project is designed to keep data local by default. It stores usage and pricing in a local SQLite database and does not persist prompts, responses, or raw API keys in the application database.
8
8
 
9
9
  ## Features
10
10
 
11
11
  - Local FastAPI backend with SQLite storage
12
12
  - Versioned pricing records for historical cost tracking
13
13
  - Usage capture for input, output, thinking, cached, and total tokens
14
- - Cost calculation per request and across analytics views
14
+ - Request-level and analytics-level cost calculation
15
15
  - React + Vite dashboard for usage, analytics, providers, models, and pricing
16
16
  - SDK wrappers for OpenAI and Gemini
17
17
  - Packaged launcher command that starts the local app and opens the browser
18
- - Open-source repository setup with tests, CI, and release automation
18
+ - Open-source repository with tests, CI, and release automation
19
19
 
20
20
  ## Installation
21
21
 
@@ -36,10 +36,16 @@ npm install
36
36
 
37
37
  ### Package install
38
38
 
39
- If you install this project as a package, the launcher command is:
39
+ ```bash
40
+ pip install llmapi-tracker
41
+ ```
42
+
43
+ If you want optional provider SDK support:
40
44
 
41
45
  ```bash
42
- api-tracker
46
+ pip install "llmapi-tracker[openai]"
47
+ pip install "llmapi-tracker[gemini]"
48
+ pip install "llmapi-tracker[all]"
43
49
  ```
44
50
 
45
51
  ## Starting the Application
@@ -59,7 +65,7 @@ npm run dev
59
65
 
60
66
  ### Packaged app
61
67
 
62
- After building the frontend assets and packaging them into the backend static directory, run:
68
+ After the frontend assets are built and packaged into the backend static directory:
63
69
 
64
70
  ```bash
65
71
  api-tracker
@@ -73,7 +79,7 @@ The launcher starts the backend server and opens the browser to the local dashbo
73
79
 
74
80
  ```python
75
81
  import os
76
- from sdk.api_tracker import APITracker
82
+ from api_tracker import APITracker
77
83
 
78
84
  tracker = APITracker(
79
85
  project="Pharmacy-AI",
@@ -172,7 +178,7 @@ python scripts/verify_frontend_assets.py
172
178
 
173
179
  ## Contributing
174
180
 
175
- This repository is intended to be open source and contributor friendly.
181
+ `llmapi-tracker` is intended to be open source and contributor friendly.
176
182
 
177
183
  Contribution files:
178
184
 
@@ -192,5 +198,5 @@ Contribution expectations:
192
198
 
193
199
  ## License
194
200
 
195
- API Tracker is released under the MIT License. See [`LICENSE`](L:/Personal/APITracker/LICENSE).
201
+ `llmapi-tracker` is released under the MIT License. See [`LICENSE`](L:/Personal/APITracker/LICENSE).
196
202
 
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
-
5
3
  from sqlalchemy import create_engine
6
4
  from sqlalchemy.orm import declarative_base, sessionmaker
7
5
 
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from pathlib import Path
4
-
5
3
  from fastapi import APIRouter
6
4
  from fastapi.responses import FileResponse
7
5
  from fastapi.staticfiles import StaticFiles
@@ -12,7 +10,11 @@ from .paths import get_frontend_dist_dir
12
10
  def mount_frontend(app, prefix: str = "") -> None:
13
11
  static_dir = get_frontend_dist_dir()
14
12
  if static_dir.exists():
15
- app.mount(prefix or "/assets", StaticFiles(directory=static_dir / "assets"), name="assets")
13
+ app.mount(
14
+ prefix or "/assets",
15
+ StaticFiles(directory=static_dir / "assets"),
16
+ name="assets",
17
+ )
16
18
 
17
19
 
18
20
  def register_frontend_routes(router: APIRouter) -> None:
@@ -10,7 +10,11 @@ import uvicorn
10
10
  def launch_app() -> None:
11
11
  host = os.getenv("API_TRACKER_HOST", "127.0.0.1")
12
12
  port = int(os.getenv("API_TRACKER_PORT", "8000"))
13
- open_browser = os.getenv("API_TRACKER_OPEN_BROWSER", "true").lower() not in {"0", "false", "no"}
13
+ open_browser = os.getenv("API_TRACKER_OPEN_BROWSER", "true").lower() not in {
14
+ "0",
15
+ "false",
16
+ "no",
17
+ }
14
18
  if open_browser:
15
19
  url = f"http://{host}:{port}"
16
20
  threading.Timer(1.0, lambda: webbrowser.open(url)).start()
@@ -1,5 +1,3 @@
1
- from pathlib import Path
2
-
3
1
  from fastapi import FastAPI
4
2
  from fastapi.middleware.cors import CORSMiddleware
5
3
  from fastapi.responses import FileResponse
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import datetime
3
+ from datetime import datetime, timezone
4
4
 
5
5
  from sqlalchemy import (
6
6
  JSON,
@@ -20,7 +20,8 @@ from .database import Base
20
20
 
21
21
 
22
22
  def utcnow() -> datetime:
23
- return datetime.utcnow()
23
+ # return datetime.utcnow()
24
+ return datetime.now(timezone.utc)
24
25
 
25
26
 
26
27
  class Project(Base):
@@ -42,21 +43,29 @@ class Provider(Base):
42
43
  name = Column(String(100), unique=True, nullable=False, index=True)
43
44
  created_at = Column(DateTime, default=utcnow, nullable=False)
44
45
 
45
- models = relationship("Model", back_populates="provider", cascade="all, delete-orphan")
46
+ models = relationship(
47
+ "Model", back_populates="provider", cascade="all, delete-orphan"
48
+ )
46
49
 
47
50
 
48
51
  class Model(Base):
49
52
  __tablename__ = "models"
50
- __table_args__ = (UniqueConstraint("provider_id", "model_name", name="uq_provider_model_name"),)
53
+ __table_args__ = (
54
+ UniqueConstraint("provider_id", "model_name", name="uq_provider_model_name"),
55
+ )
51
56
 
52
57
  id = Column(Integer, primary_key=True, index=True)
53
- provider_id = Column(Integer, ForeignKey("providers.id"), nullable=False, index=True)
58
+ provider_id = Column(
59
+ Integer, ForeignKey("providers.id"), nullable=False, index=True
60
+ )
54
61
  model_name = Column(String(150), nullable=False, index=True)
55
62
  model_type = Column(String(50), default="text", nullable=False)
56
63
  created_at = Column(DateTime, default=utcnow, nullable=False)
57
64
 
58
65
  provider = relationship("Provider", back_populates="models")
59
- pricing = relationship("ModelPricing", back_populates="model", cascade="all, delete-orphan")
66
+ pricing = relationship(
67
+ "ModelPricing", back_populates="model", cascade="all, delete-orphan"
68
+ )
60
69
  usage = relationship("ApiUsage", back_populates="model_ref")
61
70
 
62
71
 
@@ -86,7 +95,9 @@ class ApiUsage(Base):
86
95
 
87
96
  id = Column(Integer, primary_key=True, index=True)
88
97
  project_id = Column(Integer, ForeignKey("projects.id"), nullable=False, index=True)
89
- provider_id = Column(Integer, ForeignKey("providers.id"), nullable=False, index=True)
98
+ provider_id = Column(
99
+ Integer, ForeignKey("providers.id"), nullable=False, index=True
100
+ )
90
101
  model_id = Column(Integer, ForeignKey("models.id"), nullable=False, index=True)
91
102
  internal_request_id = Column(String(255), nullable=False, unique=True, index=True)
92
103
  provider_request_id = Column(String(255), nullable=True, index=True)
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import os
4
4
  from pathlib import Path
5
5
 
6
-
7
6
  APP_NAME = "API Tracker"
8
7
 
9
8
 
File without changes
@@ -0,0 +1,171 @@
1
+ from fastapi import APIRouter, Depends
2
+ from sqlalchemy import func
3
+ from sqlalchemy.orm import Session
4
+
5
+ from ..database import get_db
6
+ from ..models import ApiUsage, Model, Project, Provider
7
+
8
+ router = APIRouter(prefix="/analytics", tags=["Analytics"])
9
+
10
+
11
+ @router.get("/overview")
12
+ def overview(db: Session = Depends(get_db)):
13
+ rows = db.query(ApiUsage).all()
14
+ count = len(rows)
15
+ total_tokens = sum(r.total_tokens for r in rows)
16
+ total_cost = sum(r.total_cost for r in rows)
17
+ avg_latency = (sum(r.latency_ms for r in rows) / count) if count else 0
18
+ error_rate = (
19
+ ((sum(1 for r in rows if r.status != "success") / count) * 100) if count else 0
20
+ )
21
+ return {
22
+ "requests": count,
23
+ "tokens": total_tokens,
24
+ "cost": total_cost,
25
+ "average_latency_ms": avg_latency,
26
+ "error_rate": error_rate,
27
+ }
28
+
29
+
30
+ @router.get("/cost")
31
+ def cost(db: Session = Depends(get_db)):
32
+ rows = (
33
+ db.query(
34
+ func.date(ApiUsage.timestamp).label("day"), func.sum(ApiUsage.total_cost)
35
+ )
36
+ .group_by("day")
37
+ .all()
38
+ )
39
+ return [{"date": str(day), "cost": cost or 0} for day, cost in rows]
40
+
41
+
42
+ @router.get("/tokens")
43
+ def tokens(db: Session = Depends(get_db)):
44
+ rows = (
45
+ db.query(
46
+ func.date(ApiUsage.timestamp).label("day"),
47
+ func.sum(ApiUsage.input_tokens),
48
+ func.sum(ApiUsage.output_tokens),
49
+ func.sum(ApiUsage.thinking_tokens),
50
+ func.sum(ApiUsage.cached_tokens),
51
+ )
52
+ .group_by("day")
53
+ .all()
54
+ )
55
+ return [
56
+ {
57
+ "date": str(day),
58
+ "input": i or 0,
59
+ "output": o or 0,
60
+ "thinking": t or 0,
61
+ "cached": c or 0,
62
+ }
63
+ for day, i, o, t, c in rows
64
+ ]
65
+
66
+
67
+ @router.get("/latency")
68
+ def latency(db: Session = Depends(get_db)):
69
+ rows = [row.latency_ms for row in db.query(ApiUsage).all()]
70
+ if not rows:
71
+ return {"average": 0, "p50": 0, "p95": 0, "p99": 0}
72
+ rows.sort()
73
+ count = len(rows)
74
+ return {
75
+ "average": sum(rows) / count,
76
+ "p50": rows[int(count * 0.5) - 1 if count > 1 else 0],
77
+ "p95": rows[int(count * 0.95) - 1 if count > 1 else 0],
78
+ "p99": rows[int(count * 0.99) - 1 if count > 1 else 0],
79
+ }
80
+
81
+
82
+ @router.get("/errors")
83
+ def errors(db: Session = Depends(get_db)):
84
+ rows = (
85
+ db.query(ApiUsage.error_type, func.count(ApiUsage.id))
86
+ .filter(ApiUsage.status != "success")
87
+ .group_by(ApiUsage.error_type)
88
+ .all()
89
+ )
90
+ return [
91
+ {"error_type": error_type or "Unknown", "count": count}
92
+ for error_type, count in rows
93
+ ]
94
+
95
+
96
+ @router.get("/by-project")
97
+ def by_project(db: Session = Depends(get_db)):
98
+ rows = (
99
+ db.query(
100
+ Project.name,
101
+ func.count(ApiUsage.id),
102
+ func.sum(ApiUsage.total_tokens),
103
+ func.sum(ApiUsage.total_cost),
104
+ func.avg(ApiUsage.latency_ms),
105
+ )
106
+ .join(ApiUsage, ApiUsage.project_id == Project.id)
107
+ .group_by(Project.name)
108
+ .all()
109
+ )
110
+ return [
111
+ {
112
+ "project": name,
113
+ "requests": req or 0,
114
+ "tokens": tokens or 0,
115
+ "cost": cost or 0,
116
+ "latency_ms": latency or 0,
117
+ }
118
+ for name, req, tokens, cost, latency in rows
119
+ ]
120
+
121
+
122
+ @router.get("/by-provider")
123
+ def by_provider(db: Session = Depends(get_db)):
124
+ rows = (
125
+ db.query(
126
+ Provider.name,
127
+ func.count(ApiUsage.id),
128
+ func.sum(ApiUsage.total_tokens),
129
+ func.sum(ApiUsage.total_cost),
130
+ func.avg(ApiUsage.latency_ms),
131
+ )
132
+ .join(ApiUsage, ApiUsage.provider_id == Provider.id)
133
+ .group_by(Provider.name)
134
+ .all()
135
+ )
136
+ return [
137
+ {
138
+ "provider": name,
139
+ "requests": req or 0,
140
+ "tokens": tokens or 0,
141
+ "cost": cost or 0,
142
+ "latency_ms": latency or 0,
143
+ }
144
+ for name, req, tokens, cost, latency in rows
145
+ ]
146
+
147
+
148
+ @router.get("/by-model")
149
+ def by_model(db: Session = Depends(get_db)):
150
+ rows = (
151
+ db.query(
152
+ Model.model_name,
153
+ func.count(ApiUsage.id),
154
+ func.sum(ApiUsage.total_tokens),
155
+ func.sum(ApiUsage.total_cost),
156
+ func.avg(ApiUsage.latency_ms),
157
+ )
158
+ .join(ApiUsage, ApiUsage.model_id == Model.id)
159
+ .group_by(Model.model_name)
160
+ .all()
161
+ )
162
+ return [
163
+ {
164
+ "model": name,
165
+ "requests": req or 0,
166
+ "tokens": tokens or 0,
167
+ "cost": cost or 0,
168
+ "latency_ms": latency or 0,
169
+ }
170
+ for name, req, tokens, cost, latency in rows
171
+ ]
@@ -17,7 +17,10 @@ def list_models(db: Session = Depends(get_db)):
17
17
  def create_model(payload: ModelCreate, db: Session = Depends(get_db)):
18
18
  row = (
19
19
  db.query(Model)
20
- .filter(Model.provider_id == payload.provider_id, Model.model_name == payload.model_name)
20
+ .filter(
21
+ Model.provider_id == payload.provider_id,
22
+ Model.model_name == payload.model_name,
23
+ )
21
24
  .first()
22
25
  )
23
26
  if row:
@@ -23,7 +23,9 @@ def list_pricing(db: Session = Depends(get_db)):
23
23
 
24
24
 
25
25
  @router.put("/{pricing_id}", response_model=PricingResponse)
26
- def update_pricing(pricing_id: int, pricing: PricingUpdate, db: Session = Depends(get_db)):
26
+ def update_pricing(
27
+ pricing_id: int, pricing: PricingUpdate, db: Session = Depends(get_db)
28
+ ):
27
29
  row = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
28
30
  if not row:
29
31
  raise HTTPException(status_code=404, detail="Pricing not found")
@@ -34,7 +34,9 @@ def get_project(project_id: int, db: Session = Depends(get_db)):
34
34
 
35
35
 
36
36
  @router.put("/{project_id}", response_model=ProjectResponse)
37
- def update_project(project_id: int, project: ProjectUpdate, db: Session = Depends(get_db)):
37
+ def update_project(
38
+ project_id: int, project: ProjectUpdate, db: Session = Depends(get_db)
39
+ ):
38
40
  row = db.query(Project).filter(Project.id == project_id).first()
39
41
  if not row:
40
42
  raise HTTPException(status_code=404, detail="Project not found")
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException
2
2
  from sqlalchemy.orm import Session
3
3
 
4
4
  from ..database import get_db
5
- from ..models import ApiUsage, Model, Project, Provider
5
+ from ..models import ApiUsage
6
6
  from ..schemas import UsageDataIn, UsageResponse
7
7
  from ..services.usage_service import create_usage
8
8
 
@@ -1,14 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import Any, Optional
4
+ from typing import Any
5
5
 
6
6
  from pydantic import BaseModel, ConfigDict, Field
7
7
 
8
8
 
9
9
  class ProjectBase(BaseModel):
10
10
  name: str
11
- description: Optional[str] = None
11
+ description: str | None = None
12
12
  environment: str = "development"
13
13
 
14
14
 
@@ -17,9 +17,9 @@ class ProjectCreate(ProjectBase):
17
17
 
18
18
 
19
19
  class ProjectUpdate(BaseModel):
20
- name: Optional[str] = None
21
- description: Optional[str] = None
22
- environment: Optional[str] = None
20
+ name: str | None = None
21
+ description: str | None = None
22
+ environment: str | None = None
23
23
 
24
24
 
25
25
  class ProjectResponse(ProjectBase):
@@ -58,17 +58,17 @@ class PricingCreate(BaseModel):
58
58
  cached_input_price_per_1m: float = 0.0
59
59
  currency: str = "USD"
60
60
  effective_from: datetime
61
- effective_to: Optional[datetime] = None
61
+ effective_to: datetime | None = None
62
62
 
63
63
 
64
64
  class PricingUpdate(BaseModel):
65
- input_price_per_1m: Optional[float] = None
66
- output_price_per_1m: Optional[float] = None
67
- thinking_price_per_1m: Optional[float] = None
68
- cached_input_price_per_1m: Optional[float] = None
69
- currency: Optional[str] = None
70
- effective_from: Optional[datetime] = None
71
- effective_to: Optional[datetime] = None
65
+ input_price_per_1m: float | None = None
66
+ output_price_per_1m: float | None = None
67
+ thinking_price_per_1m: float | None = None
68
+ cached_input_price_per_1m: float | None = None
69
+ currency: str | None = None
70
+ effective_from: datetime | None = None
71
+ effective_to: datetime | None = None
72
72
 
73
73
 
74
74
  class PricingResponse(PricingCreate):
@@ -83,7 +83,7 @@ class UsageDataIn(BaseModel):
83
83
  provider: str
84
84
  model: str
85
85
  internal_request_id: str
86
- provider_request_id: Optional[str] = None
86
+ provider_request_id: str | None = None
87
87
  input_tokens: int = 0
88
88
  output_tokens: int = 0
89
89
  thinking_tokens: int = 0
@@ -94,8 +94,8 @@ class UsageDataIn(BaseModel):
94
94
  request_count: int = 1
95
95
  latency_ms: float = 0.0
96
96
  status: str = "success"
97
- http_status_code: Optional[int] = None
98
- error_type: Optional[str] = None
97
+ http_status_code: int | None = None
98
+ error_type: str | None = None
99
99
  metadata: dict[str, Any] = Field(default_factory=dict)
100
100
  capture_content: bool = False
101
101
 
@@ -106,7 +106,7 @@ class UsageResponse(BaseModel):
106
106
  provider_id: int
107
107
  model_id: int
108
108
  internal_request_id: str
109
- provider_request_id: Optional[str]
109
+ provider_request_id: str | None
110
110
  timestamp: datetime
111
111
  input_tokens: int
112
112
  output_tokens: int
@@ -123,8 +123,8 @@ class UsageResponse(BaseModel):
123
123
  request_count: int
124
124
  latency_ms: float
125
125
  status: str
126
- http_status_code: Optional[int]
127
- error_type: Optional[str]
126
+ http_status_code: int | None
127
+ error_type: str | None
128
128
  metadata_json: dict[str, Any]
129
129
  capture_content: bool
130
130
  model_config = ConfigDict(from_attributes=True)
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import datetime
3
+ from datetime import datetime, timezone
4
4
  from typing import Any
5
5
 
6
6
  from sqlalchemy.orm import Session
@@ -46,13 +46,18 @@ def _get_or_create_model(db: Session, provider_id: int, model_name: str) -> Mode
46
46
  return model
47
47
 
48
48
 
49
- def get_applicable_pricing(db: Session, model_id: int, timestamp: datetime) -> ModelPricing | None:
49
+ def get_applicable_pricing(
50
+ db: Session, model_id: int, timestamp: datetime
51
+ ) -> ModelPricing | None:
50
52
  pricing = (
51
53
  db.query(ModelPricing)
52
54
  .filter(
53
55
  ModelPricing.model_id == model_id,
54
56
  ModelPricing.effective_from <= timestamp,
55
- ((ModelPricing.effective_to.is_(None)) | (ModelPricing.effective_to >= timestamp)),
57
+ (
58
+ (ModelPricing.effective_to.is_(None))
59
+ | (ModelPricing.effective_to >= timestamp)
60
+ ),
56
61
  )
57
62
  .order_by(ModelPricing.effective_from.desc())
58
63
  .first()
@@ -71,7 +76,8 @@ def create_usage(db: Session, payload: dict[str, Any]) -> ApiUsage:
71
76
  project = _get_or_create_project(db, payload["project"])
72
77
  provider = _get_or_create_provider(db, payload["provider"])
73
78
  model = _get_or_create_model(db, provider.id, payload["model"])
74
- timestamp = payload.get("timestamp") or datetime.utcnow()
79
+ # timestamp = payload.get("timestamp") or datetime.utcnow()
80
+ timestamp = payload.get("timestamp") or datetime.now(timezone.utc)
75
81
  pricing = get_applicable_pricing(db, model.id, timestamp)
76
82
  snapshot = PricingSnapshot(
77
83
  input_price_per_1m=getattr(pricing, "input_price_per_1m", 0.0),
@@ -1,9 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llmapi-tracker
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Local-first API usage and cost tracking platform
5
5
  Author: API Tracker
6
6
  License: MIT
7
+ Project-URL: Homepage, https://github.com/Lns9777/API-Usage-Tracker
8
+ Project-URL: Repository, https://github.com/Lns9777/API-Usage-Tracker
9
+ Project-URL: Issues, https://github.com/Lns9777/API-Usage-Tracker/issues
7
10
  Requires-Python: >=3.10
8
11
  Description-Content-Type: text/markdown
9
12
  License-File: LICENSE
@@ -27,24 +30,24 @@ Requires-Dist: build>=1.2; extra == "dev"
27
30
  Requires-Dist: twine>=5.1; extra == "dev"
28
31
  Dynamic: license-file
29
32
 
30
- # API Tracker
33
+ # llmapi-tracker
31
34
 
32
35
  ## What API Tracker Is
33
36
 
34
- API Tracker is a local-first API usage and cost observability tool. It records request volume, token usage, latency, status, and versioned pricing for models from providers such as OpenAI and Gemini.
37
+ `llmapi-tracker` is a local-first API usage and cost observability platform for LLM applications. It records request counts, token usage, latency, errors, and versioned pricing for provider models such as OpenAI and Gemini.
35
38
 
36
- The project is built to keep data local by default. It stores usage and pricing in a local SQLite database and does not persist prompts, responses, or raw API keys in the application database.
39
+ The project is designed to keep data local by default. It stores usage and pricing in a local SQLite database and does not persist prompts, responses, or raw API keys in the application database.
37
40
 
38
41
  ## Features
39
42
 
40
43
  - Local FastAPI backend with SQLite storage
41
44
  - Versioned pricing records for historical cost tracking
42
45
  - Usage capture for input, output, thinking, cached, and total tokens
43
- - Cost calculation per request and across analytics views
46
+ - Request-level and analytics-level cost calculation
44
47
  - React + Vite dashboard for usage, analytics, providers, models, and pricing
45
48
  - SDK wrappers for OpenAI and Gemini
46
49
  - Packaged launcher command that starts the local app and opens the browser
47
- - Open-source repository setup with tests, CI, and release automation
50
+ - Open-source repository with tests, CI, and release automation
48
51
 
49
52
  ## Installation
50
53
 
@@ -65,10 +68,16 @@ npm install
65
68
 
66
69
  ### Package install
67
70
 
68
- If you install this project as a package, the launcher command is:
71
+ ```bash
72
+ pip install llmapi-tracker
73
+ ```
74
+
75
+ If you want optional provider SDK support:
69
76
 
70
77
  ```bash
71
- api-tracker
78
+ pip install "llmapi-tracker[openai]"
79
+ pip install "llmapi-tracker[gemini]"
80
+ pip install "llmapi-tracker[all]"
72
81
  ```
73
82
 
74
83
  ## Starting the Application
@@ -88,7 +97,7 @@ npm run dev
88
97
 
89
98
  ### Packaged app
90
99
 
91
- After building the frontend assets and packaging them into the backend static directory, run:
100
+ After the frontend assets are built and packaged into the backend static directory:
92
101
 
93
102
  ```bash
94
103
  api-tracker
@@ -102,7 +111,7 @@ The launcher starts the backend server and opens the browser to the local dashbo
102
111
 
103
112
  ```python
104
113
  import os
105
- from sdk.api_tracker import APITracker
114
+ from api_tracker import APITracker
106
115
 
107
116
  tracker = APITracker(
108
117
  project="Pharmacy-AI",
@@ -201,7 +210,7 @@ python scripts/verify_frontend_assets.py
201
210
 
202
211
  ## Contributing
203
212
 
204
- This repository is intended to be open source and contributor friendly.
213
+ `llmapi-tracker` is intended to be open source and contributor friendly.
205
214
 
206
215
  Contribution files:
207
216
 
@@ -221,5 +230,5 @@ Contribution expectations:
221
230
 
222
231
  ## License
223
232
 
224
- API Tracker is released under the MIT License. See [`LICENSE`](L:/Personal/APITracker/LICENSE).
233
+ `llmapi-tracker` is released under the MIT License. See [`LICENSE`](L:/Personal/APITracker/LICENSE).
225
234
 
@@ -0,0 +1,83 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+
6
+ [project]
7
+ name = "llmapi-tracker"
8
+ version = "1.0.2"
9
+ description = "Local-first API usage and cost tracking platform"
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ license = { text = "MIT" }
13
+ authors = [
14
+ { name = "API Tracker" }
15
+ ]
16
+
17
+ dependencies = [
18
+ "requests>=2.32",
19
+ "fastapi>=0.115",
20
+ "uvicorn[standard]>=0.30",
21
+ "sqlalchemy>=2.0",
22
+ "pydantic>=2.8",
23
+ ]
24
+
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/Lns9777/API-Usage-Tracker"
28
+ Repository = "https://github.com/Lns9777/API-Usage-Tracker"
29
+ Issues = "https://github.com/Lns9777/API-Usage-Tracker/issues"
30
+
31
+
32
+ [project.optional-dependencies]
33
+ openai = ["openai>=1.40"]
34
+ gemini = ["google-genai>=0.7"]
35
+ all = ["openai>=1.40", "google-genai>=0.7"]
36
+ dev = [
37
+ "pytest>=8.0",
38
+ "httpx>=0.27",
39
+ "ruff>=0.6",
40
+ "build>=1.2",
41
+ "twine>=5.1",
42
+ ]
43
+
44
+
45
+ [project.scripts]
46
+ api-tracker = "backend.app.launcher:launch_app"
47
+
48
+
49
+ [tool.setuptools]
50
+ package-dir = { "backend" = "backend", "api_tracker" = "sdk/api_tracker" }
51
+ packages = [
52
+ "backend",
53
+ "backend.app",
54
+ "backend.app.routes",
55
+ "backend.app.services",
56
+ "api_tracker",
57
+ "api_tracker.providers",
58
+ ]
59
+
60
+
61
+ [tool.setuptools.package-data]
62
+ api_tracker = ["py.typed"]
63
+ backend = ["app/static/**/*"]
64
+
65
+
66
+ [tool.ruff]
67
+ line-length = 88
68
+ target-version = "py310"
69
+
70
+
71
+ [tool.ruff.lint]
72
+ select = [
73
+ "E",
74
+ "F",
75
+ "I",
76
+ "B",
77
+ "UP",
78
+ "RUF",
79
+ "DTZ",
80
+ ]
81
+ ignore = [
82
+ "B008",
83
+ ]
@@ -1,5 +1,3 @@
1
- from datetime import datetime
2
-
3
1
  from fastapi.testclient import TestClient
4
2
 
5
3
  from backend.app.main import app
@@ -1,5 +1,3 @@
1
- from pathlib import Path
2
-
3
1
  from backend.app import paths
4
2
 
5
3
 
@@ -13,7 +11,10 @@ def test_database_path_uses_explicit_overrides(monkeypatch, tmp_path):
13
11
 
14
12
  assert paths.get_data_dir() == data_dir
15
13
  assert paths.get_database_path() == data_dir / "api_tracker.db"
16
- assert paths.get_database_url() == f"sqlite:///{(data_dir / 'api_tracker.db').as_posix()}"
14
+ assert (
15
+ paths.get_database_url()
16
+ == f"sqlite:///{(data_dir / 'api_tracker.db').as_posix()}"
17
+ )
17
18
 
18
19
  monkeypatch.setenv("API_TRACKER_DB_PATH", str(db_path))
19
20
  assert paths.get_database_path() == db_path
@@ -1,4 +1,4 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timezone
2
2
 
3
3
  from fastapi.testclient import TestClient
4
4
  from sqlalchemy import create_engine
@@ -8,7 +8,6 @@ from sqlalchemy.pool import StaticPool
8
8
  from backend.app.database import Base, get_db
9
9
  from backend.app.main import app
10
10
 
11
-
12
11
  engine = create_engine(
13
12
  "sqlite://",
14
13
  connect_args={"check_same_thread": False},
@@ -36,7 +35,9 @@ def teardown_module():
36
35
 
37
36
  def test_project_crud():
38
37
  client = TestClient(app)
39
- response = client.post("/projects", json={"name": "alpha", "description": "A", "environment": "dev"})
38
+ response = client.post(
39
+ "/projects", json={"name": "alpha", "description": "A", "environment": "dev"}
40
+ )
40
41
  assert response.status_code == 200
41
42
  project_id = response.json()["id"]
42
43
 
@@ -52,7 +53,14 @@ def test_project_crud():
52
53
  def test_pricing_and_usage_endpoints():
53
54
  client = TestClient(app)
54
55
  provider = client.post("/providers", json={"name": "openai"}).json()
55
- model = client.post("/models", json={"provider_id": provider["id"], "model_name": "gpt-5", "model_type": "text"}).json()
56
+ model = client.post(
57
+ "/models",
58
+ json={
59
+ "provider_id": provider["id"],
60
+ "model_name": "gpt-5",
61
+ "model_type": "text",
62
+ },
63
+ ).json()
56
64
  client.post(
57
65
  "/pricing",
58
66
  json={
@@ -62,7 +70,8 @@ def test_pricing_and_usage_endpoints():
62
70
  "thinking_price_per_1m": 3.0,
63
71
  "cached_input_price_per_1m": 4.0,
64
72
  "currency": "USD",
65
- "effective_from": datetime.utcnow().isoformat(),
73
+ # "effective_from": datetime.utcnow().isoformat(),
74
+ "effective_from": datetime.now(timezone.utc).isoformat(),
66
75
  "effective_to": None,
67
76
  },
68
77
  )
@@ -77,7 +86,8 @@ def test_pricing_and_usage_endpoints():
77
86
  "output_tokens": 1000000,
78
87
  "thinking_tokens": 1000000,
79
88
  "cached_tokens": 1000000,
80
- "timestamp": datetime.utcnow().isoformat(),
89
+ # "timestamp": datetime.now(timezone.utc).isoformat(),
90
+ "timestamp": datetime.now(timezone.utc).isoformat(),
81
91
  },
82
92
  )
83
93
  assert usage.status_code == 200
@@ -15,7 +15,9 @@ def test_tracker_records_usage(monkeypatch):
15
15
  dummy = DummyTransport()
16
16
  tracker.transport = dummy
17
17
 
18
- usage = tracker.record(model="gpt-5", input_tokens=10, output_tokens=5, total_tokens=15)
18
+ usage = tracker.record(
19
+ model="gpt-5", input_tokens=10, output_tokens=5, total_tokens=15
20
+ )
19
21
 
20
22
  assert usage.total_tokens == 15
21
23
  assert dummy.sent[0]["model"] == "gpt-5"
@@ -1,15 +1,17 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timezone
2
2
 
3
3
  from sqlalchemy import create_engine
4
4
  from sqlalchemy.orm import sessionmaker
5
5
 
6
6
  from backend.app.database import Base
7
- from backend.app.models import ModelPricing, Project, Provider, Model
7
+ from backend.app.models import Model, ModelPricing, Project, Provider
8
8
  from backend.app.services.usage_service import create_usage, get_applicable_pricing
9
9
 
10
10
 
11
11
  def make_session():
12
- engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
12
+ engine = create_engine(
13
+ "sqlite:///:memory:", connect_args={"check_same_thread": False}
14
+ )
13
15
  Base.metadata.create_all(bind=engine)
14
16
  return sessionmaker(bind=engine)()
15
17
 
@@ -29,8 +31,10 @@ def seed_pricing(db):
29
31
  thinking_price_per_1m=3.0,
30
32
  cached_input_price_per_1m=4.0,
31
33
  currency="USD",
32
- effective_from=datetime(2026, 1, 1),
33
- effective_to=datetime(2026, 6, 30),
34
+ # effective_from=datetime(2026, 1, 1),
35
+ effective_from=datetime(2026, 1, 1, tzinfo=timezone.utc),
36
+ # effective_to=datetime(2026, 6, 30),
37
+ effective_to=datetime(2026, 6, 30, tzinfo=timezone.utc),
34
38
  )
35
39
  )
36
40
  db.commit()
@@ -40,7 +44,12 @@ def seed_pricing(db):
40
44
  def test_get_applicable_pricing_returns_version_for_timestamp():
41
45
  db = make_session()
42
46
  model = seed_pricing(db)
43
- pricing = get_applicable_pricing(db, model.id, datetime(2026, 3, 1))
47
+ # pricing = get_applicable_pricing(db, model.id, datetime(2026, 3, 1))
48
+ pricing = get_applicable_pricing(
49
+ db,
50
+ model.id,
51
+ datetime(2026, 3, 1, tzinfo=timezone.utc),
52
+ )
44
53
  assert pricing is not None
45
54
  assert pricing.input_price_per_1m == 1.0
46
55
 
@@ -59,7 +68,8 @@ def test_create_usage_applies_pricing_snapshot():
59
68
  "output_tokens": 1_000_000,
60
69
  "thinking_tokens": 1_000_000,
61
70
  "cached_tokens": 1_000_000,
62
- "timestamp": datetime(2026, 3, 1),
71
+ # "timestamp": datetime(2026, 3, 1),
72
+ "timestamp": datetime(2026, 3, 1, tzinfo=timezone.utc),
63
73
  },
64
74
  )
65
75
  assert usage.total_cost == 10.0
@@ -1 +0,0 @@
1
- from . import analytics, models, pricing, projects, providers, usage
@@ -1,91 +0,0 @@
1
- from fastapi import APIRouter, Depends
2
- from sqlalchemy import func
3
- from sqlalchemy.orm import Session
4
-
5
- from ..database import get_db
6
- from ..models import ApiUsage, Model, Project, Provider
7
-
8
- router = APIRouter(prefix="/analytics", tags=["Analytics"])
9
-
10
-
11
- @router.get("/overview")
12
- def overview(db: Session = Depends(get_db)):
13
- rows = db.query(ApiUsage).all()
14
- count = len(rows)
15
- total_tokens = sum(r.total_tokens for r in rows)
16
- total_cost = sum(r.total_cost for r in rows)
17
- avg_latency = (sum(r.latency_ms for r in rows) / count) if count else 0
18
- error_rate = ((sum(1 for r in rows if r.status != "success") / count) * 100) if count else 0
19
- return {
20
- "requests": count,
21
- "tokens": total_tokens,
22
- "cost": total_cost,
23
- "average_latency_ms": avg_latency,
24
- "error_rate": error_rate,
25
- }
26
-
27
-
28
- @router.get("/cost")
29
- def cost(db: Session = Depends(get_db)):
30
- rows = db.query(func.date(ApiUsage.timestamp).label("day"), func.sum(ApiUsage.total_cost)).group_by("day").all()
31
- return [{"date": str(day), "cost": cost or 0} for day, cost in rows]
32
-
33
-
34
- @router.get("/tokens")
35
- def tokens(db: Session = Depends(get_db)):
36
- rows = db.query(func.date(ApiUsage.timestamp).label("day"), func.sum(ApiUsage.input_tokens), func.sum(ApiUsage.output_tokens), func.sum(ApiUsage.thinking_tokens), func.sum(ApiUsage.cached_tokens)).group_by("day").all()
37
- return [{"date": str(day), "input": i or 0, "output": o or 0, "thinking": t or 0, "cached": c or 0} for day, i, o, t, c in rows]
38
-
39
-
40
- @router.get("/latency")
41
- def latency(db: Session = Depends(get_db)):
42
- rows = [row.latency_ms for row in db.query(ApiUsage).all()]
43
- if not rows:
44
- return {"average": 0, "p50": 0, "p95": 0, "p99": 0}
45
- rows.sort()
46
- count = len(rows)
47
- return {
48
- "average": sum(rows) / count,
49
- "p50": rows[int(count * 0.5) - 1 if count > 1 else 0],
50
- "p95": rows[int(count * 0.95) - 1 if count > 1 else 0],
51
- "p99": rows[int(count * 0.99) - 1 if count > 1 else 0],
52
- }
53
-
54
-
55
- @router.get("/errors")
56
- def errors(db: Session = Depends(get_db)):
57
- rows = db.query(ApiUsage.error_type, func.count(ApiUsage.id)).filter(ApiUsage.status != "success").group_by(ApiUsage.error_type).all()
58
- return [{"error_type": error_type or "Unknown", "count": count} for error_type, count in rows]
59
-
60
-
61
- @router.get("/by-project")
62
- def by_project(db: Session = Depends(get_db)):
63
- rows = (
64
- db.query(Project.name, func.count(ApiUsage.id), func.sum(ApiUsage.total_tokens), func.sum(ApiUsage.total_cost), func.avg(ApiUsage.latency_ms))
65
- .join(ApiUsage, ApiUsage.project_id == Project.id)
66
- .group_by(Project.name)
67
- .all()
68
- )
69
- return [{"project": name, "requests": req or 0, "tokens": tokens or 0, "cost": cost or 0, "latency_ms": latency or 0} for name, req, tokens, cost, latency in rows]
70
-
71
-
72
- @router.get("/by-provider")
73
- def by_provider(db: Session = Depends(get_db)):
74
- rows = (
75
- db.query(Provider.name, func.count(ApiUsage.id), func.sum(ApiUsage.total_tokens), func.sum(ApiUsage.total_cost), func.avg(ApiUsage.latency_ms))
76
- .join(ApiUsage, ApiUsage.provider_id == Provider.id)
77
- .group_by(Provider.name)
78
- .all()
79
- )
80
- return [{"provider": name, "requests": req or 0, "tokens": tokens or 0, "cost": cost or 0, "latency_ms": latency or 0} for name, req, tokens, cost, latency in rows]
81
-
82
-
83
- @router.get("/by-model")
84
- def by_model(db: Session = Depends(get_db)):
85
- rows = (
86
- db.query(Model.model_name, func.count(ApiUsage.id), func.sum(ApiUsage.total_tokens), func.sum(ApiUsage.total_cost), func.avg(ApiUsage.latency_ms))
87
- .join(ApiUsage, ApiUsage.model_id == Model.id)
88
- .group_by(Model.model_name)
89
- .all()
90
- )
91
- return [{"model": name, "requests": req or 0, "tokens": tokens or 0, "cost": cost or 0, "latency_ms": latency or 0} for name, req, tokens, cost, latency in rows]
@@ -1,45 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=68", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "llmapi-tracker"
7
- version = "1.0.0"
8
- description = "Local-first API usage and cost tracking platform"
9
- readme = "README.md"
10
- requires-python = ">=3.10"
11
- license = { text = "MIT" }
12
- authors = [
13
- { name = "API Tracker" }
14
- ]
15
- dependencies = [
16
- "requests>=2.32",
17
- "fastapi>=0.115",
18
- "uvicorn[standard]>=0.30",
19
- "sqlalchemy>=2.0",
20
- "pydantic>=2.8",
21
- ]
22
-
23
- [project.optional-dependencies]
24
- openai = ["openai>=1.40"]
25
- gemini = ["google-genai>=0.7"]
26
- all = ["openai>=1.40", "google-genai>=0.7"]
27
- dev = ["pytest>=8.0", "httpx>=0.27", "ruff>=0.6", "build>=1.2", "twine>=5.1"]
28
-
29
- [project.scripts]
30
- api-tracker = "backend.app.launcher:launch_app"
31
-
32
- [tool.setuptools]
33
- package-dir = {"backend" = "backend", "api_tracker" = "sdk/api_tracker"}
34
- packages = [
35
- "backend",
36
- "backend.app",
37
- "backend.app.routes",
38
- "backend.app.services",
39
- "api_tracker",
40
- "api_tracker.providers",
41
- ]
42
-
43
- [tool.setuptools.package-data]
44
- api_tracker = ["py.typed"]
45
- backend = ["app/static/**/*"]
File without changes
File without changes