user_llm_config
Per-user, per-channel LLM inference overrides (!apiurl / !model / !apikey).
Redis hash at stargazer:user_llm:{user_id}:{platform}:{channel_id} with
optional fields api_url, model, api_key (encrypted at rest).
- user_llm_config.redis_key(user_id, platform, channel_id)[source]
Build the Redis hash key holding a user’s LLM overrides for one channel.
Joins the module prefix
REDIS_KEY_PREFIXwith the user, platform, and channel into the single keystargazer:user_llm:{user_id}:{platform}:{channel_id}under which theapi_url/model/api_key/toggle_specificfields are stored. Keying per channel is what lets the same user carry different inference overrides in different rooms.Called by the get/set/clear helpers in this module (
get_user_llm_config(),set_user_llm_field(),clear_user_llm_field(), andclear_all_user_llm_config()) to address the hash, and bytests/test_user_llm_config.pyto assert on stored fields. It only computes a string and touches no I/O.
- user_llm_config.sanitize_llm_http_url(url)[source]
Normalize a user-supplied OpenAI-compatible base URL before it is used or stored.
Trims surrounding whitespace, strips embedded CR/LF (which would otherwise enable header-injection style mischief), and then delegates to
_truncate_junk_after_openai_v1_path()to discard Discord-embed or pasted-HTML garbage that follows/v1when it is not a legitimate subpath. This is the single hardening choke point for inbound API base URLs.Called by
config.pywhen resolving the bot’s ownllm_base_url, bychat_completions_url()in this module, and byopenrouter_client/transport.pywhen building request URLs (including per-user override chat URLs). Pure string work; no I/O.
- user_llm_config.sanitize_llm_model_id_display(raw, *, max_len=120)[source]
Restrict raw to characters typical of provider model ids before echoing it in UI text.
User-supplied
modelfields (via overrides or hostile upstream bodies) must not emit raw angle brackets, quotes, Markdown, JSON breaks, control characters, or other payloads that could alter HTML/Matrix formatting when shown as-assistant-output.
- user_llm_config.chat_completions_url(base_url)[source]
Derive the full chat-completions endpoint from an OpenAI-compatible base URL.
First runs the base through
sanitize_llm_http_url()so any user-pasted junk or newlines are removed, then appends/chat/completions(collapsing any trailing slashes) to yield the concrete POST target. This keeps per-user override URLs and the bot default on the same sanitized path-building rules.Called by
message_processor/generate_and_send.pyto compute the override chat URL that gets handed to the inference transport when a user has set a customapi_url. Pure string work; no I/O.
- async user_llm_config.get_user_llm_config(redis, user_id, platform, channel_id, *, config=None)[source]
Load and decrypt a user’s per-channel LLM overrides from Redis.
Reads the hash addressed by
redis_key()viaHGETALL, keeps only the recognized fields in_HASH_FIELDS, coercestoggle_specificto a boolean, and transparently decrypts a storedapi_keywhen it is encrypted at rest. For an encrypted key it resolves the process master key viaresolve_master_keyand the per-user key viaget_or_create_user_key(which reads the SQLite store located by_encryption_db_path()), then callsdecrypt. This is the read side of the!apiurl/!model/!apikeyoverride feature, letting inference honor a user’s custom endpoint, model, and credentials for one channel.Failures are swallowed defensively and logged: a Redis error, a missing master key, or a decrypt error each cause the affected field (or the whole result) to be omitted rather than raised, so a bad override never breaks message handling.
Called by
message_processor/generate_and_send.pybefore building an inference request to fetch any active overrides, and exercised bytests/test_proxy_toggle.py. Touches Redis (HGETALL) and, for an encrypted key, the encryption-key SQLite database.- Parameters:
redis – Async Redis client;
Noneshort-circuits to an empty result.user_id (
str) – User whose overrides to load; falsy short-circuits to empty.platform (
str) – Source platform name.channel_id (
str) – Channel/room identifier.config (
Any|None) – Optional bot config, used only to locate the encryption-key database.
- Returns:
Present overrides among
api_url,model,api_key(decrypted), andtoggle_specific(bool). Empty if nothing is set, Redis is unavailable, or a decryptedapi_keycould not be recovered.- Return type:
- async user_llm_config.set_user_llm_field(redis, user_id, platform, channel_id, field, value, *, config=None)[source]
Validate and persist one LLM-override field to the user’s Redis hash.
The write side of the
!apiurl/!model/!apikeycommands. It normalizes the field name and value, rejects anything outside_HASH_FIELDS, and applies per-field rules:api_urlmust behttp/httpsand withinMAX_API_URL_LEN;modelmust be non-empty and withinMAX_MODEL_LEN;toggle_specificmust be1or0; andapi_keymust satisfy the length bounds and is encrypted before storage. Encryption requires a configured master key (resolve_master_key) and a per-user key fromget_or_create_user_key(reading the SQLite store at_encryption_db_path()), after which the value is sealed withencrypt. On success it writes the field withHSETunder the key fromredis_key().Returning the error as a string rather than raising lets the command handler surface a friendly chat reply.
Called by
message_processor/processor.pyfrom the_apply_fieldhelper inside the user-LLM command dispatch, and exercised bytests/test_proxy_toggle.py. Touches Redis (HSET) and, forapi_key, the encryption-key SQLite database.- Parameters:
redis – Async Redis client;
Noneyields an error string.user_id (
str) – User whose override is being set.platform (
str) – Source platform name.channel_id (
str) – Channel/room identifier.field (
str) – One ofapi_url,model,api_key,toggle_specific.value (
str) – Raw value to validate and store (encrypted forapi_key).config (
Any|None) – Optional bot config, used only to locate the encryption-key database.
- Returns:
A human-readable error message on rejection or Redis failure, or
Nonewhen the field was stored successfully.- Return type:
- async user_llm_config.clear_user_llm_field(redis, user_id, platform, channel_id, field)[source]
Delete a single LLM-override field from the user’s Redis hash.
Reverts one setting back to the bot default without disturbing the user’s other overrides for the channel. Normalizes and validates the field name against
_HASH_FIELDS, then removes it withHDELon the key fromredis_key(); deleting an absent field is a harmless no-op. Like its siblings it returns the error as a string so the command handler can echo it to chat rather than raising.Called by
message_processor/processor.pyfrom the user-LLM command dispatch, including the!clear...reset commands and the cleanup that follows a rejectedset. Touches Redis (HDEL).- Parameters:
- Returns:
An error message on an invalid field or Redis failure, else
None.- Return type:
- async user_llm_config.clear_all_user_llm_config(redis, user_id, platform, channel_id)[source]
Delete a user’s entire LLM-override hash for one channel.
Wipes every stored field at once (
api_url,model, encryptedapi_key, andtoggle_specific) by issuing a RedisDELETEon the key fromredis_key(), fully resetting the user back to bot defaults for that channel. Unlikeclear_user_llm_field(), this is all-or-nothing and does not touch the encryption key store. As with the other helpers it reports problems as a returned string instead of raising.A convenience entry point exported for callers wanting a one-shot reset; no in-repo caller currently invokes it (the command dispatch in
message_processor/processor.pyclears fields individually). Touches Redis (DELETE).- Parameters:
- Returns:
An error message on Redis failure, or
Noneon success (including when the hash did not exist).- Return type: