LemonAi.Providers.GoogleShared (lemon_ai v0.1.0)

View Source

Shared utilities for Google Generative AI, Vertex AI, and Gemini CLI providers.

This module handles:

  • Content conversion to Gemini format
  • Tool declaration formatting
  • Stop reason mapping
  • Thought signature validation
  • Unicode sanitization

Summary

Functions

Calculate cost for usage based on model pricing.

Clamp thinking level (remove :xhigh).

Convert internal messages to Gemini Content[] format.

Convert tools to Gemini function declarations format.

Get default thinking budgets for Gemini 2.5 Flash models.

Get default thinking budgets for Gemini 2.5 Pro models.

Extract a clean, user-friendly error message from Google API error response.

Extract retry delay from error response (in milliseconds). Parses patterns like

Check if model is Gemini 3 Flash.

Check if model is Gemini 3 Pro.

Get thinking level for Gemini 3 models based on effort.

Get thinking budget for a model and effort level.

Map Gemini FinishReason string to our stop_reason.

Map tool choice to Gemini FunctionCallingConfigMode string.

Normalize error response bodies from Req into a plain string.

Check if a model requires explicit tool call IDs in function calls/responses. Claude and GPT-OSS models via Google APIs require explicit IDs.

Only keep signatures from the same provider/model and with valid base64.

Retain thought signatures during streaming.

Check if an error is retryable (rate limit, server error, network error, etc.)

Sanitize unpaired surrogates from a string. These can cause issues with some APIs.

Determines whether a streamed Gemini Part should be treated as "thinking".

Check if a thought signature is valid base64.

Types

content()

@type content() :: %{role: String.t(), parts: [part()]}

function_declaration()

@type function_declaration() :: %{
  name: String.t(),
  description: String.t() | nil,
  parameters: map()
}

google_api_type()

@type google_api_type() :: :google_generative_ai | :google_vertex | :google_gemini_cli

part()

@type part() :: %{
  optional(:text) => String.t(),
  optional(:thought) => boolean(),
  optional(:thought_signature) => String.t(),
  optional(:inline_data) => %{mime_type: String.t(), data: String.t()},
  optional(:function_call) => %{
    :name => String.t(),
    :args => map(),
    optional(:id) => String.t()
  },
  optional(:function_response) => %{
    :name => String.t(),
    :response => map(),
    optional(:id) => String.t(),
    optional(:parts) => [part()]
  }
}

stop_reason()

@type stop_reason() :: :stop | :length | :tool_use | :error | :aborted

thinking_level()

@type thinking_level() :: :minimal | :low | :medium | :high

tool_choice()

@type tool_choice() :: :auto | :none | :any

Functions

calculate_cost(model, usage)

@spec calculate_cost(LemonAi.Types.Model.t(), map()) :: map()

Calculate cost for usage based on model pricing.

clamp_reasoning(level)

@spec clamp_reasoning(atom() | nil) :: thinking_level() | nil

Clamp thinking level (remove :xhigh).

convert_messages(model, context)

@spec convert_messages(LemonAi.Types.Model.t(), LemonAi.Types.Context.t()) :: [
  content()
]

Convert internal messages to Gemini Content[] format.

convert_tools(tools)

@spec convert_tools([LemonAi.Types.Tool.t()]) ::
  [%{functionDeclarations: [function_declaration()]}] | nil

Convert tools to Gemini function declarations format.

default_budgets_2_5_flash()

@spec default_budgets_2_5_flash() :: %{
  required(thinking_level()) => non_neg_integer()
}

Get default thinking budgets for Gemini 2.5 Flash models.

default_budgets_2_5_pro()

@spec default_budgets_2_5_pro() :: %{required(thinking_level()) => non_neg_integer()}

Get default thinking budgets for Gemini 2.5 Pro models.

extract_error_message(error_text)

@spec extract_error_message(String.t()) :: String.t()

Extract a clean, user-friendly error message from Google API error response.

extract_retry_delay(error_text, headers \\ %{})

@spec extract_retry_delay(String.t(), map()) :: non_neg_integer() | nil

Extract retry delay from error response (in milliseconds). Parses patterns like:

  • "Your quota will reset after 39s"
  • "Your quota will reset after 18h31m10s"
  • "Please retry in Xs" or "Please retry in Xms"
  • "retryDelay": "34.074824224s" (JSON field)

gemini_3_flash?(model_id)

@spec gemini_3_flash?(String.t()) :: boolean()

Check if model is Gemini 3 Flash.

gemini_3_pro?(model_id)

@spec gemini_3_pro?(String.t()) :: boolean()

Check if model is Gemini 3 Pro.

get_gemini_3_thinking_level(effort, model_id)

@spec get_gemini_3_thinking_level(thinking_level(), String.t()) :: String.t()

Get thinking level for Gemini 3 models based on effort.

get_thinking_budget(model, effort, custom_budgets)

@spec get_thinking_budget(LemonAi.Types.Model.t(), thinking_level(), map()) ::
  integer()

Get thinking budget for a model and effort level.

map_stop_reason(arg1)

@spec map_stop_reason(String.t()) :: stop_reason()

Map Gemini FinishReason string to our stop_reason.

map_tool_choice(arg1)

@spec map_tool_choice(tool_choice()) :: String.t()

Map tool choice to Gemini FunctionCallingConfigMode string.

normalize_http_error_body(body)

@spec normalize_http_error_body(term()) :: String.t()

Normalize error response bodies from Req into a plain string.

Handles plain binaries, maps, and Req.Response.Async bodies produced by streaming requests (into: :self) so provider logs include the real upstream error JSON instead of an async struct dump.

normalize_http_error_body(body, timeout_ms)

@spec normalize_http_error_body(term(), non_neg_integer()) :: String.t()

requires_tool_call_id?(model_id)

@spec requires_tool_call_id?(String.t()) :: boolean()

Check if a model requires explicit tool call IDs in function calls/responses. Claude and GPT-OSS models via Google APIs require explicit IDs.

resolve_thought_signature(same_provider_and_model, signature)

@spec resolve_thought_signature(boolean(), String.t() | nil) :: String.t() | nil

Only keep signatures from the same provider/model and with valid base64.

retain_thought_signature(existing, incoming)

@spec retain_thought_signature(String.t() | nil, String.t() | nil) :: String.t() | nil

Retain thought signatures during streaming.

Some backends only send thoughtSignature on the first delta for a given part/block; later deltas may omit it. This helper preserves the last non-empty signature.

retryable_error?(status, error_text)

@spec retryable_error?(non_neg_integer(), String.t()) :: boolean()

Check if an error is retryable (rate limit, server error, network error, etc.)

sanitize_surrogates(text)

@spec sanitize_surrogates(String.t()) :: String.t()

Sanitize unpaired surrogates from a string. These can cause issues with some APIs.

thinking_part?(arg1)

@spec thinking_part?(map()) :: boolean()

Determines whether a streamed Gemini Part should be treated as "thinking".

Protocol note (Gemini / Vertex AI thought signatures):

  • thought: true is the definitive marker for thinking content (thought summaries).
  • thoughtSignature is an encrypted representation of the model's internal thought process used to preserve reasoning context across multi-turn interactions.
  • thoughtSignature can appear on ANY part type (text, functionCall, etc.) - it does NOT indicate the part itself is thinking content.

valid_thought_signature?(signature)

@spec valid_thought_signature?(String.t() | nil) :: boolean()

Check if a thought signature is valid base64.