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):
- Session override (temporary, not persisted)
- Thread-level policy (most specific route)
- Peer/Chat-level policy
- Account-level policy
- Channel-type policy (least specific route)
- 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
@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
@type model_id() :: String.t()
Model identifier string
@type policy() :: %{ :model_id => model_id(), optional(:thinking_level) => thinking_level(), optional(:metadata) => metadata() }
Model policy struct
@type thinking_level() :: :off | :minimal | :low | :medium | :high | :xhigh | nil
Thinking level for reasoning models
Functions
@spec clear(LemonChannels.ModelPolicy.Route.t()) :: :ok | {:error, term()}
Clears the policy for a specific route.
Examples
iex> ModelPolicy.clear(route)
:ok
@spec clear_channel(LemonChannels.ModelPolicy.Route.channel_id()) :: :ok
Clears all policies for a channel.
Examples
iex> ModelPolicy.clear_channel("telegram")
:ok
@spec exists?(LemonChannels.ModelPolicy.Route.t()) :: boolean()
Checks if a policy exists for the exact route.
Examples
iex> ModelPolicy.exists?(route)
true
@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
@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: "...", ...}}, ...]
@spec list(LemonChannels.ModelPolicy.Route.channel_id()) :: [ {LemonChannels.ModelPolicy.Route.t(), 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: _}}
@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:
- Exact route match (thread-level)
- Peer-level (without thread)
- Account-level
- 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}
@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
@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
@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
@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