LemonChannels.ModelPolicy (lemon_channels v0.1.0)

View Source

Route-based model policy management for Lemon.

Provides persistent storage and resolution of model preferences at different granularities: channel, account, peer (chat/DM), and thread.

Policy Structure

A model policy consists of:

  • model_id - The AI model identifier (e.g., "claude-sonnet-4-20250514")
  • thinking_level - Optional thinking level (:minimal, :low, :medium, :high, :xhigh)
  • metadata - Additional policy metadata (set_by, reason, timestamp, etc.)

Precedence

Policies are resolved with the following precedence (highest to lowest):

  1. Session override (temporary, not persisted)
  2. Thread-level policy (most specific route)
  3. Peer/Chat-level policy
  4. Account-level policy
  5. Channel-type policy (least specific route)
  6. Global default (from config)

Usage

# Set a policy for a specific Telegram thread
route = Route.new("telegram", "default", "-1001234567890", "456")
policy = ModelPolicy.new_policy("claude-sonnet-4-20250514", thinking_level: :medium)
ModelPolicy.set(route, policy)

# Resolve policy for a route (finds most specific match)
{:ok, resolved} = ModelPolicy.resolve(route)

# Clear a policy
ModelPolicy.clear(route)

# List all policies for a channel
policies = ModelPolicy.list("telegram")

Summary

Types

Policy metadata map

Model identifier string

Model policy struct

Thinking level for reasoning models

Functions

Clears the policy for a specific route.

Clears all policies for a channel.

Checks if a policy exists for the exact route.

Gets the exact policy for a route without resolution.

Lists all policies, optionally filtered by channel.

Creates a new policy map with the given model and options.

Resolves the effective policy for a route using precedence rules.

Resolves the effective model ID for a route.

Resolves the effective thinking level for a route.

Sets a model policy for a specific route.

Updates a policy's metadata without changing the model.

Types

metadata()

@type metadata() :: %{
  optional(:set_by) => String.t(),
  optional(:reason) => String.t(),
  optional(:set_at_ms) => integer(),
  optional(:updated_at_ms) => integer(),
  optional(atom()) => term()
}

Policy metadata map

model_id()

@type model_id() :: String.t()

Model identifier string

policy()

@type policy() :: %{
  :model_id => model_id(),
  optional(:thinking_level) => thinking_level(),
  optional(:metadata) => metadata()
}

Model policy struct

thinking_level()

@type thinking_level() :: :off | :minimal | :low | :medium | :high | :xhigh | nil

Thinking level for reasoning models

Functions

clear(route)

@spec clear(LemonChannels.ModelPolicy.Route.t()) :: :ok | {:error, term()}

Clears the policy for a specific route.

Examples

iex> ModelPolicy.clear(route)
:ok

clear_channel(channel_id)

@spec clear_channel(LemonChannels.ModelPolicy.Route.channel_id()) :: :ok

Clears all policies for a channel.

Examples

iex> ModelPolicy.clear_channel("telegram")
:ok

exists?(route)

Checks if a policy exists for the exact route.

Examples

iex> ModelPolicy.exists?(route)
true

get(route)

@spec get(LemonChannels.ModelPolicy.Route.t()) :: policy() | nil

Gets the exact policy for a route without resolution.

Returns nil if no policy is set for the exact route.

Examples

iex> ModelPolicy.get(route)
%{model_id: "claude-sonnet-4-20250514", metadata: %{...}}

iex> ModelPolicy.get(unknown_route)
nil

list()

@spec list() :: [{LemonChannels.ModelPolicy.Route.t(), policy()}]

Lists all policies, optionally filtered by channel.

Examples

# All policies
iex> ModelPolicy.list()
[{%{channel_id: "telegram", ...}, %{model_id: "...", ...}}, ...]

# Policies for Telegram only
iex> ModelPolicy.list("telegram")
[{%{channel_id: "telegram", ...}, %{model_id: "...", ...}}, ...]

list(channel_id)

new_policy(model_id, opts \\ [])

@spec new_policy(
  model_id(),
  keyword()
) :: policy()

Creates a new policy map with the given model and options.

Options

  • :thinking_level - The thinking level (:minimal, :low, :medium, :high, :xhigh)
  • :set_by - Identifier of who/what set this policy
  • :reason - Optional reason for the policy
  • :metadata - Additional metadata to include

Examples

iex> ModelPolicy.new_policy("claude-sonnet-4-20250514")
%{model_id: "claude-sonnet-4-20250514", metadata: %{set_at_ms: _}}

iex> ModelPolicy.new_policy("gpt-4o", thinking_level: :medium, set_by: "admin")
%{model_id: "gpt-4o", thinking_level: :medium, metadata: %{set_by: "admin", set_at_ms: _}}

resolve(route)

@spec resolve(LemonChannels.ModelPolicy.Route.t()) ::
  {:ok, policy()} | {:error, :not_found}

Resolves the effective policy for a route using precedence rules.

Checks policies in order of specificity:

  1. Exact route match (thread-level)
  2. Peer-level (without thread)
  3. Account-level
  4. Channel-level

Returns {:ok, policy} if a policy is found, or {:error, :not_found} if no policy exists at any level.

Examples

iex> route = Route.new("telegram", "default", "-1001234567890", "456")
iex> ModelPolicy.resolve(route)
{:ok, %{model_id: "claude-sonnet-4-20250514", metadata: %{...}}}

iex> ModelPolicy.resolve(unknown_route)
{:error, :not_found}

resolve_model_id(route)

@spec resolve_model_id(LemonChannels.ModelPolicy.Route.t()) :: model_id() | nil

Resolves the effective model ID for a route.

Convenience function that returns just the model ID, or nil if not found.

Examples

iex> ModelPolicy.resolve_model_id(route)
"claude-sonnet-4-20250514"

iex> ModelPolicy.resolve_model_id(unknown_route)
nil

resolve_thinking_level(route)

@spec resolve_thinking_level(LemonChannels.ModelPolicy.Route.t()) :: thinking_level()

Resolves the effective thinking level for a route.

Examples

iex> ModelPolicy.resolve_thinking_level(route)
:medium

iex> ModelPolicy.resolve_thinking_level(unknown_route)
nil

set(route, policy)

@spec set(LemonChannels.ModelPolicy.Route.t(), policy()) :: :ok | {:error, term()}

Sets a model policy for a specific route.

Examples

iex> route = Route.new("telegram", "default", "-1001234567890", nil)
iex> policy = ModelPolicy.new_policy("claude-sonnet-4-20250514")
iex> ModelPolicy.set(route, policy)
:ok

update_metadata(route, updates)

@spec update_metadata(
  LemonChannels.ModelPolicy.Route.t(),
  keyword()
) :: :ok | {:error, term()}

Updates a policy's metadata without changing the model.

Examples

iex> ModelPolicy.update_metadata(route, reason: "Updated for cost optimization")
:ok