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_PREFIX with the user, platform, and channel into the single key stargazer:user_llm:{user_id}:{platform}:{channel_id} under which the api_url / model / api_key / toggle_specific fields 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(), and clear_all_user_llm_config()) to address the hash, and by tests/test_user_llm_config.py to assert on stored fields. It only computes a string and touches no I/O.

Parameters:
  • user_id (str) – Identifier of the user owning the overrides.

  • platform (str) – Source platform name (e.g. discord, matrix).

  • channel_id (str) – Channel/room identifier scoping the overrides.

Returns:

The fully qualified Redis hash key.

Return type:

str

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 /v1 when it is not a legitimate subpath. This is the single hardening choke point for inbound API base URLs.

Called by config.py when resolving the bot’s own llm_base_url, by chat_completions_url() in this module, and by openrouter_client/transport.py when building request URLs (including per-user override chat URLs). Pure string work; no I/O.

Parameters:

url (str) – Raw base URL, possibly with whitespace, newlines, or trailing junk.

Returns:

The cleaned base URL (empty string if url was falsy).

Return type:

str

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 model fields (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.

Return type:

str

Parameters:
  • raw (str | None)

  • max_len (int)

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.py to compute the override chat URL that gets handed to the inference transport when a user has set a custom api_url. Pure string work; no I/O.

Parameters:

base_url (str) – An OpenAI-compatible API base (e.g. ending in /v1).

Returns:

The sanitized base with /chat/completions appended.

Return type:

str

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() via HGETALL, keeps only the recognized fields in _HASH_FIELDS, coerces toggle_specific to a boolean, and transparently decrypts a stored api_key when it is encrypted at rest. For an encrypted key it resolves the process master key via resolve_master_key and the per-user key via get_or_create_user_key (which reads the SQLite store located by _encryption_db_path()), then calls decrypt. This is the read side of the !apiurl / !model / !apikey override 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.py before building an inference request to fetch any active overrides, and exercised by tests/test_proxy_toggle.py. Touches Redis (HGETALL) and, for an encrypted key, the encryption-key SQLite database.

Parameters:
  • redis – Async Redis client; None short-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), and toggle_specific (bool). Empty if nothing is set, Redis is unavailable, or a decrypted api_key could not be recovered.

Return type:

dict[str, Any]

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 / !apikey commands. It normalizes the field name and value, rejects anything outside _HASH_FIELDS, and applies per-field rules: api_url must be http/https and within MAX_API_URL_LEN; model must be non-empty and within MAX_MODEL_LEN; toggle_specific must be 1 or 0; and api_key must satisfy the length bounds and is encrypted before storage. Encryption requires a configured master key (resolve_master_key) and a per-user key from get_or_create_user_key (reading the SQLite store at _encryption_db_path()), after which the value is sealed with encrypt. On success it writes the field with HSET under the key from redis_key().

Returning the error as a string rather than raising lets the command handler surface a friendly chat reply.

Called by message_processor/processor.py from the _apply_field helper inside the user-LLM command dispatch, and exercised by tests/test_proxy_toggle.py. Touches Redis (HSET) and, for api_key, the encryption-key SQLite database.

Parameters:
  • redis – Async Redis client; None yields 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 of api_url, model, api_key, toggle_specific.

  • value (str) – Raw value to validate and store (encrypted for api_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 None when the field was stored successfully.

Return type:

str | None

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 with HDEL on the key from redis_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.py from the user-LLM command dispatch, including the !clear... reset commands and the cleanup that follows a rejected set. Touches Redis (HDEL).

Parameters:
  • redis – Async Redis client; None yields an error string.

  • user_id (str) – User whose override is being cleared.

  • platform (str) – Source platform name.

  • channel_id (str) – Channel/room identifier.

  • field (str) – One of api_url, model, api_key, toggle_specific.

Returns:

An error message on an invalid field or Redis failure, else None.

Return type:

str | None

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, encrypted api_key, and toggle_specific) by issuing a Redis DELETE on the key from redis_key(), fully resetting the user back to bot defaults for that channel. Unlike clear_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.py clears fields individually). Touches Redis (DELETE).

Parameters:
  • redis – Async Redis client; None yields an error string.

  • user_id (str) – User whose overrides are being wiped.

  • platform (str) – Source platform name.

  • channel_id (str) – Channel/room identifier.

Returns:

An error message on Redis failure, or None on success (including when the hash did not exist).

Return type:

str | None