Current section
Files
Jump to
Current section
Files
lib/codex/protocol/rate_limit.ex
defmodule Codex.Protocol.RateLimit do
@moduledoc """
Rate limit snapshot types for TokenCount events.
"""
defmodule Helpers do
@moduledoc false
@spec normalize_float(term(), keyword()) :: {:ok, float()} | {:error, String.t()}
def normalize_float(nil, _opts), do: {:error, "missing used_percent"}
def normalize_float(value, _opts) when is_integer(value), do: {:ok, value * 1.0}
def normalize_float(value, _opts) when is_float(value), do: {:ok, value}
def normalize_float(_value, _opts), do: {:error, "invalid used_percent"}
@spec normalize_keys(map(), map()) :: map()
def normalize_keys(%{} = data, mapping) when is_map(mapping) do
data
|> Enum.map(fn {key, value} ->
{normalize_key(key, mapping), normalize_nested_value(value, mapping)}
end)
|> Map.new()
end
def normalize_key(key, mapping) when is_atom(key) do
key
|> Atom.to_string()
|> normalize_key(mapping)
end
def normalize_key(key, mapping) when is_binary(key), do: Map.get(mapping, key, key)
def normalize_nested_value(%{} = value, mapping), do: normalize_keys(value, mapping)
def normalize_nested_value(value, mapping) when is_list(value),
do: Enum.map(value, &normalize_nested_value(&1, mapping))
def normalize_nested_value(value, _mapping), do: value
@spec maybe_put(map(), String.t(), term()) :: map()
def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)
end
defmodule Window do
@moduledoc "A rate limit window"
use TypedStruct
alias Codex.Protocol.RateLimit.Helpers
alias Codex.Schema
@key_mapping %{
"usedPercent" => "used_percent",
"windowMinutes" => "window_minutes",
"windowDurationMins" => "window_minutes",
"window_duration_mins" => "window_minutes",
"resetsAt" => "resets_at"
}
@known_fields ["used_percent", "window_minutes", "resets_at"]
@schema Zoi.map(
%{
"used_percent" => Zoi.number() |> Zoi.transform({Helpers, :normalize_float, []}),
"window_minutes" => Zoi.optional(Zoi.nullish(Zoi.integer())),
"resets_at" => Zoi.optional(Zoi.nullish(Zoi.integer()))
},
unrecognized_keys: :preserve
)
typedstruct do
field(:used_percent, float(), enforce: true)
field(:window_minutes, integer() | nil)
field(:resets_at, integer() | nil)
field(:extra, map(), default: %{})
end
@spec schema() :: Zoi.schema()
def schema, do: @schema
@spec parse(map() | keyword() | t()) ::
{:ok, t()}
| {:error, {:invalid_rate_limit_window, CliSubprocessCore.Schema.error_detail()}}
def parse(%__MODULE__{} = window), do: {:ok, window}
def parse(data) when is_list(data), do: parse(Enum.into(data, %{}))
def parse(data) do
case Schema.parse(@schema, normalize_input(data), :invalid_rate_limit_window) do
{:ok, parsed} ->
{known, extra} = Schema.split_extra(parsed, @known_fields)
{:ok,
%__MODULE__{
used_percent: Map.fetch!(known, "used_percent"),
window_minutes: Map.get(known, "window_minutes"),
resets_at: Map.get(known, "resets_at"),
extra: extra
}}
{:error, {:invalid_rate_limit_window, details}} ->
{:error, {:invalid_rate_limit_window, details}}
end
end
@spec parse!(map() | keyword() | t()) :: t()
def parse!(%__MODULE__{} = window), do: window
def parse!(data) when is_list(data), do: parse!(Enum.into(data, %{}))
def parse!(data) do
parsed = Schema.parse!(@schema, normalize_input(data), :invalid_rate_limit_window)
{known, extra} = Schema.split_extra(parsed, @known_fields)
%__MODULE__{
used_percent: Map.fetch!(known, "used_percent"),
window_minutes: Map.get(known, "window_minutes"),
resets_at: Map.get(known, "resets_at"),
extra: extra
}
end
@spec from_map(map()) :: t()
def from_map(data), do: parse!(data)
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = window) do
%{"used_percent" => window.used_percent}
|> Helpers.maybe_put("window_minutes", window.window_minutes)
|> Helpers.maybe_put("resets_at", window.resets_at)
|> Map.merge(window.extra)
end
defp normalize_input(%{} = data), do: Helpers.normalize_keys(data, @key_mapping)
defp normalize_input(other), do: other
end
defmodule CreditsSnapshot do
@moduledoc "Credits balance snapshot"
use TypedStruct
alias CliSubprocessCore.Schema.Conventions
alias Codex.Protocol.RateLimit.Helpers
alias Codex.Schema
@key_mapping %{
"hasCredits" => "has_credits",
"isUnlimited" => "unlimited"
}
@known_fields ["has_credits", "unlimited", "balance"]
@schema Zoi.map(
%{
"has_credits" => Zoi.boolean(),
"unlimited" => Zoi.boolean(),
"balance" => Conventions.optional_trimmed_string()
},
unrecognized_keys: :preserve
)
typedstruct do
field(:has_credits, boolean(), enforce: true)
field(:unlimited, boolean(), enforce: true)
field(:balance, String.t() | nil)
field(:extra, map(), default: %{})
end
@spec schema() :: Zoi.schema()
def schema, do: @schema
@spec parse(map() | keyword() | t()) ::
{:ok, t()}
| {:error, {:invalid_rate_limit_credits, CliSubprocessCore.Schema.error_detail()}}
def parse(%__MODULE__{} = credits), do: {:ok, credits}
def parse(data) when is_list(data), do: parse(Enum.into(data, %{}))
def parse(data) do
case Schema.parse(@schema, normalize_input(data), :invalid_rate_limit_credits) do
{:ok, parsed} ->
{known, extra} = Schema.split_extra(parsed, @known_fields)
{:ok,
%__MODULE__{
has_credits: Map.fetch!(known, "has_credits"),
unlimited: Map.fetch!(known, "unlimited"),
balance: Map.get(known, "balance"),
extra: extra
}}
{:error, {:invalid_rate_limit_credits, details}} ->
{:error, {:invalid_rate_limit_credits, details}}
end
end
@spec parse!(map() | keyword() | t()) :: t()
def parse!(%__MODULE__{} = credits), do: credits
def parse!(data) when is_list(data), do: parse!(Enum.into(data, %{}))
def parse!(data) do
parsed = Schema.parse!(@schema, normalize_input(data), :invalid_rate_limit_credits)
{known, extra} = Schema.split_extra(parsed, @known_fields)
%__MODULE__{
has_credits: Map.fetch!(known, "has_credits"),
unlimited: Map.fetch!(known, "unlimited"),
balance: Map.get(known, "balance"),
extra: extra
}
end
@spec from_map(map()) :: t()
def from_map(data), do: parse!(data)
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = snapshot) do
%{
"has_credits" => snapshot.has_credits,
"unlimited" => snapshot.unlimited
}
|> Helpers.maybe_put("balance", snapshot.balance)
|> Map.merge(snapshot.extra)
end
defp normalize_input(%{} = data), do: Helpers.normalize_keys(data, @key_mapping)
defp normalize_input(other), do: other
end
defmodule Snapshot do
@moduledoc "Complete rate limit snapshot"
use TypedStruct
alias CliSubprocessCore.Schema.Conventions
alias Codex.Protocol.RateLimit.{CreditsSnapshot, Helpers, Window}
alias Codex.Schema
@type plan_type :: :plus | :pro | :team | :enterprise | :api | nil
@key_mapping %{
"planType" => "plan_type",
"usedPercent" => "used_percent",
"windowMinutes" => "window_minutes",
"windowDurationMins" => "window_minutes",
"window_duration_mins" => "window_minutes",
"resetsAt" => "resets_at",
"hasCredits" => "has_credits",
"isUnlimited" => "unlimited"
}
@known_fields ["primary", "secondary", "credits", "plan_type"]
@schema Zoi.map(
%{
"primary" => Zoi.optional(Zoi.nullish(Window.schema())),
"secondary" => Zoi.optional(Zoi.nullish(Window.schema())),
"credits" => Zoi.optional(Zoi.nullish(CreditsSnapshot.schema())),
"plan_type" => Conventions.optional_any()
},
unrecognized_keys: :preserve
)
typedstruct do
field(:primary, Window.t() | nil)
field(:secondary, Window.t() | nil)
field(:credits, CreditsSnapshot.t() | nil)
field(:plan_type, plan_type())
field(:extra, map(), default: %{})
end
@spec schema() :: Zoi.schema()
def schema, do: @schema
@spec parse(map() | keyword() | t()) ::
{:ok, t()}
| {:error, {:invalid_rate_limit_snapshot, CliSubprocessCore.Schema.error_detail()}}
def parse(%__MODULE__{} = snapshot), do: {:ok, snapshot}
def parse(data) when is_list(data), do: parse(Enum.into(data, %{}))
def parse(data) do
case Schema.parse(@schema, normalize_input(data), :invalid_rate_limit_snapshot) do
{:ok, parsed} ->
{known, extra} = Schema.split_extra(parsed, @known_fields)
{:ok,
%__MODULE__{
primary: parse_window(Map.get(known, "primary")),
secondary: parse_window(Map.get(known, "secondary")),
credits: parse_credits(Map.get(known, "credits")),
plan_type: Map.get(known, "plan_type") |> parse_plan_type(),
extra: extra
}}
{:error, {:invalid_rate_limit_snapshot, details}} ->
{:error, {:invalid_rate_limit_snapshot, details}}
end
end
@spec parse!(map() | keyword() | t()) :: t()
def parse!(%__MODULE__{} = snapshot), do: snapshot
def parse!(data) when is_list(data), do: parse!(Enum.into(data, %{}))
def parse!(data) do
parsed = Schema.parse!(@schema, normalize_input(data), :invalid_rate_limit_snapshot)
{known, extra} = Schema.split_extra(parsed, @known_fields)
%__MODULE__{
primary: parse_window(Map.get(known, "primary")),
secondary: parse_window(Map.get(known, "secondary")),
credits: parse_credits(Map.get(known, "credits")),
plan_type: Map.get(known, "plan_type") |> parse_plan_type(),
extra: extra
}
end
@spec from_map(map()) :: t()
def from_map(data), do: parse!(data)
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = snapshot) do
%{}
|> Helpers.maybe_put("primary", snapshot.primary && Window.to_map(snapshot.primary))
|> Helpers.maybe_put("secondary", snapshot.secondary && Window.to_map(snapshot.secondary))
|> Helpers.maybe_put(
"credits",
snapshot.credits && CreditsSnapshot.to_map(snapshot.credits)
)
|> Helpers.maybe_put("plan_type", snapshot.plan_type && Atom.to_string(snapshot.plan_type))
|> Map.merge(snapshot.extra)
end
defp normalize_input(%{} = data), do: Helpers.normalize_keys(data, @key_mapping)
defp normalize_input(other), do: other
defp parse_window(nil), do: nil
defp parse_window(data), do: Window.parse!(data)
defp parse_credits(nil), do: nil
defp parse_credits(data), do: CreditsSnapshot.parse!(data)
defp parse_plan_type(nil), do: nil
defp parse_plan_type("plus"), do: :plus
defp parse_plan_type("pro"), do: :pro
defp parse_plan_type("team"), do: :team
defp parse_plan_type("enterprise"), do: :enterprise
defp parse_plan_type("api"), do: :api
defp parse_plan_type(_), do: nil
end
end