feat: add organization sessions mode for Devin self-serve usage tracking

Add `DEVIN_MODE` configuration supporting `enterprise` (default) and `organization_sessions` modes. Organization sessions mode uses `/v3/organizations/{org_id}/sessions` API with `ViewOrgSessions` permission, suitable for non-Enterprise organizations. Sums session ACUs consumed within configurable rolling window (1-365 days via `DEVIN_SESSION_WINDOW_DAYS`). Mode does not show quota, percentage, credit, or reset fields as these are unav
This commit is contained in:
nessi committed 2026-09-18 12:04:13 +02:00
1 parent 58eab24258
commit 792f85fc8b
8 files changed
+201 -8

No files matched your search

+92
View File
@@ -123,3 +123,95 @@ async def test_optional_org_forbidden_keeps_consumption():
assert result.limits[0].used == 12
assert result.limits[0].limit is None
await client.aclose()
@pytest.mark.asyncio
async def test_organization_sessions_sums_pages_and_user_filter(monkeypatch):
now = 1_800_000_000
days = 30
monkeypatch.setattr('app.providers.devin.time.time', lambda: now)
requests = []
async def handler(request):
requests.append(request)
assert request.url.raw_path.split(b'?', 1)[0] == b'/v3/organizations/org%2Fid/sessions'
assert request.url.params.get('first') == '200'
assert int(request.url.params['created_before']) == now
assert int(request.url.params['created_after']) == now - days * 86400
assert request.url.params.get_list('user_ids') == ['user-123']
if request.url.params.get('after') == 'next':
return httpx.Response(200, json={'items': [
{'session_id': 'session-2', 'created_at': now - 20, 'acus_consumed': 7.6},
]})
return httpx.Response(200, json={'items': [
{'session_id': 'session-1', 'created_at': now - 10, 'acus_consumed': 12.4},
], 'has_next_page': True, 'end_cursor': 'next'})
client = make_client(handler)
provider = DevinProvider(Settings(devin_mode='organization_sessions', devin_org_id='org/id',
devin_user_id=' user-123 ', devin_session_window_days=days), client)
result = await provider.get_usage()
assert result.status == 'ok'
assert result.source == 'Devin Organization Sessions API'
assert len(requests) == 2
assert requests[1].url.params.get('after') == 'next'
acus, count = result.limits
assert acus.id == 'organization-session-acus'
assert acus.name == '30-Day Session Usage'
assert acus.used == pytest.approx(20)
assert acus.unit == 'ACUs'
assert acus.window == 'sessions created in the last 30 days'
assert all(getattr(acus, field) is None for field in ('used_percent', 'remaining_percent', 'remaining', 'limit', 'reset_at'))
assert count.id == 'organization-session-count'
assert count.used == 2
assert count.unit == 'sessions'
assert count.window == 'created in the last 30 days'
assert all(getattr(count, field) is None for field in ('used_percent', 'remaining_percent', 'remaining', 'limit', 'reset_at'))
await client.aclose()
@pytest.mark.asyncio
async def test_organization_sessions_requires_org_without_request():
requests = []
async def handler(request):
requests.append(request)
return httpx.Response(200, json={'items': []})
client = make_client(handler)
provider = DevinProvider(Settings(devin_mode='organization_sessions'), client)
result = await provider.get_usage()
assert result.status == 'error'
assert result.error == 'Unable to retrieve usage information'
assert requests == []
await client.aclose()
@pytest.mark.asyncio
async def test_organization_sessions_forbidden_is_generic():
async def handler(request):
return httpx.Response(403, text='secret organization response')
client = make_client(handler)
provider = DevinProvider(Settings(devin_mode='organization_sessions', devin_org_id='org'), client)
result = await provider.get_usage()
assert result.status == 'error'
assert result.error == 'Unable to retrieve usage information'
assert 'secret organization response' not in result.model_dump_json()
await client.aclose()
@pytest.mark.asyncio
@pytest.mark.parametrize('bad_acus', [-1, 'not-a-number'])
async def test_organization_sessions_rejects_bad_acus(bad_acus):
async def handler(request):
return httpx.Response(200, json={'items': [
{'session_id': 'session-1', 'created_at': 1_800_000_000, 'acus_consumed': bad_acus},
]})
client = make_client(handler)
provider = DevinProvider(Settings(devin_mode='organization_sessions', devin_org_id='org'), client)
result = await provider.get_usage()
assert result.status == 'error'
assert result.error == 'Unable to retrieve usage information'
await client.aclose()
+4
View File
@@ -13,4 +13,8 @@ def test_frontend_assets_exist_and_use_api():
assert "fetch('/api/usage'" in script
assert 'entry.id === limit.id' in script
assert 'requestAnimationFrame' in script
assert "if (limit.used != null) return `${fmt(limit.used)} ${limit.unit || ''}`.trim();" in script
assert "meta.classList.add('standalone')" in script
assert '.meta.standalone .amount' in css
assert 'font-size: clamp(24px, 2.2vw, 34px)' in css
assert 'grid-template-columns' in css