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:
2026-06-18 22:09:34 +00:00
parent 95dca73291
commit 6ceca3d0d4
146 changed files with 6079 additions and 392 deletions
+20 -10
View File
@@ -336,16 +336,22 @@ class DevPlaceBot:
return ""
return (key or "").strip()
async def _adopt_account_api_key(self) -> None:
async def _adopt_account_api_key(self) -> bool:
if self.state.account_api_key:
return
key = await self._fetch_account_api_key()
if not key:
return
self.state.account_api_key = key
self.llm.api_key = key
self._save()
self._log("Adopted account API key for gateway calls")
self.llm.api_key = self.state.account_api_key
return True
for attempt in range(5):
key = await self._fetch_account_api_key()
if key:
self.state.account_api_key = key
self.llm.api_key = key
self._save()
self._log("Adopted account API key for gateway calls")
return True
if attempt < 4:
await asyncio.sleep(2)
self._log("Account API key unavailable after retries")
return False
async def _read_like_human(self, min_s: float = 3.0, max_s: float = 90.0) -> None:
if not self.state.reading_speed_wpm:
@@ -2833,7 +2839,11 @@ class DevPlaceBot:
await asyncio.sleep(60)
continue
await self._adopt_account_api_key()
if not await self._adopt_account_api_key():
self._log("No own API key yet; ending session to avoid shared-key billing")
await b.close()
await asyncio.sleep(60)
continue
self._bind_monitor()
+2 -2
View File
@@ -8,8 +8,8 @@ BASE_URL_DEFAULT = "https://pravda.education"
API_URL_DEFAULT = INTERNAL_GATEWAY_URL
NEWS_API_DEFAULT = "https://news.app.molodetz.nl/api"
MODEL_DEFAULT = INTERNAL_MODEL
INPUT_COST_PER_1M_DEFAULT = 0.27
OUTPUT_COST_PER_1M_DEFAULT = 1.10
INPUT_COST_PER_1M_DEFAULT = 0.14
OUTPUT_COST_PER_1M_DEFAULT = 0.28
COST_WINDOW_SECONDS = 600
COST_WARMUP_SECONDS = 120
+124 -37
View File
@@ -9,15 +9,16 @@ from devplacepy.services.bot.config import HANDLE_SUFFIX_WORDS
MIN_HANDLE_LEN = 3
MAX_HANDLE_LEN = 20
MIN_SEED_TOKEN_LEN = 3
LEET_PROBABILITY = 0.35
LEET_CHAR_PROBABILITY = 0.5
NUMBER_SUFFIX_PROBABILITY = 0.30
SEPARATOR_PROBABILITY = 0.30
CAPITALIZE_PROBABILITY = 0.22
ALLCAPS_PROBABILITY = 0.08
LEET_CHAR_PROBABILITY = 0.35
NUMBER_SUFFIX_PROBABILITY = 0.26
DECORATION_STYLES = ["plain", "leet", "vowel"]
DECORATION_WEIGHTS = [64, 22, 14]
CAPITALIZE_PROBABILITY = 0.20
ALLCAPS_PROBABILITY = 0.06
ALLCAPS_MAX_LEN = 8
PAD_NUMBER_FLOOR = 10
PAD_NUMBER_CEIL = 9999
VOWELS = "aeiou"
LEET_MAP = {
"a": "4",
@@ -39,33 +40,60 @@ TECH_NOUNS = [
"mutex", "syntax", "binary", "octet", "raster", "shader", "codec",
"vertex", "matrix", "scalar", "payload", "opcode", "runtime", "compiler",
"parser", "lexer", "sandbox", "firmware", "bytecode", "checksum",
"entropy", "signal", "overflow", "nibbler", "cursor", "patch",
"entropy", "signal", "overflow", "nibbler", "cursor", "patch", "semaphore",
"spinlock", "deadlock", "webhook", "endpoint", "gateway", "registry",
"pipeline", "tensor", "gradient", "cluster", "shard", "replica", "schema",
"commit", "branch", "rebase", "stash", "blob", "symlink", "inode", "mmap",
"syscall", "interrupt", "watchdog", "heartbeat", "beacon", "subnet",
"datagram", "loopback", "nullref", "dangle", "leak", "trace", "kilobyte",
"qubit", "monad", "closure", "thunk", "fiber", "coro", "atom",
]
HANDLE_ADJECTIVES = [
"dark", "mad", "lazy", "quantum", "cyber", "neon", "ghost", "hyper",
"ultra", "neo", "retro", "raw", "zero", "async", "blind", "silent",
"atomic", "fatal", "rogue", "idle", "stale", "dirty", "lucid", "prime",
"hex", "cold", "lone", "wired", "broken", "feral",
"hex", "cold", "lone", "wired", "broken", "feral", "stray", "numb",
"vivid", "molten", "frozen", "hollow", "jagged", "static", "volatile",
"nomadic", "unsigned", "verbose", "terse", "nocturnal", "caffeinated",
"deprecated", "headless", "stateless", "immutable", "recursive",
]
HANDLE_CREATURES = [
"fox", "wolf", "raven", "owl", "octopus", "gecko", "mantis", "viper",
"lynx", "moth", "hydra", "kraken", "phoenix", "dragon", "falcon",
"panther", "cobra", "badger", "otter", "sloth", "hornet", "beetle",
"lemur", "narwhal", "basilisk",
"lemur", "narwhal", "basilisk", "magpie", "heron", "ferret", "weasel",
"jackal", "marten", "axolotl", "tardigrade", "wyvern", "griffin",
"salamander", "newt", "stoat", "crow", "shrike", "ibis",
]
HANDLE_PREFIXES = ["the", "mr", "dr", "lord", "captain", "uncle", "sir", "prof", "agent"]
HANDLE_PREFIXES = [
"the", "mr", "dr", "lord", "captain", "uncle", "sir", "prof", "agent",
"old", "lil", "big", "saint", "baron", "comrade", "major",
]
NUMBER_SUFFIXES = ["42", "1337", "404", "64", "99", "007", "23", "88", "256", "512", "101", "777"]
ROLE_WORDS = [
"dev", "coder", "hacker", "ninja", "guru", "wizard", "smith", "monk",
"nomad", "ghost", "pilot", "surgeon", "gremlin", "goblin", "wrangler",
"jockey", "slinger", "herder", "whisperer", "monkey", "sage", "druid",
"ronin", "samurai", "pirate", "tinkerer", "alchemist", "janitor",
"plumber", "gardener", "scribe", "warden", "ranger", "miner", "forge",
]
SEPARATORS = ["_", "-"]
NUMBER_SUFFIXES = [
"42", "1337", "404", "64", "99", "007", "23", "88", "256", "512", "101",
"777", "13", "21", "69", "420", "127", "255", "80", "443", "3000", "8080",
"2k", "9000", "65535", "16", "32", "128",
]
VERSION_TAGS = [
"v2", "v3", "x64", "x86", "js", "py", "sh", "io", "fn", "db", "ai", "os",
"hq", "exe", "dev", "rs", "go", "cpp", "ml",
]
def _join(rng: random.Random, parts: list[str]) -> str:
separator = rng.choice(SEPARATORS) if rng.random() < SEPARATOR_PROBABILITY else ""
return separator.join(parts)
JOIN_STYLES = ["none", "under", "hyphen", "camel"]
JOIN_WEIGHTS = [46, 19, 12, 23]
def _seed_tokens(interests: list[str]) -> list[str]:
@@ -77,26 +105,81 @@ def _seed_tokens(interests: list[str]) -> list[str]:
return tokens
def _build_core(rng: random.Random, interests: list[str]) -> str:
seeds = _seed_tokens(interests)
builders = [
lambda: rng.choice(TECH_NOUNS),
lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(TECH_NOUNS)]),
lambda: _join(rng, [rng.choice(TECH_NOUNS), rng.choice(HANDLE_CREATURES)]),
lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(HANDLE_CREATURES)]),
lambda: _join(rng, [rng.choice(HANDLE_PREFIXES), rng.choice(TECH_NOUNS + HANDLE_CREATURES)]),
lambda: _join(rng, [rng.choice(TECH_NOUNS), rng.choice(HANDLE_SUFFIX_WORDS)]),
def _seed_word(rng: random.Random, seeds: list[str]) -> str:
return rng.choice(seeds) if seeds else rng.choice(TECH_NOUNS)
def _tokens(rng: random.Random, seeds: list[str]) -> list[str]:
noun = lambda: rng.choice(TECH_NOUNS)
adjective = lambda: rng.choice(HANDLE_ADJECTIVES)
creature = lambda: rng.choice(HANDLE_CREATURES)
role = lambda: rng.choice(ROLE_WORDS)
prefix = lambda: rng.choice(HANDLE_PREFIXES)
suffix = lambda: rng.choice(HANDLE_SUFFIX_WORDS)
seed = lambda: _seed_word(rng, seeds)
plans = [
(7, lambda: [noun()]),
(3, lambda: [role()]),
(8, lambda: [adjective(), noun()]),
(6, lambda: [noun(), creature()]),
(5, lambda: [adjective(), creature()]),
(5, lambda: [noun(), role()]),
(4, lambda: [adjective(), role()]),
(4, lambda: [creature(), role()]),
(4, lambda: [prefix(), noun()]),
(3, lambda: [prefix(), creature()]),
(4, lambda: [role(), noun()]),
(3, lambda: [noun(), suffix()]),
(3, lambda: [adjective(), suffix()]),
(3, lambda: [noun(), noun()]),
(2, lambda: [adjective(), noun(), creature()]),
(2, lambda: [prefix(), adjective(), noun()]),
(6, lambda: [seed(), role()]),
(5, lambda: [adjective(), seed()]),
(4, lambda: [seed(), noun()]),
(3, lambda: [seed()]),
]
weights = [3, 4, 3, 2, 2, 2]
if seeds:
builders.append(
lambda: _join(rng, [rng.choice(seeds), rng.choice(HANDLE_SUFFIX_WORDS + TECH_NOUNS)])
)
builders.append(lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(seeds)]))
weights += [5, 3]
weights = [weight for weight, _ in plans]
builders = [builder for _, builder in plans]
return rng.choices(builders, weights=weights)[0]()
def _dedupe(rng: random.Random, tokens: list[str]) -> list[str]:
result: list[str] = []
for token in tokens:
if result and result[-1] == token:
token = rng.choice(TECH_NOUNS)
result.append(token)
return result
def _fuse(rng: random.Random, tokens: list[str]) -> str:
if len(tokens) == 1:
return tokens[0]
style = rng.choices(JOIN_STYLES, weights=JOIN_WEIGHTS)[0]
if style == "camel":
head, *rest = tokens
return head + "".join(part[:1].upper() + part[1:] for part in rest)
separator = {"none": "", "under": "_", "hyphen": "-"}[style]
return separator.join(tokens)
def _drop_vowels(rng: random.Random, text: str) -> str:
kept = [text[0]]
for char in text[1:]:
if char in VOWELS and rng.random() < 0.55:
continue
kept.append(char)
result = "".join(kept)
return result if len(result) >= MIN_HANDLE_LEN else text
def _maybe_tag(rng: random.Random, core: str) -> str:
if rng.random() >= NUMBER_SUFFIX_PROBABILITY:
return core
return f"{core}{rng.choice(NUMBER_SUFFIXES + VERSION_TAGS)}"
def apply_leet(text: str, rng: random.Random) -> str:
chars: list[str] = []
for char in text:
@@ -108,7 +191,7 @@ def apply_leet(text: str, rng: random.Random) -> str:
return "".join(chars)
def _apply_case(text: str, rng: random.Random) -> str:
def _apply_case(rng: random.Random, text: str) -> str:
roll = rng.random()
if roll < ALLCAPS_PROBABILITY and len(text) <= ALLCAPS_MAX_LEN:
return text.upper()
@@ -125,13 +208,17 @@ def sanitize_handle(raw: str) -> str:
def make_handle(interests: Optional[list[str]] = None, rng: Optional[random.Random] = None) -> str:
generator = rng or random
core = _build_core(generator, interests or [])
if generator.random() < LEET_PROBABILITY:
seeds = _seed_tokens(interests or [])
core = _fuse(generator, _dedupe(generator, _tokens(generator, seeds)))
decoration = generator.choices(DECORATION_STYLES, weights=DECORATION_WEIGHTS)[0]
if decoration == "vowel" and core.isalpha() and core.islower() and len(core) > 6:
core = _drop_vowels(generator, core)
elif decoration == "leet":
core = apply_leet(core, generator)
if generator.random() < NUMBER_SUFFIX_PROBABILITY:
core = f"{core}{generator.choice(NUMBER_SUFFIXES)}"
if decoration != "leet":
core = _maybe_tag(generator, core)
handle = sanitize_handle(core)
if len(handle) < MIN_HANDLE_LEN:
handle = f"{handle}{generator.randint(PAD_NUMBER_FLOOR, PAD_NUMBER_CEIL)}"
handle = _apply_case(handle, generator)
handle = _apply_case(generator, handle)
return handle or f"dev{generator.randint(PAD_NUMBER_FLOOR, PAD_NUMBER_CEIL)}"
+32 -11
View File
@@ -19,6 +19,7 @@ from devplacepy.services.bot.config import (
TRIVIAL_GIST_TERMS,
)
from devplacepy.services.bot.handles import MAX_HANDLE_LEN, sanitize_handle
from devplacepy.services.openai_gateway.usage import parse_usage_headers
HANDLE_CANDIDATE_TARGET = 8
@@ -74,11 +75,8 @@ class LLMClient:
raw = resp.content
logger.info("LLM <<< %s", raw[:2000].decode(errors="replace"))
result = resp.json()
usage = result.get("usage", {})
in_tokens = usage.get("prompt_tokens", 0)
out_tokens = usage.get("completion_tokens", 0)
cost = (in_tokens * self.input_cost_per_1m / 1_000_000) + (
out_tokens * self.output_cost_per_1m / 1_000_000
in_tokens, out_tokens, cost = self._account_usage(
resp.headers, result.get("usage", {})
)
self.total_cost += cost
self.total_calls += 1
@@ -96,6 +94,21 @@ class LLMClient:
time.sleep(2**attempt)
return ""
def _account_usage(self, response_headers, body_usage: dict) -> tuple[int, int, float]:
parsed = parse_usage_headers(response_headers)
if parsed is not None:
in_tokens = parsed["prompt_tokens"]
out_tokens = parsed["completion_tokens"]
if not out_tokens and parsed["total_tokens"] > in_tokens:
out_tokens = parsed["total_tokens"] - in_tokens
return in_tokens, out_tokens, parsed["cost_usd"]
in_tokens = body_usage.get("prompt_tokens", 0)
out_tokens = body_usage.get("completion_tokens", 0)
cost = (in_tokens * self.input_cost_per_1m / 1_000_000) + (
out_tokens * self.output_cost_per_1m / 1_000_000
)
return in_tokens, out_tokens, cost
def _call(self, system: str, prompt: str, temperature: float = 0.7) -> str:
return self.clean(self._raw_call(system, prompt, temperature))
@@ -496,12 +509,20 @@ class LLMClient:
"You invent online handles for a developer signing up to a programmer "
"community in the style of devRant or Hacker News. The handles read like a "
"real nerd picked them, never like a person's full name. Lean on programming "
"and hacker culture: tech nouns (null, kernel, segfault, daemon), occasional "
"leetspeak (c0d3r, h4x), an adjective plus noun, a creature, or a short word "
f"with a number. Lowercase mostly, {MAX_HANDLE_LEN} characters or fewer, only "
"letters, digits, underscores and hyphens, no spaces and no dots. Return ONLY "
f'a JSON object {{"handles": ["...", "..."]}} with {HANDLE_CANDIDATE_TARGET} '
"distinct handles, no prose, no markdown fences. No em dashes."
"and hacker culture, but make every handle in the list a DIFFERENT shape so "
"they never feel formulaic. Mix these forms across the list: a single fused "
"word (segfault, mutexlord, kernelpanic); two words joined with no separator "
"(darkbyte, neonferret); camelCase (nullPointer, byteFox); an underscore or a "
"hyphen but NOT on most of them (lazy_daemon, cold-stack); leetspeak "
"(n0_scalar, c0d3r, h4xwolf); a word plus a number, port, or version tag "
"(void404, daemon1337, byte_v2, heap8080); dropped vowels (krnlpnc, bffr, "
"mtxguru); and an unexpected creature, role, or prefix (dr_segfault, "
"raven_smith, axolotl_dev). Vary the length from 4 to 18. Lowercase mostly "
"with the odd capital. Do NOT make them all the 'word_word' underscore "
f"pattern. {MAX_HANDLE_LEN} characters or fewer, only letters, digits, "
"underscores and hyphens, no spaces and no dots. Return ONLY a JSON object "
f'{{"handles": ["...", "..."]}} with {HANDLE_CANDIDATE_TARGET} distinct '
"handles, each a distinct shape, no prose, no markdown fences. No em dashes."
)
prompt = (
f"Archetype: {archetype}\n"
+2 -2
View File
@@ -98,7 +98,7 @@ class BotsService(BaseService):
type="float",
default=config.INPUT_COST_PER_1M_DEFAULT,
minimum=0,
help="Used to compute live spend per bot and across the fleet.",
help="Fallback pricing, used only when the LLM endpoint returns no gateway cost headers. The internal gateway reports authoritative cache-aware cost per call.",
group="LLM",
),
ConfigField(
@@ -107,7 +107,7 @@ class BotsService(BaseService):
type="float",
default=config.OUTPUT_COST_PER_1M_DEFAULT,
minimum=0,
help="Used to compute live spend per bot and across the fleet.",
help="Fallback pricing, used only when the LLM endpoint returns no gateway cost headers. The internal gateway reports authoritative cache-aware cost per call.",
group="LLM",
),
ConfigField(