tools.manage_api_keys module

Per-user API key management + global/channel API key pools (v4)

Per-user keys follow the user across channels and servers. Pool keys are shared – either globally or scoped to a single channel.

Resolution order: user key -> channel pool -> global pool -> env/config

Redis key patterns:

stargazer:user_api_keys:{user_id} (per-user hash) stargazer:global_api_pool:{service} (global pool hash) stargazer:channel_api_pool:{channel_id}:{service} (channel pool hash)

API keys are encrypted at rest (AES-256-GCM) with per-user keys stored in SQLite.

tools.manage_api_keys.missing_api_key_error(service)[source]

Return a verbose, actionable error message for a missing API key.

Intended to be called by consumer tools when get_user_api_key returns None. The message includes the service display name, signup URL (when known), and the exact set_user_api_key command so the LLM can relay it to the user.

Return type:

str

Parameters:

service (str)

async tools.manage_api_keys.check_default_key_limit(user_id, tool_name, redis_client, daily_limit=20)[source]

Check whether a user is within the daily shared-key usage limit.

Reads the per-user, per-tool counter at stargazer:default_key_usage:{user_id}:{tool_name} so tools that fall back to a shared (non-user-supplied) API key can cap free usage. The check is fail-open: any missing Redis client, missing counter, or Redis error is treated as “allowed” so a Redis hiccup never blocks legitimate calls. Reads Redis only; increment_default_key_usage performs the matching write.

Called by many sibling tools that consume shared keys – e.g. tools/generate_image.py, tools/brave_search.py, tools/research_tool.py, tools/play_music.py, tools/youtube_describe.py – typically guarded by default_key_limit_applies.

Parameters:
  • user_id (str) – The calling user’s id.

  • tool_name (str) – The tool the counter is scoped to.

  • redis_client – The Redis client, or None to bypass the check.

  • daily_limit (int) – The per-day ceiling (default 20).

Returns:

(allowed, current_count, daily_limit).

Return type:

tuple[bool, int, int]

async tools.manage_api_keys.increment_default_key_usage(user_id, tool_name, redis_client)[source]

Bump the daily shared-key usage counter after a successful call.

Records one use of a shared (non-user-supplied) API key by incrementing the per-user, per-tool counter at stargazer:default_key_usage:{user_id}:{tool_name} and (re)setting its TTL to _DEFAULT_KEY_TTL (24 hours), so the limit window rolls forward and the counter self-expires. Issues the INCR and EXPIRE in a single Redis pipeline; a missing client is a no-op and Redis errors are logged but swallowed so counting never breaks the tool that just succeeded. The companion read is check_default_key_limit.

Called by the same shared-key tools after they complete a call – e.g. tools/generate_image.py, tools/brave_search.py, tools/research_tool.py, tools/play_music.py, tools/youtube_describe.py.

Parameters:
  • user_id (str) – The calling user’s id.

  • tool_name (str) – The tool the counter is scoped to.

  • redis_client – The Redis client, or None to skip incrementing.

Return type:

None

Returns:

None

tools.manage_api_keys.default_key_limit_error(tool_name, current, limit)[source]

Build the user-facing message shown when the shared-key limit is hit.

Produces a verbose, actionable string a tool returns to the LLM once check_default_key_limit reports the user is over quota: it states the current count and limit and tells the user to set their own (unlimited) key via set_user_api_key. Pure string formatting with no I/O.

Called by shared-key tools when the daily limit is exceeded – e.g. tools/generate_veo_video.py, tools/brave_search.py, tools/research_tool.py, tools/play_music.py, tools/youtube_describe.py.

Parameters:
  • tool_name (str) – The tool the limit applies to.

  • current (int) – The user’s current usage count.

  • limit (int) – The daily ceiling that was reached.

Returns:

A human-readable explanation and remediation message.

Return type:

str

async tools.manage_api_keys.default_key_limit_applies(ctx, using_own_key=False)[source]

Return True if the shared default-key daily limit should be enforced.

Returns False (i.e. limit is not enforced) when any of these hold: - using_own_key is True (caller supplied their own API key) - ctx is None, or ctx.user_id / ctx.redis are missing - The calling user is a bot admin (config.admin_user_ids) - The calling user has the BYPASS_RATELIMIT privilege bit set

All other callers are subject to the limit. Safe to call multiple times per request (one lightweight Redis GET per call).

Return type:

bool

Parameters:

using_own_key (bool)

async tools.manage_api_keys.get_gitea_credentials(user_id, *, redis_client=None, channel_id=None, fallback_to_pool=True, config=None)[source]

Return (token, base_url) for Gitea, or None.

Resolution order: user key -> channel pool -> global pool -> GITEA_TOKEN -> GITEA_TOKEN_FILE (default GITEA_TOKEN_FILE_DEFAULT).

Parses plain token or JSON {“token”: “…”, “base_url”: “…”}. Env/file fallbacks use GITEA_BASE_URL (default https://git.neko.li) when the raw value is a plain token; JSON may override with base_url.

Return type:

tuple[str, str] | None

Parameters:
  • user_id (str)

  • channel_id (str | None)

  • fallback_to_pool (bool)

async tools.manage_api_keys.get_pool_api_key(service, *, redis_client=None)[source]

Return a random shared key from the global pool for a service.

Public helper used during key resolution: it names the global pool hash via _pool_key(service) and draws a decrypted key through _get_pool_api_key_impl. Returns None (rather than raising) when no Redis client is supplied or anything fails, logging the failure so a degraded pool never breaks the calling tool. Reads Redis only.

Called by get_user_api_key in this module as the final fallback in the resolution chain; not invoked directly by other modules.

Parameters:
  • service (str) – The service to draw a pooled key for.

  • redis_client – The Redis client to use, or None to skip the pool.

Returns:

A pooled plaintext API key, or None if unavailable.

Return type:

str | None

async tools.manage_api_keys.get_channel_pool_api_key(channel_id, service, *, redis_client=None)[source]

Return a random shared key from a channel’s pool for a service.

Public helper used during key resolution: it names the channel-scoped pool hash via _channel_pool_key(channel_id, service) and draws a decrypted key through _get_pool_api_key_impl. Returns None (rather than raising) when no Redis client or channel id is supplied or anything fails, logging the failure so a degraded pool never breaks the calling tool. Reads Redis only.

Called by get_user_api_key in this module, preferred over the global pool when a channel id is in scope; not invoked directly by other modules.

Parameters:
  • channel_id (str) – The channel whose pool to draw from.

  • service (str) – The service to draw a pooled key for.

  • redis_client – The Redis client to use, or None to skip the pool.

Returns:

A pooled plaintext API key, or None if unavailable.

Return type:

str | None

async tools.manage_api_keys.get_user_api_key(user_id, service, *, redis_client=None, channel_id=None, fallback_to_pool=True, config=None)[source]

Return the best available API key for service.

Resolution order: user key -> channel pool -> global pool.

Return type:

str | None

Parameters:
  • user_id (str)

  • service (str)

  • channel_id (str | None)

  • fallback_to_pool (bool)