feat: docs and ui tweaks

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-07-06 19:26:35 -07:00
parent 64f2b4a6eb
commit 271a21aee6
103 changed files with 2161 additions and 2625 deletions

View file

@ -28,7 +28,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.context import AuthContext
from app.capabilities.core.access.rate_limit import enforce_capability_rate_limit
from app.capabilities.core.billing import charge_capability, gate_capability
from app.capabilities.core.billing import (
charge_capability,
gate_capability,
pricing_meters,
)
from app.capabilities.core.events import run_event_bus
from app.capabilities.core.progress import progress_scope
from app.capabilities.core.runs import (
@ -55,13 +59,22 @@ _SSE_HEADERS = {
}
class PricingMeter(BaseModel):
"""One live per-item rate a verb charges on, e.g. 3500 micro-USD per place."""
unit: str
micros_per_unit: int
class CapabilitySummary(BaseModel):
"""A verb's identity + input/output JSON schemas, for the playground UI."""
"""A verb's identity + input/output JSON schemas + pricing, for the playground UI."""
name: str
description: str
input_schema: dict
output_schema: dict
# Empty list = free (billing disabled or an unmetered verb).
pricing: list[PricingMeter] = []
class RunSummary(BaseModel):
@ -125,12 +138,17 @@ def _register_capabilities_list(
) -> None:
"""Register the ``GET`` that lists verbs + their input schemas for the UI."""
summaries = [
CapabilitySummary(
name=capability.name,
description=capability.description,
input_schema=capability.input_schema.model_json_schema(),
output_schema=capability.output_schema.model_json_schema(),
# Schemas are static; pricing is attached per request because rates are
# read live from config (env retune + restart, no rebuild).
base_summaries = [
(
CapabilitySummary(
name=capability.name,
description=capability.description,
input_schema=capability.input_schema.model_json_schema(),
output_schema=capability.output_schema.model_json_schema(),
),
capability.billing_unit,
)
for capability in capabilities
]
@ -141,7 +159,12 @@ def _register_capabilities_list(
auth: AuthContext = Depends(get_auth_context),
) -> list[CapabilitySummary]:
await check_workspace_access(session, auth, workspace_id)
return summaries
return [
summary.model_copy(
update={"pricing": [PricingMeter(**m) for m in pricing_meters(unit)]}
)
for summary, unit in base_summaries
]
router.add_api_route(
"/workspaces/{workspace_id}/scrapers/capabilities",

View file

@ -43,6 +43,53 @@ def _platform_rate(unit: BillingUnit) -> int:
return int(getattr(config, _PLATFORM_RATE_KEYS[unit]))
# Display noun for each platform meter, e.g. "$3.50 / 1k places".
_UNIT_NOUNS: dict[BillingUnit, str] = {
BillingUnit.REDDIT_ITEM: "item",
BillingUnit.GOOGLE_SEARCH_SERP: "SERP",
BillingUnit.GOOGLE_MAPS_PLACE: "place",
BillingUnit.GOOGLE_MAPS_REVIEW: "review",
BillingUnit.YOUTUBE_VIDEO: "video",
BillingUnit.YOUTUBE_COMMENT: "comment",
}
def pricing_meters(unit: BillingUnit | None) -> list[dict]:
"""The live per-item rates a verb charges, for UI display. Empty = free.
Mirrors the gate/charge logic exactly: meters whose billing flag is off are
omitted, so a self-hosted install with billing disabled reads as free.
"""
if unit is None:
return []
if unit is BillingUnit.WEB_CRAWL:
meters = []
if WebCrawlCreditService.billing_enabled():
meters.append(
{"unit": "page", "micros_per_unit": config.WEB_CRAWL_MICROS_PER_SUCCESS}
)
if WebCrawlCreditService.captcha_billing_enabled() and captcha_enabled():
meters.append(
{
"unit": "captcha solve",
"micros_per_unit": config.WEB_CRAWL_CAPTCHA_MICROS_PER_SOLVE,
}
)
return meters
if not config.PLATFORM_SCRAPE_BILLING_ENABLED:
return []
meters = [{"unit": _UNIT_NOUNS[unit], "micros_per_unit": _platform_rate(unit)}]
if unit is BillingUnit.GOOGLE_MAPS_PLACE:
# Dual-metered: attached reviews bill on their own meter.
meters.append(
{
"unit": _UNIT_NOUNS[BillingUnit.GOOGLE_MAPS_REVIEW],
"micros_per_unit": _platform_rate(BillingUnit.GOOGLE_MAPS_REVIEW),
}
)
return meters
async def gate_capability(
payload: BillableInput, unit: BillingUnit | None, ctx: CapabilityContext
) -> None: