-
Notifications
You must be signed in to change notification settings - Fork 46k
feat(platform/admin): cost tracking for system credentials #12696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 81 commits
Commits
Show all changes
102 commits
Select commit
Hold shift + click to select a range
1a6c50c
feat(platform): add platform cost tracking for system credentials
majdyz 308357d
fix(platform): address PR review round 1
majdyz 26b0c95
fix(platform): address PR review round 2
majdyz 072b623
fix(platform): address PR review round 3
majdyz 476d9dc
fix(platform): address PR review round 4
majdyz f3b5d58
fix(platform): address PR review round 5
majdyz c054799
fix: regenerate API schema and block docs
majdyz 9924ded
fix(platform): address bot review comments (sentry + coderabbit)
majdyz b933698
fix(platform): re-add credit_cost to platform cost log metadata
majdyz 7020ae2
fix(backend): handle NULL userId in platform cost models and queries
majdyz 2c517ff
feat(platform): add per-provider cost extraction
majdyz 95524e9
feat(platform): add tracking_type and tracking_amount to cost log met…
majdyz 83b00f4
feat(platform): add copilot/autopilot cost tracking via token_trackin…
majdyz c2a054c
fix(backend): prevent provider_cost loss on stats merge and widen cos…
majdyz 7f4398e
feat(platform): provider-specific tracking types for accurate cost me…
majdyz a338c72
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into co…
majdyz fbe634f
fix(platform): handle null user_id in cost logs and fix 0.0 cost stor…
majdyz 173614b
fix(platform): audit and fix per-provider tracking accuracy
majdyz a8a62ee
feat(platform): add merge_stats tracking to all system credential blocks
majdyz 993c43b
feat(platform): add merge_stats to remaining blocks (FAL, Revid, D-ID…
majdyz 4bad083
feat(platform): extract OpenRouter cost from baseline copilot path
majdyz 6fa66ac
feat(platform): add cost/token OTEL attributes to both copilot paths
majdyz 9a08011
fix(platform): move opentelemetry import to top-level in both copilot…
majdyz 45d3193
fix(platform): move baseline cost extraction to finally + accumulate …
majdyz ab16e63
fix(platform): pass model name to copilot cost tracking
majdyz 405403e
fix(backend): initialize response before try block to satisfy pyright
majdyz 61ef54a
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into co…
majdyz 567bc73
fix(blocks): regenerate block docs after merge with dev
majdyz 1d7d2f7
feat(platform): tracking-aware dashboard with generated API client
majdyz 54e5059
fix(platform): use generated Pagination type + estimate token costs
majdyz da544d3
fix(platform): fix CI - regenerate API schema + fix Date type mismatch
majdyz 45b6ada
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into co…
majdyz 759effa
test(frontend): add unit tests for onboarding store and GenericTool h…
majdyz 960f893
test(platform): add unit tests for platform cost helpers and data layer
majdyz 71315aa
fix(backend): use actual provider in persist_and_record_usage cost lo…
majdyz 4190f75
test: additional coverage for platform cost and token tracking
majdyz 1ee0bd6
fix(platform): use round() for microdollar conversion and add cost tr…
majdyz b5acfb7
fix: resolve merge conflict with dev in helpers.test.ts
majdyz b00e16b
fix(platform): fix model_test to use Optional fields for None-skip test
majdyz 8d22653
fix(platform): address 4 review blockers on cost tracking
majdyz 7afd2b2
fix(platform): address 9 should-fix items from PR review
majdyz 372900c
fix(platform): address 5 self-review items on cost tracking
majdyz 7b36bed
fix(platform): address autogpt-reviewer feedback on cost tracking
majdyz 5e59523
test(platform): align actions tests with string date passthrough
majdyz 6f0c1df
fix(platform): close tracking gaps found during audit
majdyz 78b95f8
fix(platform): add provider_cost tracking to Exa code_context block
majdyz 44714f1
refactor(platform): use provider_cost_type Literal instead of output_…
majdyz f04cd08
feat(platform): add trackingAmount column + per-type rate estimation
majdyz 2f63fcd
test(frontend): update platform-costs helpers tests for per-type rate…
majdyz dca89d1
refactor(platform): address autogpt-reviewer feedback (batch 1)
majdyz 689cd67
refactor(platform): address autogpt-reviewer feedback (batch 2)
majdyz d57da6c
refactor(platform): extract usePlatformCostContent hook
majdyz 6de79fb
fix: resolve merge conflicts with dev branch
majdyz fb63ae5
refactor(platform): address review comments on platform cost tracking
majdyz 0365a26
refactor(backend): add clarifying comment to NodeExecutionStats.__iad…
majdyz 1812085
fix(backend): update token_tracking_test mock targets after _schedule…
majdyz 3895d95
fix(platform): address reviewer comments — tests, a11y, and frontend …
majdyz b64d1ed
Merge branch 'dev' into codex/platform-cost-tracking
ntindle f87bbd5
fix(backend): route platform cost logging through DatabaseManagerAsyn…
majdyz 16d696e
fix(platform): address autogpt-reviewer blockers and should-fix items
majdyz 27e34e9
fix(platform): drain pending cost logs on shutdown, remove dark: badges
majdyz 1d45f2f
fix(platform): fix baseline copilot OpenRouter cost extraction and cr…
majdyz a31be77
fix(platform): address additional reviewer feedback on platform cost …
majdyz 1c15d6a
fix(frontend): update platform-costs tests for Skeleton loading state…
majdyz 3f7a8dc
fix(backend/copilot): use active_model instead of config.model for co…
majdyz 50a8df3
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into co…
majdyz c946183
fix(backend): address all open review comments on platform cost tracking
majdyz a616e5a
fix(backend): address PR review — email masking, semaphore, openroute…
majdyz 254e605
fix(backend/copilot): connect Prisma in copilot executor for cost log…
majdyz 2a73d1b
fix(backend/copilot): route copilot cost logging through DatabaseMana…
majdyz 752a238
refactor(frontend): replace server actions with React Query hooks for…
majdyz 9b11754
fix(backend/copilot): update stale comment in processor.py after cost…
majdyz db6b444
fix(platform): address autogpt-reviewer should-fix items
majdyz e1ea4cf
test(frontend): rewrite PlatformCostContent tests to mock Orval hooks
majdyz bbdf13c
test(backend): add missing cost-tracking tests for Exa research, Apol…
majdyz 7c685c6
fix(backend): update platform_cost_test to expect masked email in das…
majdyz 630d6d4
fix(backend): add semaphore to executor cost log tasks; fix type anno…
majdyz b89321a
Merge remote-tracking branch 'origin/dev' into codex/platform-cost-tr…
majdyz 1c3fe14
fix(backend): address 4 unresolved review threads on cost tracking
majdyz 22fb354
test(frontend): fix null-user dash assertion using getAllByText to ha…
majdyz e7bd05c
fix(backend): filter drain tasks by current event loop to prevent cro…
majdyz cf605ef
fix(backend): fix race condition on _pending_log_tasks and uncapped t…
majdyz 5164fa8
fix(backend): fix race condition on _copilot_tasks concurrent iterati…
majdyz e7ca81e
fix(backend): address coderabbitai nitpicks in cost tracking files
majdyz 91af007
fix(backend): guard against non-finite cost_usd in persist_and_record…
majdyz 0e310c7
fix(backend): fix TestGetPlatformCostDashboard mocks to match 3-query…
majdyz b80d7ab
fix(backend): remove per-worker Prisma connect, route DB via Database…
majdyz 3e01650
fix(backend): replace getattr/hasattr duck-typing with try/except in …
majdyz fa06502
refactor(backend): extract shared _update_title_async to copilot/serv…
majdyz 5cc72e7
fix(backend): validate non-finite float in extract_openrouter_cost, r…
majdyz 91a3874
Merge remote-tracking branch 'origin/dev' into codex/platform-cost-tr…
majdyz 0c196cc
fix(backend): fix rate_limit_test mocks and reject negative extract_o…
majdyz 1f70eb1
test(backend): add missing edge-case tests for cost tracking
majdyz 4185040
fix(backend): fix TestSetUserTier cache_invalidated test to use user_…
majdyz e99a46f
refactor(backend): replace manual TTLCache with @cached decorator in …
majdyz db60929
fix(platform): address active review threads — UI labels, cost valida…
majdyz a9fa464
fix(backend/executor): clamp negative provider_cost in resolve_tracki…
majdyz b9789d1
fix(platform): add response=None baseline test and fix PlatformCostCo…
majdyz c2d3910
fix(backend): clear @cached result between TestGetPlatformCostDashboa…
majdyz de19865
Merge branch 'dev' into codex/platform-cost-tracking
majdyz 7e16c3b
fix(platform): address Pwuts review — accessor pattern, Prisma insert…
majdyz fde3f6a
Merge remote-tracking branch 'origin/codex/platform-cost-tracking' in…
majdyz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
98 changes: 98 additions & 0 deletions
98
autogpt_platform/backend/backend/api/features/admin/platform_cost_routes.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import logging | ||
| from datetime import datetime | ||
|
|
||
| from autogpt_libs.auth import get_user_id, requires_admin_user | ||
| from cachetools import TTLCache | ||
| from fastapi import APIRouter, Query, Security | ||
| from pydantic import BaseModel | ||
|
|
||
| from backend.data.platform_cost import ( | ||
| CostLogRow, | ||
| PlatformCostDashboard, | ||
| get_platform_cost_dashboard, | ||
| get_platform_cost_logs, | ||
| ) | ||
| from backend.util.models import Pagination | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # Cache dashboard results for 30 seconds per unique filter combination. | ||
| # The table is append-only so stale reads are acceptable for analytics. | ||
| _DASHBOARD_CACHE_TTL = 30 | ||
| _dashboard_cache: TTLCache[tuple, PlatformCostDashboard] = TTLCache( | ||
| maxsize=256, ttl=_DASHBOARD_CACHE_TTL | ||
|
majdyz marked this conversation as resolved.
Outdated
majdyz marked this conversation as resolved.
Outdated
majdyz marked this conversation as resolved.
Outdated
majdyz marked this conversation as resolved.
Outdated
majdyz marked this conversation as resolved.
Outdated
majdyz marked this conversation as resolved.
Outdated
majdyz marked this conversation as resolved.
Outdated
|
||
| ) | ||
|
|
||
|
|
||
| router = APIRouter( | ||
| prefix="/platform-costs", | ||
| tags=["platform-cost", "admin"], | ||
| dependencies=[Security(requires_admin_user)], | ||
| ) | ||
|
majdyz marked this conversation as resolved.
majdyz marked this conversation as resolved.
|
||
|
|
||
|
|
||
| class PlatformCostLogsResponse(BaseModel): | ||
| logs: list[CostLogRow] | ||
| pagination: Pagination | ||
|
|
||
|
|
||
| @router.get( | ||
| "/dashboard", | ||
| response_model=PlatformCostDashboard, | ||
| summary="Get Platform Cost Dashboard", | ||
| ) | ||
| async def get_cost_dashboard( | ||
| admin_user_id: str = Security(get_user_id), | ||
| start: datetime | None = Query(None), | ||
| end: datetime | None = Query(None), | ||
| provider: str | None = Query(None), | ||
| user_id: str | None = Query(None), | ||
| ): | ||
| logger.info("Admin %s fetching platform cost dashboard", admin_user_id) | ||
| cache_key = (start, end, provider, user_id) | ||
| cached = _dashboard_cache.get(cache_key) | ||
| if cached is not None: | ||
| return cached | ||
| result = await get_platform_cost_dashboard( | ||
| start=start, | ||
| end=end, | ||
| provider=provider, | ||
| user_id=user_id, | ||
| ) | ||
| _dashboard_cache[cache_key] = result | ||
| return result | ||
|
|
||
|
|
||
| @router.get( | ||
| "/logs", | ||
| response_model=PlatformCostLogsResponse, | ||
|
majdyz marked this conversation as resolved.
|
||
| summary="Get Platform Cost Logs", | ||
| ) | ||
| async def get_cost_logs( | ||
| admin_user_id: str = Security(get_user_id), | ||
| start: datetime | None = Query(None), | ||
| end: datetime | None = Query(None), | ||
| provider: str | None = Query(None), | ||
| user_id: str | None = Query(None), | ||
| page: int = Query(1, ge=1), | ||
| page_size: int = Query(50, ge=1, le=200), | ||
| ): | ||
| logger.info("Admin %s fetching platform cost logs", admin_user_id) | ||
| logs, total = await get_platform_cost_logs( | ||
| start=start, | ||
|
majdyz marked this conversation as resolved.
|
||
| end=end, | ||
| provider=provider, | ||
| user_id=user_id, | ||
| page=page, | ||
| page_size=page_size, | ||
| ) | ||
| total_pages = (total + page_size - 1) // page_size | ||
| return PlatformCostLogsResponse( | ||
| logs=logs, | ||
| pagination=Pagination( | ||
| total_items=total, | ||
| total_pages=total_pages, | ||
| current_page=page, | ||
| page_size=page_size, | ||
| ), | ||
| ) | ||
192 changes: 192 additions & 0 deletions
192
autogpt_platform/backend/backend/api/features/admin/platform_cost_routes_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| from unittest.mock import AsyncMock | ||
|
|
||
| import fastapi | ||
| import fastapi.testclient | ||
| import pytest | ||
| import pytest_mock | ||
| from autogpt_libs.auth.jwt_utils import get_jwt_payload | ||
|
|
||
| from backend.data.platform_cost import PlatformCostDashboard | ||
|
|
||
| from . import platform_cost_routes | ||
| from .platform_cost_routes import router as platform_cost_router | ||
|
|
||
| app = fastapi.FastAPI() | ||
| app.include_router(platform_cost_router) | ||
|
|
||
| client = fastapi.testclient.TestClient(app) | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def setup_app_admin_auth(mock_jwt_admin): | ||
| """Setup admin auth overrides for all tests in this module""" | ||
| app.dependency_overrides[get_jwt_payload] = mock_jwt_admin["get_jwt_payload"] | ||
| # Clear TTL cache so each test starts cold. | ||
| platform_cost_routes._dashboard_cache.clear() | ||
| yield | ||
| app.dependency_overrides.clear() | ||
|
|
||
|
|
||
| def test_get_dashboard_success( | ||
| mocker: pytest_mock.MockerFixture, | ||
| ) -> None: | ||
| real_dashboard = PlatformCostDashboard( | ||
| by_provider=[], | ||
| by_user=[], | ||
| total_cost_microdollars=0, | ||
| total_requests=0, | ||
| total_users=0, | ||
| ) | ||
| mocker.patch( | ||
| "backend.api.features.admin.platform_cost_routes.get_platform_cost_dashboard", | ||
| AsyncMock(return_value=real_dashboard), | ||
| ) | ||
|
|
||
| response = client.get("/platform-costs/dashboard") | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
| assert "by_provider" in data | ||
| assert "by_user" in data | ||
| assert data["total_cost_microdollars"] == 0 | ||
|
|
||
|
|
||
| def test_get_logs_success( | ||
| mocker: pytest_mock.MockerFixture, | ||
| ) -> None: | ||
| mocker.patch( | ||
| "backend.api.features.admin.platform_cost_routes.get_platform_cost_logs", | ||
| AsyncMock(return_value=([], 0)), | ||
| ) | ||
|
|
||
| response = client.get("/platform-costs/logs") | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
| assert data["logs"] == [] | ||
| assert data["pagination"]["total_items"] == 0 | ||
|
|
||
|
|
||
| def test_get_dashboard_with_filters( | ||
| mocker: pytest_mock.MockerFixture, | ||
| ) -> None: | ||
| real_dashboard = PlatformCostDashboard( | ||
| by_provider=[], | ||
| by_user=[], | ||
| total_cost_microdollars=0, | ||
| total_requests=0, | ||
| total_users=0, | ||
| ) | ||
| mock_dashboard = AsyncMock(return_value=real_dashboard) | ||
| mocker.patch( | ||
| "backend.api.features.admin.platform_cost_routes.get_platform_cost_dashboard", | ||
| mock_dashboard, | ||
| ) | ||
|
|
||
| response = client.get( | ||
| "/platform-costs/dashboard", | ||
| params={ | ||
| "start": "2026-01-01T00:00:00", | ||
| "end": "2026-04-01T00:00:00", | ||
| "provider": "openai", | ||
| "user_id": "test-user-123", | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| mock_dashboard.assert_called_once() | ||
| call_kwargs = mock_dashboard.call_args.kwargs | ||
| assert call_kwargs["provider"] == "openai" | ||
| assert call_kwargs["user_id"] == "test-user-123" | ||
| assert call_kwargs["start"] is not None | ||
| assert call_kwargs["end"] is not None | ||
|
|
||
|
|
||
| def test_get_logs_with_pagination( | ||
| mocker: pytest_mock.MockerFixture, | ||
| ) -> None: | ||
| mocker.patch( | ||
| "backend.api.features.admin.platform_cost_routes.get_platform_cost_logs", | ||
| AsyncMock(return_value=([], 0)), | ||
| ) | ||
|
|
||
| response = client.get( | ||
| "/platform-costs/logs", | ||
| params={"page": 2, "page_size": 25, "provider": "anthropic"}, | ||
| ) | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
| assert data["pagination"]["current_page"] == 2 | ||
| assert data["pagination"]["page_size"] == 25 | ||
|
|
||
|
|
||
| def test_get_dashboard_requires_admin() -> None: | ||
| import fastapi | ||
| from fastapi import HTTPException | ||
|
|
||
| def reject_jwt(request: fastapi.Request): | ||
| raise HTTPException(status_code=401, detail="Not authenticated") | ||
|
|
||
| app.dependency_overrides[get_jwt_payload] = reject_jwt | ||
| try: | ||
| response = client.get("/platform-costs/dashboard") | ||
| assert response.status_code == 401 | ||
| response = client.get("/platform-costs/logs") | ||
| assert response.status_code == 401 | ||
| finally: | ||
| app.dependency_overrides.clear() | ||
|
|
||
|
|
||
| def test_get_dashboard_rejects_non_admin(mock_jwt_user, mock_jwt_admin) -> None: | ||
| """Non-admin JWT must be rejected with 403 by requires_admin_user.""" | ||
| app.dependency_overrides[get_jwt_payload] = mock_jwt_user["get_jwt_payload"] | ||
| try: | ||
| response = client.get("/platform-costs/dashboard") | ||
| assert response.status_code == 403 | ||
| response = client.get("/platform-costs/logs") | ||
| assert response.status_code == 403 | ||
| finally: | ||
| app.dependency_overrides[get_jwt_payload] = mock_jwt_admin["get_jwt_payload"] | ||
|
|
||
|
|
||
| def test_get_logs_invalid_page_size_too_large() -> None: | ||
| """page_size > 200 must be rejected with 422.""" | ||
| response = client.get("/platform-costs/logs", params={"page_size": 201}) | ||
| assert response.status_code == 422 | ||
|
|
||
|
|
||
| def test_get_logs_invalid_page_size_zero() -> None: | ||
| """page_size = 0 (below ge=1) must be rejected with 422.""" | ||
| response = client.get("/platform-costs/logs", params={"page_size": 0}) | ||
| assert response.status_code == 422 | ||
|
|
||
|
|
||
| def test_get_logs_invalid_page_negative() -> None: | ||
| """page < 1 must be rejected with 422.""" | ||
| response = client.get("/platform-costs/logs", params={"page": 0}) | ||
| assert response.status_code == 422 | ||
|
|
||
|
|
||
| def test_get_dashboard_invalid_date_format() -> None: | ||
| """Malformed start date must be rejected with 422.""" | ||
| response = client.get("/platform-costs/dashboard", params={"start": "not-a-date"}) | ||
| assert response.status_code == 422 | ||
|
|
||
|
|
||
| def test_get_dashboard_cache_hit( | ||
| mocker: pytest_mock.MockerFixture, | ||
| ) -> None: | ||
| """Second identical request returns cached result without calling the DB again.""" | ||
| real_dashboard = PlatformCostDashboard( | ||
| by_provider=[], | ||
| by_user=[], | ||
| total_cost_microdollars=42, | ||
| total_requests=1, | ||
| total_users=1, | ||
| ) | ||
| mock_fn = mocker.patch( | ||
| "backend.api.features.admin.platform_cost_routes.get_platform_cost_dashboard", | ||
| AsyncMock(return_value=real_dashboard), | ||
| ) | ||
|
|
||
| client.get("/platform-costs/dashboard") | ||
| client.get("/platform-costs/dashboard") | ||
|
|
||
| mock_fn.assert_awaited_once() # second request hit the cache |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.