feat(kiwi): add /orch-usage proxy endpoint for frontend budget display
This commit is contained in:
parent
1a6898324c
commit
006582b179
4 changed files with 110 additions and 1 deletions
24
.gitleaks.toml
Normal file
24
.gitleaks.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Kiwi gitleaks config — extends base CircuitForge config with local rules
|
||||||
|
|
||||||
|
[extend]
|
||||||
|
path = "/Library/Development/CircuitForge/circuitforge-hooks/gitleaks.toml"
|
||||||
|
|
||||||
|
# ── Test fixture allowlists ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[[rules]]
|
||||||
|
id = "cf-generic-env-token"
|
||||||
|
description = "Generic KEY=<token> in env-style assignment — catches FORGEJO_API_TOKEN=hex etc."
|
||||||
|
regex = '''(?i)(token|secret|key|password|passwd|pwd|api_key)\s*[=:]\s*['"]?[A-Za-z0-9\-_]{20,}['"]?'''
|
||||||
|
[rules.allowlist]
|
||||||
|
paths = [
|
||||||
|
'.*test.*',
|
||||||
|
]
|
||||||
|
regexes = [
|
||||||
|
'api_key:\s*ollama',
|
||||||
|
'api_key:\s*any',
|
||||||
|
'your-[a-z\-]+-here',
|
||||||
|
'replace-with-',
|
||||||
|
'xxxx',
|
||||||
|
'test-fixture-',
|
||||||
|
'CFG-KIWI-TEST-',
|
||||||
|
]
|
||||||
27
app/api/endpoints/orch_usage.py
Normal file
27
app/api/endpoints/orch_usage.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""Proxy endpoint: exposes cf-orch call budget to the Kiwi frontend.
|
||||||
|
|
||||||
|
Only lifetime/founders users have a license_key — subscription and free
|
||||||
|
users receive null (no budget UI shown).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app.cloud_session import CloudUser, get_session
|
||||||
|
from app.services.heimdall_orch import get_orch_usage
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def orch_usage_endpoint(
|
||||||
|
session: CloudUser = Depends(get_session),
|
||||||
|
) -> dict | None:
|
||||||
|
"""Return the current period's orch usage for the authenticated user.
|
||||||
|
|
||||||
|
Returns null if the user has no lifetime/founders license key (i.e. they
|
||||||
|
are on a subscription or free plan — no budget cap applies to them).
|
||||||
|
"""
|
||||||
|
if session.license_key is None:
|
||||||
|
return None
|
||||||
|
return get_orch_usage(session.license_key, "kiwi")
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from app.api.endpoints import health, receipts, export, inventory, ocr, recipes, settings, staples, feedback, household, saved_recipes, imitate, meal_plans
|
from app.api.endpoints import health, receipts, export, inventory, ocr, recipes, settings, staples, feedback, household, saved_recipes, imitate, meal_plans, orch_usage
|
||||||
from app.api.endpoints.community import router as community_router
|
from app.api.endpoints.community import router as community_router
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
@ -17,4 +17,5 @@ api_router.include_router(feedback.router, prefix="/feedback", tags=
|
||||||
api_router.include_router(household.router, prefix="/household", tags=["household"])
|
api_router.include_router(household.router, prefix="/household", tags=["household"])
|
||||||
api_router.include_router(imitate.router, prefix="/imitate", tags=["imitate"])
|
api_router.include_router(imitate.router, prefix="/imitate", tags=["imitate"])
|
||||||
api_router.include_router(meal_plans.router, prefix="/meal-plans", tags=["meal-plans"])
|
api_router.include_router(meal_plans.router, prefix="/meal-plans", tags=["meal-plans"])
|
||||||
|
api_router.include_router(orch_usage.router, prefix="/orch-usage", tags=["orch-usage"])
|
||||||
api_router.include_router(community_router)
|
api_router.include_router(community_router)
|
||||||
|
|
|
||||||
57
tests/api/test_orch_usage.py
Normal file
57
tests/api/test_orch_usage.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""Tests for the /orch-usage proxy endpoint."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.cloud_session import CloudUser, get_session
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
def _make_session(license_key=None, tier="paid"):
|
||||||
|
return CloudUser(
|
||||||
|
user_id="user-1",
|
||||||
|
tier=tier,
|
||||||
|
db=Path("/tmp/kiwi_test.db"),
|
||||||
|
has_byok=False,
|
||||||
|
license_key=license_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_orch_usage_returns_data_for_lifetime_user():
|
||||||
|
"""GET /orch-usage with a lifetime key returns usage data."""
|
||||||
|
app.dependency_overrides[get_session] = lambda: _make_session(
|
||||||
|
license_key="CFG-KIWI-TEST-0000-0000"
|
||||||
|
)
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
with patch("app.api.endpoints.orch_usage.get_orch_usage") as mock_usage:
|
||||||
|
mock_usage.return_value = {
|
||||||
|
"calls_used": 10,
|
||||||
|
"topup_calls": 0,
|
||||||
|
"calls_total": 60,
|
||||||
|
"period_start": "2026-04-14",
|
||||||
|
"resets_on": "2026-05-14",
|
||||||
|
}
|
||||||
|
resp = client.get("/api/v1/orch-usage")
|
||||||
|
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["calls_used"] == 10
|
||||||
|
assert data["calls_total"] == 60
|
||||||
|
|
||||||
|
|
||||||
|
def test_orch_usage_returns_null_for_subscription_user():
|
||||||
|
"""GET /orch-usage with no license_key returns null."""
|
||||||
|
app.dependency_overrides[get_session] = lambda: _make_session(
|
||||||
|
license_key=None, tier="paid"
|
||||||
|
)
|
||||||
|
client = TestClient(app)
|
||||||
|
resp = client.get("/api/v1/orch-usage")
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json() is None
|
||||||
Loading…
Reference in a new issue