Current section

Files

Jump to
gemini_ex lib gemini apis enhanced_models.ex
Raw

lib/gemini/apis/enhanced_models.ex

defmodule Gemini.APIs.Models do
@moduledoc """
Complete Models API implementation following the unified architecture.
Provides comprehensive access to Gemini model information including:
- Listing available models with pagination
- Getting detailed model information
- Querying model capabilities and features
- Statistical analysis and filtering
## Examples
# List all available models
{:ok, response} = Models.list()
models = response.models
# Get specific model information
{:ok, model} = Models.get("gemini-2.0-flash")
# Check if model exists
{:ok, true} = Models.exists?("gemini-pro")
# Filter models by capabilities
{:ok, streaming_models} = Models.supporting_method("streamGenerateContent")
"""
alias Gemini.Client
alias Gemini.Types.Request.{ListModelsRequest, GetModelRequest}
alias Gemini.Types.Response.{ListModelsResponse, Model}
alias Gemini.{Error, Telemetry}
require Logger
@doc """
List available Gemini models with optional pagination.
## Parameters
- `opts` - Keyword list of options:
- `:page_size` - Maximum number of models per page (1-1000, default: 50)
- `:page_token` - Token for retrieving the next page of results
## Returns
- `{:ok, ListModelsResponse.t()}` - Success with models and pagination info
- `{:error, Error.t()}` - Validation error, API error, or network failure
## Examples
# List first 50 models (default)
{:ok, response} = Models.list()
models = response.models
next_token = response.next_page_token
# Custom page size
{:ok, response} = Models.list(page_size: 10)
# Pagination
{:ok, page1} = Models.list(page_size: 10)
{:ok, page2} = Models.list(page_size: 10, page_token: page1.next_page_token)
## API Reference
Corresponds to: `GET https://generativelanguage.googleapis.com/v1beta/models`
"""
@spec list(keyword()) :: {:ok, ListModelsResponse.t()} | {:error, Error.t()}
def list(opts \\ []) do
start_time = System.monotonic_time()
with {:ok, request} <- ListModelsRequest.new(opts),
query_params = ListModelsRequest.to_query_params(request),
path = "models#{query_params}",
telemetry_opts = build_telemetry_opts(:list_models, opts),
{:ok, response} <- Client.get(path, telemetry_opts),
{:ok, parsed_response} <- parse_list_models_response(response) do
# Emit success telemetry
emit_models_telemetry(:list, :success, start_time, %{
model_count: length(parsed_response.models),
page_size: Keyword.get(opts, :page_size)
})
{:ok, parsed_response}
else
{:error, %Error{} = error} ->
emit_models_telemetry(:list, :error, start_time, %{error_type: error.type})
{:error, error}
{:error, reason} when is_binary(reason) ->
error = Error.validation_error(reason)
emit_models_telemetry(:list, :error, start_time, %{error_type: :validation_error})
{:error, error}
end
end
@doc """
Get detailed information about a specific model.
## Parameters
- `model_name` - The model identifier, with or without "models/" prefix
Examples: "gemini-2.0-flash", "models/gemini-1.5-pro"
## Returns
- `{:ok, Model.t()}` - Success with model details
- `{:error, Error.t()}` - Model not found, validation error, or API error
## Examples
# Get model by base ID
{:ok, model} = Models.get("gemini-2.0-flash")
# Get model by full resource name
{:ok, model} = Models.get("models/gemini-1.5-pro")
# Handle not found cases properly in your application code
## API Reference
Corresponds to: `GET https://generativelanguage.googleapis.com/v1beta/{name=models/*}`
"""
@spec get(String.t()) :: {:ok, Model.t()} | {:error, Error.t()}
def get(model_name) do
start_time = System.monotonic_time()
with {:ok, request} <- GetModelRequest.new(model_name),
telemetry_opts =
build_telemetry_opts(:get_model, model: extract_base_model_id(request.name)),
{:ok, response} <- Client.get(request.name, telemetry_opts),
{:ok, parsed_model} <- parse_model_response(response) do
# Emit success telemetry
emit_models_telemetry(:get, :success, start_time, %{
model: extract_base_model_id(request.name)
})
{:ok, parsed_model}
else
{:error, %Error{} = error} ->
model_id = extract_base_model_id_safe(model_name)
emit_models_telemetry(:get, :error, start_time, %{
model: model_id,
error_type: error.type
})
{:error, error}
{:error, reason} when is_binary(reason) ->
error = Error.validation_error(reason)
model_id = extract_base_model_id_safe(model_name)
emit_models_telemetry(:get, :error, start_time, %{
model: model_id,
error_type: :validation_error
})
{:error, error}
end
end
@doc """
List all available model names as simple strings.
This is a convenience function that extracts just the base model IDs
from the full models list response.
## Returns
- `{:ok, [String.t()]}` - List of base model IDs
- `{:error, Error.t()}` - API error
## Examples
{:ok, names} = Models.list_names()
# => ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"]
# Use with enum functions
{:ok, names} = Models.list_names()
flash_models = Enum.filter(names, &String.contains?(&1, "flash"))
"""
@spec list_names() :: {:ok, [String.t()]} | {:error, Error.t()}
def list_names do
case list() do
{:ok, %ListModelsResponse{models: models}} ->
names =
models
|> Enum.map(&Model.effective_base_id/1)
|> Enum.uniq()
|> Enum.sort()
{:ok, names}
{:error, error} ->
{:error, error}
end
end
@doc """
Check if a specific model exists and is available.
## Parameters
- `model_name` - The model identifier to check
## Returns
- `{:ok, true}` - Model exists and is available
- `{:ok, false}` - Model does not exist
- `{:error, Error.t()}` - Network or other API error
## Examples
{:ok, true} = Models.exists?("gemini-2.0-flash")
{:ok, false} = Models.exists?("nonexistent-model")
# Use in conditional logic
case Models.exists?("gemini-pro") do
{:ok, true} -> generate_with_model("gemini-pro")
{:ok, false} -> use_fallback_model()
{:error, _} -> handle_api_error()
end
"""
@spec exists?(String.t()) :: {:ok, boolean()} | {:error, Error.t()}
def exists?(model_name) when is_binary(model_name) do
case get(model_name) do
{:ok, _model} ->
{:ok, true}
{:error, %Error{type: :api_error, http_status: 404}} ->
{:ok, false}
{:error, %Error{api_reason: 404}} ->
{:ok, false}
{:error, error} ->
{:error, error}
end
end
@doc """
Get models that support a specific generation method.
## Parameters
- `method` - The generation method to filter by
Examples: "generateContent", "streamGenerateContent", "countTokens"
## Returns
- `{:ok, [Model.t()]}` - List of models supporting the method
- `{:error, Error.t()}` - API error
## Examples
# Find streaming-capable models
{:ok, streaming_models} = Models.supporting_method("streamGenerateContent")
# Find models that support content generation
{:ok, generation_models} = Models.supporting_method("generateContent")
# Check capabilities
{:ok, models} = Models.supporting_method("countTokens")
token_counting_available = length(models) > 0
"""
@spec supporting_method(String.t()) :: {:ok, [Model.t()]} | {:error, Error.t()}
def supporting_method(method) when is_binary(method) and method != "" do
case list() do
{:ok, %ListModelsResponse{models: models}} ->
supporting_models =
models
|> Enum.filter(&Model.supports_method?(&1, method))
{:ok, supporting_models}
{:error, error} ->
{:error, error}
end
end
def supporting_method("") do
{:error, Error.validation_error("Generation method cannot be empty")}
end
def supporting_method(_) do
{:error, Error.validation_error("Generation method must be a string")}
end
@doc """
Get models filtered by capabilities or parameters.
## Parameters
- `filter_opts` - Keyword list of filter criteria:
- `:min_input_tokens` - Minimum input token limit
- `:min_output_tokens` - Minimum output token limit
- `:supports_methods` - List of required methods
- `:has_temperature` - Boolean, requires temperature parameter
- `:has_top_k` - Boolean, requires top_k parameter
- `:has_top_p` - Boolean, requires top_p parameter
- `:production_ready` - Boolean, filter for production-ready models
- `:model_family` - String, filter by model family (e.g., "gemini")
## Returns
- `{:ok, [Model.t()]}` - Filtered list of models
- `{:error, Error.t()}` - API error
## Examples
# High-capacity models
{:ok, large_models} = Models.filter(min_input_tokens: 100_000)
# Models with advanced parameters
{:ok, tunable_models} = Models.filter(has_temperature: true, has_top_k: true)
# Multi-method support
{:ok, versatile_models} = Models.filter(
supports_methods: ["generateContent", "streamGenerateContent"]
)
# Production-ready models only
{:ok, production_models} = Models.filter(production_ready: true)
"""
@spec filter(keyword()) :: {:ok, [Model.t()]} | {:error, Error.t()}
def filter(filter_opts \\ []) do
case list() do
{:ok, %ListModelsResponse{models: models}} ->
filtered_models =
models
|> Enum.filter(&model_matches_filter?(&1, filter_opts))
{:ok, filtered_models}
{:error, error} ->
{:error, error}
end
end
@doc """
Get comprehensive model statistics and summary.
## Returns
- `{:ok, map()}` - Statistics about available models
- `{:error, Error.t()}` - API error
## Example Response
{:ok, stats} = Models.get_stats()
# =>
# %{
# total_models: 5,
# by_version: %{"1.5" => 3, "2.0" => 2},
# by_method: %{
# "generateContent" => 5,
# "streamGenerateContent" => 4,
# "countTokens" => 5
# },
# token_limits: %{
# max_input: 2_000_000,
# max_output: 8192,
# avg_input: 800_000,
# avg_output: 4096
# },
# capabilities: %{
# with_temperature: 5,
# with_top_k: 3,
# with_top_p: 5,
# production_ready: 4
# },
# by_family: %{"gemini" => 4, "text" => 1}
# }
"""
@spec get_stats() :: {:ok, map()} | {:error, Error.t()}
def get_stats do
case list() do
{:ok, %ListModelsResponse{models: models}} ->
stats = %{
total_models: length(models),
by_version: count_by_version(models),
by_method: count_by_methods(models),
by_family: count_by_family(models),
token_limits: calculate_token_stats(models),
capabilities: count_capabilities(models),
capacity_distribution: analyze_capacity_distribution(models),
models_by_name: Enum.map(models, &{Model.effective_base_id(&1), &1.display_name})
}
{:ok, stats}
{:error, error} ->
{:error, error}
end
end
# Private implementation functions
@spec build_telemetry_opts(atom(), keyword()) :: keyword()
defp build_telemetry_opts(function, opts) do
telemetry_metadata = %{
function: function,
api: :models
}
# Add function-specific metadata
telemetry_metadata =
case function do
:get_model ->
Map.put(telemetry_metadata, :model, Keyword.get(opts, :model, "unknown"))
_ ->
telemetry_metadata
end
[telemetry_metadata: telemetry_metadata]
end
@spec emit_models_telemetry(atom(), atom(), integer(), map()) :: :ok
defp emit_models_telemetry(operation, status, start_time, additional_metadata) do
duration = Telemetry.calculate_duration(start_time)
measurements = %{duration: duration}
measurements = Map.merge(measurements, Map.take(additional_metadata, [:model_count]))
metadata = %{
api: :models,
operation: operation
}
metadata = Map.merge(metadata, Map.drop(additional_metadata, [:model_count]))
Telemetry.execute([:gemini, :models, operation, status], measurements, metadata)
end
@spec parse_list_models_response(map()) :: {:ok, ListModelsResponse.t()} | {:error, Error.t()}
defp parse_list_models_response(response) do
try do
models =
response
|> Map.get("models", [])
|> Enum.map(&parse_model_data/1)
list_response = %ListModelsResponse{
models: models,
next_page_token: Map.get(response, "nextPageToken")
}
{:ok, list_response}
rescue
error ->
Logger.error("Failed to parse models list response: #{inspect(error)}")
{:error, Error.invalid_response("Failed to parse models response: #{inspect(error)}")}
end
end
@spec parse_model_response(map()) :: {:ok, Model.t()} | {:error, Error.t()}
defp parse_model_response(response) do
try do
model = parse_model_data(response)
{:ok, model}
rescue
error ->
Logger.error("Failed to parse model response: #{inspect(error)}")
{:error, Error.invalid_response("Failed to parse model response: #{inspect(error)}")}
end
end
@spec parse_model_data(map()) :: Model.t()
defp parse_model_data(model_data) do
%Model{
name: Map.get(model_data, "name", ""),
base_model_id: Map.get(model_data, "baseModelId"),
version: Map.get(model_data, "version", ""),
display_name: Map.get(model_data, "displayName", ""),
description: Map.get(model_data, "description", ""),
input_token_limit: Map.get(model_data, "inputTokenLimit", 0),
output_token_limit: Map.get(model_data, "outputTokenLimit", 0),
supported_generation_methods: Map.get(model_data, "supportedGenerationMethods", []),
temperature: Map.get(model_data, "temperature"),
max_temperature: Map.get(model_data, "maxTemperature"),
top_p: Map.get(model_data, "topP"),
top_k: Map.get(model_data, "topK")
}
end
@spec extract_base_model_id(String.t()) :: String.t()
defp extract_base_model_id("models/" <> base_id), do: base_id
defp extract_base_model_id(name), do: name
@spec extract_base_model_id_safe(String.t()) :: String.t()
defp extract_base_model_id_safe(model_name) when is_binary(model_name) do
extract_base_model_id(model_name)
end
defp extract_base_model_id_safe(_), do: "unknown"
@spec model_matches_filter?(Model.t(), keyword()) :: boolean()
defp model_matches_filter?(model, filter_opts) do
Enum.all?(filter_opts, fn {key, value} ->
case key do
:min_input_tokens ->
model.input_token_limit >= value
:min_output_tokens ->
model.output_token_limit >= value
:supports_methods when is_list(value) ->
Enum.all?(value, &Model.supports_method?(model, &1))
:has_temperature ->
(value and not is_nil(model.temperature)) or not value
:has_top_k ->
(value and not is_nil(model.top_k)) or not value
:has_top_p ->
(value and not is_nil(model.top_p)) or not value
:production_ready ->
(value and Model.production_ready?(model)) or not value
:model_family ->
Model.model_family(model) == value
:latest_version ->
(value and Model.latest_version?(model)) or not value
_ ->
true
end
end)
end
@spec count_by_version([Model.t()]) :: map()
defp count_by_version(models) do
models
|> Enum.group_by(& &1.version)
|> Enum.map(fn {version, models_list} -> {version, length(models_list)} end)
|> Enum.into(%{})
end
@spec count_by_methods([Model.t()]) :: map()
defp count_by_methods(models) do
models
|> Enum.flat_map(& &1.supported_generation_methods)
|> Enum.frequencies()
end
@spec count_by_family([Model.t()]) :: map()
defp count_by_family(models) do
models
|> Enum.group_by(&Model.model_family/1)
|> Enum.map(fn {family, models_list} -> {family, length(models_list)} end)
|> Enum.into(%{})
end
@spec calculate_token_stats([Model.t()]) :: map()
defp calculate_token_stats([]), do: %{max_input: 0, max_output: 0, avg_input: 0, avg_output: 0}
defp calculate_token_stats(models) do
input_limits = Enum.map(models, & &1.input_token_limit)
output_limits = Enum.map(models, & &1.output_token_limit)
%{
max_input: Enum.max(input_limits),
max_output: Enum.max(output_limits),
min_input: Enum.min(input_limits),
min_output: Enum.min(output_limits),
avg_input: round(Enum.sum(input_limits) / length(input_limits)),
avg_output: round(Enum.sum(output_limits) / length(output_limits))
}
end
@spec count_capabilities([Model.t()]) :: map()
defp count_capabilities(models) do
%{
with_temperature: Enum.count(models, &(not is_nil(&1.temperature))),
with_top_k: Enum.count(models, &(not is_nil(&1.top_k))),
with_top_p: Enum.count(models, &(not is_nil(&1.top_p))),
with_max_temperature: Enum.count(models, &(not is_nil(&1.max_temperature))),
supports_streaming: Enum.count(models, &Model.supports_streaming?/1),
supports_token_counting: Enum.count(models, &Model.supports_token_counting?/1),
supports_embeddings: Enum.count(models, &Model.supports_embeddings?/1),
production_ready: Enum.count(models, &Model.production_ready?/1),
latest_versions: Enum.count(models, &Model.latest_version?/1)
}
end
@spec analyze_capacity_distribution([Model.t()]) :: map()
defp analyze_capacity_distribution(models) do
input_distribution =
models
|> Enum.group_by(&Model.input_capacity_tier/1)
|> Enum.map(fn {tier, models_list} -> {tier, length(models_list)} end)
|> Enum.into(%{})
output_distribution =
models
|> Enum.group_by(&Model.output_capacity_tier/1)
|> Enum.map(fn {tier, models_list} -> {tier, length(models_list)} end)
|> Enum.into(%{})
%{
input_capacity: input_distribution,
output_capacity: output_distribution
}
end
end