forked from retoor/devplacepy
docs: document server-side rendering pipeline, response timing middleware, and Telegram pairing API
- Add comprehensive documentation for backend content rendering in AGENTS.md, detailing the new `render_content` and `render_title` Jinja globals built on mistune with media processing, emoji shortcodes, and XSS protection
- Document the `X-Response-Time` header and bottom-left render time indicator in README.md
- Update bot token pricing documentation to clarify fallback vs gateway cost headers
- Add `email_accounts` to soft-delete tables and `idx_users_role` composite index in database schema
- Implement `telegram_pairings` and `telegram_links` table creation with column migration and indexes
- Add `/profile/{username}/telegram` endpoint to docs API with request/unpair actions
- Register `TelegramService` in main.py lifespan and add `response_timing` middleware emitting `X-Response-Time` header
- Introduce `TelegramPairForm` model and `guard_public_host_sync` synchronous host validation function
This commit is contained in:
@@ -25,7 +25,21 @@ def _now() -> datetime:
|
||||
|
||||
|
||||
def _iso(moment: datetime) -> str:
|
||||
return moment.isoformat()
|
||||
return moment.isoformat(timespec="microseconds")
|
||||
|
||||
|
||||
def _hours_since(iso_value: Optional[str], now: datetime) -> float:
|
||||
try:
|
||||
moment = datetime.fromisoformat(iso_value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
return max(0.0, (now - moment).total_seconds() / 3600.0)
|
||||
|
||||
|
||||
def _project_monthly(cost: float, observed_hours: float) -> float:
|
||||
if observed_hours >= 1.0:
|
||||
return cost / observed_hours * 24 * 30
|
||||
return cost * 30
|
||||
|
||||
|
||||
def _pset(values: list[float]) -> dict:
|
||||
@@ -163,6 +177,11 @@ def build_analytics(
|
||||
first_hour[owner] = bucket
|
||||
|
||||
total_cost = sum(float(r.get("cost_usd") or 0) for r in rows)
|
||||
token_cost = sum(
|
||||
float(r.get("cost_usd") or 0)
|
||||
for r in rows
|
||||
if int(r.get("total_tokens") or 0) > 0
|
||||
)
|
||||
input_cost = sum(float(r.get("input_cost_usd") or 0) for r in rows)
|
||||
output_cost = sum(float(r.get("output_cost_usd") or 0) for r in rows)
|
||||
cost_this_hour = sum(
|
||||
@@ -170,8 +189,12 @@ def build_analytics(
|
||||
for r in rows
|
||||
if r["created_at"][:13] == hour_start
|
||||
)
|
||||
cost_24h = sum(
|
||||
float(r.get("cost_usd") or 0) for r in rows if r["created_at"] >= day_cutoff
|
||||
last_24h_rows = [r for r in rows if r["created_at"] >= day_cutoff]
|
||||
cost_24h = sum(float(r.get("cost_usd") or 0) for r in last_24h_rows)
|
||||
observed_24h_hours = (
|
||||
min(24.0, _hours_since(last_24h_rows[0]["created_at"], now))
|
||||
if last_24h_rows
|
||||
else 0.0
|
||||
)
|
||||
|
||||
prompt_total = sum(int(r.get("prompt_tokens") or 0) for r in rows)
|
||||
@@ -187,7 +210,7 @@ def build_analytics(
|
||||
chat_cache_hits = sum(
|
||||
int(r.get("cache_hit_tokens") or 0)
|
||||
for r in rows
|
||||
if r.get("backend") == "chat"
|
||||
if r.get("backend") == "chat" and not int(r.get("native_cost") or 0)
|
||||
)
|
||||
caching_savings = chat_cache_hits / PER_MILLION * rate_delta
|
||||
|
||||
@@ -308,8 +331,8 @@ def build_analytics(
|
||||
"output_usd": round(output_cost, 6),
|
||||
"this_hour_usd": round(cost_this_hour, 6),
|
||||
"last_24h_usd": round(cost_24h, 6),
|
||||
"projected_monthly_usd": round(cost_24h * 30, 2),
|
||||
"effective_per_1k_tokens_usd": round(total_cost / total_tokens * 1000, 6)
|
||||
"projected_monthly_usd": round(_project_monthly(cost_24h, observed_24h_hours), 2),
|
||||
"effective_per_1k_tokens_usd": round(token_cost / total_tokens * 1000, 6)
|
||||
if total_tokens
|
||||
else 0.0,
|
||||
"caching_savings_usd": round(caching_savings, 6),
|
||||
@@ -509,7 +532,9 @@ def build_user_usage(
|
||||
avg_latency = round(sum(latencies) / len(latencies), 1) if latencies else 0.0
|
||||
tps = _positive(rows, "tokens_per_second")
|
||||
avg_tps = round(sum(tps) / len(tps), 2) if tps else 0.0
|
||||
cost_per_hour = total_cost / hours
|
||||
observed_hours = min(float(hours), _hours_since(rows[0]["created_at"], now))
|
||||
rate_hours = observed_hours if observed_hours >= 1.0 else float(hours)
|
||||
cost_per_hour = total_cost / rate_hours
|
||||
cost_per_request = total_cost / requests if requests else 0.0
|
||||
|
||||
return {
|
||||
@@ -539,7 +564,9 @@ def build_user_usage(
|
||||
"by_backend": _top_group(rows, lambda r: r.get("backend") or "unknown", 0),
|
||||
"hourly": _user_hourly(rows),
|
||||
"notes": {
|
||||
"projection": "30-day projection extrapolates the full 24h spend (24h cost x 30)"
|
||||
"projection": "30-day projection from the observed hourly burn rate "
|
||||
"(per-hour spend x 24 x 30); windows with under an hour of activity fall "
|
||||
"back to a conservative window-average to avoid over-extrapolation"
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ class GatewayRuntime:
|
||||
handle_start = time.monotonic()
|
||||
messages = body.get("messages", []) or []
|
||||
|
||||
vision_cost = 0.0
|
||||
if cfg["gateway_vision_enabled"]:
|
||||
augmenter = VisionAugmenter(
|
||||
cfg["gateway_vision_url"],
|
||||
@@ -238,6 +239,7 @@ class GatewayRuntime:
|
||||
)
|
||||
messages = await augmenter.augment_messages(client, messages)
|
||||
self.vision_calls += augmenter.calls
|
||||
vision_cost = augmenter.cost_usd
|
||||
|
||||
messages = apply_system_directives(messages, cfg.get("gateway_system_preamble", ""))
|
||||
|
||||
@@ -300,9 +302,13 @@ class GatewayRuntime:
|
||||
base["success"] = success
|
||||
base["error_category"] = category
|
||||
base["usage"] = usage
|
||||
return usage_response_headers(
|
||||
self._ledger.record(base, pricing, context_map)
|
||||
)
|
||||
row = self._ledger.record(base, pricing, context_map)
|
||||
headers = usage_response_headers(row)
|
||||
if vision_cost and headers:
|
||||
chat_cost = float((row or {}).get("cost_usd") or 0.0)
|
||||
headers["X-Gateway-Cost-USD"] = f"{chat_cost + vision_cost:.8f}"
|
||||
headers["X-Gateway-Vision-Cost-USD"] = f"{vision_cost:.8f}"
|
||||
return headers
|
||||
|
||||
if timing["circuit_open"]:
|
||||
resp_headers = finalize(503, False, "circuit_open")
|
||||
@@ -370,6 +376,7 @@ class GatewayRuntime:
|
||||
self, body: dict, cfg: dict, owner: tuple, user_agent: str, log=None
|
||||
):
|
||||
log = log or (lambda message: None)
|
||||
vision_cost = 0.0
|
||||
overlay = embed_overlay(body.get("model"), cfg)
|
||||
if overlay:
|
||||
cfg = {**cfg, **overlay}
|
||||
@@ -465,9 +472,13 @@ class GatewayRuntime:
|
||||
base["success"] = success
|
||||
base["error_category"] = category
|
||||
base["usage"] = usage
|
||||
return usage_response_headers(
|
||||
self._ledger.record(base, pricing, context_map)
|
||||
)
|
||||
row = self._ledger.record(base, pricing, context_map)
|
||||
headers = usage_response_headers(row)
|
||||
if vision_cost and headers:
|
||||
chat_cost = float((row or {}).get("cost_usd") or 0.0)
|
||||
headers["X-Gateway-Cost-USD"] = f"{chat_cost + vision_cost:.8f}"
|
||||
headers["X-Gateway-Vision-Cost-USD"] = f"{vision_cost:.8f}"
|
||||
return headers
|
||||
|
||||
if timing["circuit_open"]:
|
||||
resp_headers = finalize(503, False, "circuit_open")
|
||||
@@ -538,6 +549,7 @@ class GatewayRuntime:
|
||||
log=None,
|
||||
):
|
||||
log = log or (lambda message: None)
|
||||
vision_cost = 0.0
|
||||
client, sem = self._ensure(cfg)
|
||||
pricing = pricing_from_cfg(cfg)
|
||||
context_map = parse_context_map(cfg.get("gateway_model_context_map"))
|
||||
@@ -583,9 +595,13 @@ class GatewayRuntime:
|
||||
base["success"] = success
|
||||
base["error_category"] = category
|
||||
base["usage"] = usage
|
||||
return usage_response_headers(
|
||||
self._ledger.record(base, pricing, context_map)
|
||||
)
|
||||
row = self._ledger.record(base, pricing, context_map)
|
||||
headers = usage_response_headers(row)
|
||||
if vision_cost and headers:
|
||||
chat_cost = float((row or {}).get("cost_usd") or 0.0)
|
||||
headers["X-Gateway-Cost-USD"] = f"{chat_cost + vision_cost:.8f}"
|
||||
headers["X-Gateway-Vision-Cost-USD"] = f"{vision_cost:.8f}"
|
||||
return headers
|
||||
|
||||
if timing["circuit_open"]:
|
||||
resp_headers = finalize(503, False, "circuit_open")
|
||||
|
||||
@@ -354,6 +354,7 @@ def chat_overlay(requested_model: Optional[str], base_cfg: dict) -> Optional[dic
|
||||
overlay["gateway_vision_enabled"] = True
|
||||
overlay["gateway_vision_model"] = route.vision_model
|
||||
overlay["gateway_vision_price_input_per_m"] = route.price_input_per_m
|
||||
overlay["gateway_vision_price_output_per_m"] = route.price_output_per_m
|
||||
vision_provider = route.vision_provider or route.provider
|
||||
if vision_provider:
|
||||
_provider_overlay(
|
||||
|
||||
@@ -25,7 +25,7 @@ def _now() -> datetime:
|
||||
|
||||
|
||||
def _iso(moment: datetime) -> str:
|
||||
return moment.isoformat()
|
||||
return moment.isoformat(timespec="microseconds")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -120,24 +120,30 @@ def normalize_usage(usage: Optional[dict]) -> dict:
|
||||
def compute_cost(
|
||||
usage: dict, norm: dict, pricing: Pricing, backend: str
|
||||
) -> tuple[float, float, float, bool]:
|
||||
native = usage.get("cost") if isinstance(usage, dict) else None
|
||||
if isinstance(native, (int, float)) and not isinstance(native, bool):
|
||||
total = float(native)
|
||||
denom = norm["prompt"] + norm["completion"]
|
||||
input_cost = total * norm["prompt"] / denom if denom > 0 else 0.0
|
||||
return total, input_cost, total - input_cost, True
|
||||
if backend == "vision":
|
||||
input_cost = norm["prompt"] / PER_MILLION * pricing.vision_input_per_m
|
||||
output_cost = norm["completion"] / PER_MILLION * pricing.vision_output_per_m
|
||||
return input_cost + output_cost, input_cost, output_cost, False
|
||||
if backend == "embed":
|
||||
elif backend == "embed":
|
||||
input_cost = norm["prompt"] / PER_MILLION * pricing.embed_input_per_m
|
||||
return input_cost, input_cost, 0.0, False
|
||||
input_cost = (
|
||||
norm["cache_hit"] / PER_MILLION * pricing.chat_cache_hit_per_m
|
||||
+ norm["cache_miss"] / PER_MILLION * pricing.chat_cache_miss_per_m
|
||||
)
|
||||
output_cost = norm["completion"] / PER_MILLION * pricing.chat_output_per_m
|
||||
output_cost = 0.0
|
||||
else:
|
||||
input_cost = (
|
||||
norm["cache_hit"] / PER_MILLION * pricing.chat_cache_hit_per_m
|
||||
+ norm["cache_miss"] / PER_MILLION * pricing.chat_cache_miss_per_m
|
||||
)
|
||||
output_cost = norm["completion"] / PER_MILLION * pricing.chat_output_per_m
|
||||
native = usage.get("cost") if isinstance(usage, dict) else None
|
||||
if isinstance(native, (int, float)) and not isinstance(native, bool):
|
||||
total = max(0.0, float(native))
|
||||
modeled = input_cost + output_cost
|
||||
if modeled > 0:
|
||||
input_share = input_cost / modeled
|
||||
elif norm["prompt"] + norm["completion"] > 0:
|
||||
input_share = norm["prompt"] / (norm["prompt"] + norm["completion"])
|
||||
else:
|
||||
input_share = 0.0
|
||||
native_input = total * input_share
|
||||
return total, native_input, total - native_input, True
|
||||
return input_cost + output_cost, input_cost, output_cost, False
|
||||
|
||||
|
||||
@@ -249,6 +255,35 @@ def usage_response_headers(row: Optional[dict]) -> dict:
|
||||
return headers
|
||||
|
||||
|
||||
def parse_usage_headers(headers) -> Optional[dict]:
|
||||
if not headers or "X-Gateway-Cost-USD" not in headers:
|
||||
return None
|
||||
|
||||
def _int(name: str) -> int:
|
||||
try:
|
||||
return int(headers.get(name) or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
def _float(name: str) -> float:
|
||||
try:
|
||||
return float(headers.get(name) or 0.0)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
return {
|
||||
"calls": 1,
|
||||
"model": headers.get("X-Gateway-Model") or "",
|
||||
"prompt_tokens": _int("X-Gateway-Prompt-Tokens"),
|
||||
"completion_tokens": _int("X-Gateway-Completion-Tokens"),
|
||||
"total_tokens": _int("X-Gateway-Total-Tokens"),
|
||||
"cost_usd": _float("X-Gateway-Cost-USD"),
|
||||
"native_cost": headers.get("X-Gateway-Cost-Native") == "1",
|
||||
"upstream_latency_ms": _float("X-Gateway-Upstream-Latency-Ms"),
|
||||
"total_latency_ms": _float("X-Gateway-Total-Latency-Ms"),
|
||||
}
|
||||
|
||||
|
||||
class GatewayUsageLedger:
|
||||
def record(self, raw: dict, pricing: Pricing, context_map: dict) -> Optional[dict]:
|
||||
try:
|
||||
|
||||
@@ -106,11 +106,12 @@ class VisionAugmenter:
|
||||
self.pricing = pricing
|
||||
self.context_map = context_map or {}
|
||||
self.calls = 0
|
||||
self.cost_usd = 0.0
|
||||
|
||||
def _record(self, latency_ms, status_code, success, category, usage):
|
||||
if self.ledger is None or self.pricing is None:
|
||||
return
|
||||
self.ledger.record(
|
||||
row = self.ledger.record(
|
||||
{
|
||||
"owner_kind": self.owner[0],
|
||||
"owner_id": self.owner[1],
|
||||
@@ -129,6 +130,8 @@ class VisionAugmenter:
|
||||
self.pricing,
|
||||
self.context_map,
|
||||
)
|
||||
if row:
|
||||
self.cost_usd += float(row.get("cost_usd") or 0.0)
|
||||
|
||||
async def _describe_one(self, client: httpx.AsyncClient, image_block: dict) -> str:
|
||||
if not self.vision_key:
|
||||
|
||||
Reference in New Issue
Block a user