Packages
langchain
0.8.6
0.9.2
0.9.1
0.9.0
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.4.0-rc.3
0.4.0-rc.2
0.4.0-rc.1
0.4.0-rc.0
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-rc.2
0.3.0-rc.1
0.3.0-rc.0
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir implementation of a LangChain style framework that lets Elixir projects integrate with and leverage LLMs.
Current section
Files
Jump to
Current section
Files
lib/chat_models/chat_vertex_ai.ex
defmodule LangChain.ChatModels.ChatVertexAI do
@moduledoc """
Parses and validates inputs for making a request for the Google AI Chat API.
Converts response into more specialized `LangChain` data structures.
Example Usage:
```elixir
alias LangChain.Chains.LLMChain
alias LangChain.Message
alias LangChain.Message.ContentPart
alias LangChain.ChatModels.ChatVertexAI
config = %{
model: "gemini-2.0-flash",
api_key: ..., # vertex requires gcloud auth token https://cloud.google.com/vertex-ai/generative-ai/docs/start/quickstarts/quickstart-multimodal#rest
temperature: 1.0,
top_p: 0.8,
receive_timeout: ...
}
model = ChatVertexAI.new!(config)
%{llm: model, verbose: false, stream: false}
|> LLMChain.new!()
|> LLMChain.add_message(
Message.new_user!([
ContentPart.new!(%{type: :text, content: "Analyse the provided file and share a summary"}),
ContentPart.new!(%{
type: :file_url,
content: ...,
options: [media: ...]
})
])
)
|> LLMChain.run()
The above call will return summary of the media content.
```
"""
use Ecto.Schema
require Logger
import Ecto.Changeset
alias __MODULE__
alias LangChain.Config
alias LangChain.ChatModels.ChatModel
alias LangChain.ChatModels.ChatOpenAI
alias LangChain.Message
alias LangChain.MessageDelta
alias LangChain.Message.ContentPart
alias LangChain.Message.ToolCall
alias LangChain.Message.ToolResult
alias LangChain.LangChainError
alias LangChain.Utils
alias LangChain.Callbacks
alias LangChain.TokenUsage
alias LangChain.NativeTool
alias LangChain.Function
@behaviour ChatModel
@current_config_version 1
# allow up to 2 minutes for response.
@receive_timeout 60_000
@primary_key false
embedded_schema do
field :endpoint, :string
field :model, :string, default: "gemini-pro"
field :api_key, :string, redact: true
# What sampling temperature to use, between 0 and 2. Higher values like 0.8
# will make the output more random, while lower values like 0.2 will make it
# more focused and deterministic.
field :temperature, :float, default: 0.9
# The topP parameter changes how the model selects tokens for output. Tokens
# are selected from the most to least probable until the sum of their
# probabilities equals the topP value. For example, if tokens A, B, and C have
# a probability of 0.3, 0.2, and 0.1 and the topP value is 0.5, then the model
# will select either A or B as the next token by using the temperature and exclude
# C as a candidate. The default topP value is 0.95.
field :top_p, :float, default: 1.0
# The topK parameter changes how the model selects tokens for output. A topK of
# 1 means the selected token is the most probable among all the tokens in the
# model's vocabulary (also called greedy decoding), while a topK of 3 means that
# the next token is selected from among the 3 most probable using the temperature.
# For each token selection step, the topK tokens with the highest probabilities
# are sampled. Tokens are then further filtered based on topP with the final token
# selected using temperature sampling.
field :top_k, :float, default: 1.0
# Configure thinking budget and whether to include thought summaries (content type `:thinking`).
# See https://docs.cloud.google.com/vertex-ai/generative-ai/docs/thinking
#
# Config reference: https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/projects.locations.evaluationRuns#ThinkingConfig
field :thinking_config, :map, default: nil
# Duration in seconds for the response to be received. When streaming a very
# lengthy response, a longer time limit may be required. However, when it
# goes on too long by itself, it tends to hallucinate more.
field :receive_timeout, :integer, default: @receive_timeout
field :json_response, :boolean, default: false
field :json_schema, :map, default: nil
field :stream, :boolean, default: false
# A list of maps for callback handlers (treated as internal)
field :callbacks, {:array, :map}, default: []
# Additional level of raw api request and response data
field :verbose_api, :boolean, default: false
# Req options to merge into the request.
# Refer to `https://hexdocs.pm/req/Req.html#new/1-options` for
# `Req.new` supported set of options.
field :req_config, :map, default: %{}
end
@type t :: %ChatVertexAI{}
@create_fields [
:endpoint,
:model,
:api_key,
:temperature,
:top_p,
:top_k,
:thinking_config,
:receive_timeout,
:json_response,
:json_schema,
:stream,
:req_config
]
@required_fields [
:endpoint,
:model
]
@spec get_api_key(t) :: String.t()
defp get_api_key(%ChatVertexAI{api_key: api_key}) do
# if no API key is set default to `""` which will raise an API error
api_key || Config.resolve(:vertex_ai_key, "")
end
@doc """
Setup a ChatVertexAI client configuration.
"""
@spec new(attrs :: map()) :: {:ok, t} | {:error, Ecto.Changeset.t()}
def new(%{} = attrs \\ %{}) do
%ChatVertexAI{}
|> cast(attrs, @create_fields)
|> common_validation()
|> apply_action(:insert)
end
@doc """
Setup a ChatVertexAI client configuration and return it or raise an error if invalid.
"""
@spec new!(attrs :: map()) :: t() | no_return()
def new!(attrs \\ %{}) do
case new(attrs) do
{:ok, chain} ->
chain
{:error, changeset} ->
raise LangChainError, changeset
end
end
defp common_validation(changeset) do
changeset
|> validate_required(@required_fields)
end
def for_api(%ChatVertexAI{} = vertex_ai, messages, functions) do
{sys_instructions, other_messages} = Utils.split_system_message(messages)
messages_for_api =
other_messages
|> Enum.map(&for_api/1)
|> List.flatten()
|> List.wrap()
{response_mime_type, response_schema} =
case vertex_ai.json_response do
true ->
{"application/json", vertex_ai.json_schema}
false ->
{nil, nil}
end
generation_config_params =
%{
"temperature" => vertex_ai.temperature,
"topP" => vertex_ai.top_p,
"topK" => vertex_ai.top_k
}
|> Utils.conditionally_add_to_map("thinkingConfig", vertex_ai.thinking_config)
|> Utils.conditionally_add_to_map("response_mime_type", response_mime_type)
|> Utils.conditionally_add_to_map("response_schema", response_schema)
req =
%{
"contents" => messages_for_api,
"generationConfig" => generation_config_params
}
|> Utils.conditionally_add_to_map("system_instruction", for_api(sys_instructions))
if functions && not Enum.empty?(functions) do
req
|> Map.put("tools", [
%{
# Google AI functions use an OpenAI compatible format.
# See: https://ai.google.dev/docs/function_calling#how_it_works
# Note: We strip the "strict" field as it's OpenAI-specific and not supported by Vertex AI
"functionDeclarations" =>
functions
|> Enum.map(&ChatOpenAI.for_api(vertex_ai, &1))
|> Enum.map(&Map.delete(&1, "strict"))
}
])
else
req
end
end
defp for_api(%Message{role: :assistant} = message) do
content_parts = get_message_contents(message) || []
tool_calls = Enum.map(message.tool_calls || [], &for_api/1)
%{
"role" => map_role(:assistant),
"parts" => content_parts ++ tool_calls
}
end
defp for_api(%Message{role: :tool} = message) do
%{
"role" => map_role(:tool),
"parts" => Enum.map(message.tool_results, &for_api/1)
}
end
defp for_api(%Message{role: :system} = message) do
# System messages should return a single text part, not a list
case get_message_contents(message) do
[%{"text" => text}] -> %{"parts" => %{"text" => text}}
_ -> %{"parts" => %{"text" => message.content}}
end
end
defp for_api(%Message{role: :user, content: content}) when is_list(content) do
%{
"role" => map_role(:user),
"parts" => Enum.map(content, &for_api(&1))
}
end
defp for_api(%Message{} = message) do
content_parts = get_message_contents(message) || []
%{
"role" => map_role(message.role),
"parts" => content_parts
}
end
defp for_api(%ContentPart{type: :text} = part) do
%{"text" => part.content}
end
defp for_api(%ContentPart{type: :thinking}) do
# The thinking parts are only thought summaries and are not meant to be
# included in future generation requests.
# See https://docs.cloud.google.com/vertex-ai/generative-ai/docs/thinking#thought-summaries
[]
end
defp for_api(%ContentPart{type: :image} = part) do
%{
"inlineData" => %{
"mimeType" => Keyword.fetch!(part.options, :media),
"data" => part.content
}
}
end
defp for_api(%ContentPart{type: :image_url} = part) do
%{
"fileData" => %{
"mimeType" => Keyword.fetch!(part.options, :media),
"fileUri" => part.content
}
}
end
defp for_api(%ContentPart{type: :file_url} = part) do
%{
"fileData" => %{
"mimeType" => Keyword.fetch!(part.options, :media),
"fileUri" => part.content
}
}
end
defp for_api(%ToolCall{metadata: %{thought_signature: signature}} = call)
when is_binary(signature) do
%{
"functionCall" => %{
"args" => call.arguments,
"name" => call.name
},
"thoughtSignature" => signature
}
end
defp for_api(%ToolCall{} = call) do
%{
"functionCall" => %{
"args" => call.arguments,
"name" => call.name
}
}
end
defp for_api(%ToolResult{} = result) do
response = tool_result_response_for_api(result.content)
response_parts = tool_result_parts_for_api(result.content)
%{
"functionResponse" => %{
"name" => result.name,
"response" => response
}
}
|> maybe_add_function_response_parts(response_parts)
end
defp for_api(%NativeTool{name: name, configuration: %{} = config}) do
%{name => config}
end
defp for_api(%NativeTool{name: name, configuration: nil}) do
name
end
defp for_api(nil), do: nil
defp maybe_add_function_response_parts(
%{"functionResponse" => function_response} = data,
[_ | _] = response_parts
) do
put_in(data, ["functionResponse"], Map.put(function_response, "parts", response_parts))
end
defp maybe_add_function_response_parts(data, _response_parts), do: data
defp tool_result_response_for_api(nil), do: %{}
defp tool_result_response_for_api(content) when is_binary(content) do
case Jason.decode(content) do
{:ok, data} ->
data
{:error, %Jason.DecodeError{}} ->
%{"result" => content}
end
end
defp tool_result_response_for_api(content_parts) when is_list(content_parts) do
text_parts = Enum.filter(content_parts, &(&1.type == :text))
case ContentPart.parts_to_string(text_parts) do
nil ->
%{}
text_content ->
case Jason.decode(text_content) do
{:ok, data} ->
data
{:error, %Jason.DecodeError{}} ->
%{"result" => text_content}
end
end
end
defp tool_result_parts_for_api(nil), do: []
defp tool_result_parts_for_api(content) when is_binary(content), do: []
defp tool_result_parts_for_api(content_parts) when is_list(content_parts) do
content_parts
|> Enum.filter(&tool_result_media_part?/1)
|> Enum.map(&tool_result_media_part_for_api/1)
end
defp tool_result_media_part_for_api(%ContentPart{type: :image} = part) do
%{
"inlineData" =>
%{
"mimeType" => Keyword.fetch!(part.options, :media),
"data" => part.content
}
|> maybe_add_display_name(part.options)
}
end
defp tool_result_media_part_for_api(%ContentPart{type: :file} = part) do
%{
"inlineData" =>
%{
"mimeType" => Keyword.fetch!(part.options, :media),
"data" => part.content
}
|> maybe_add_display_name(part.options)
}
end
defp tool_result_media_part_for_api(%ContentPart{type: :image_url} = part) do
%{
"fileData" =>
%{
"mimeType" => Keyword.fetch!(part.options, :media),
"fileUri" => part.content
}
|> maybe_add_display_name(part.options)
}
end
defp tool_result_media_part_for_api(%ContentPart{type: :file_url} = part) do
%{
"fileData" =>
%{
"mimeType" => Keyword.fetch!(part.options, :media),
"fileUri" => part.content
}
|> maybe_add_display_name(part.options)
}
end
defp maybe_add_display_name(data, options) do
case Keyword.get(options || [], :display_name) do
nil -> data
display_name -> Map.put(data, "displayName", display_name)
end
end
defp tool_result_media_part?(%ContentPart{type: type})
when type in [:image, :file, :image_url, :file_url],
do: true
defp tool_result_media_part?(%ContentPart{}), do: false
@doc """
Calls the Google AI API passing the ChatVertexAI struct with configuration,
plus either a simple message or the list of messages to act as the prompt.
Optionally pass in a list of tools available to the LLM for requesting
execution in response.
**NOTE:** This function *can* be used directly, but the primary interface
should be through `LangChain.Chains.LLMChain`. The `ChatVertexAI` module is
more focused on translating the `LangChain` data structures to and from the
OpenAI API.
Another benefit of using `LangChain.Chains.LLMChain` is that it combines the
storage of messages, adding tools, adding custom context that should be passed
to tools, and automatically applying `LangChain.MessageDelta` structs as they
are are received, then converting those to the full `LangChain.Message` once
fully complete.
"""
@impl ChatModel
def call(openai, prompt, tools \\ [])
def call(%ChatVertexAI{} = vertex_ai, prompt, tools) when is_binary(prompt) do
messages = [
Message.new_system!(),
Message.new_user!(prompt)
]
call(vertex_ai, messages, tools)
end
def call(%ChatVertexAI{} = vertex_ai, messages, tools)
when is_list(messages) do
metadata = %{
model: vertex_ai.model,
message_count: length(messages),
tools_count: length(tools)
}
LangChain.Telemetry.span([:langchain, :llm, :call], metadata, fn ->
try do
# Track the prompt being sent
LangChain.Telemetry.llm_prompt(
%{system_time: System.system_time()},
%{model: vertex_ai.model, messages: messages}
)
case do_api_request(vertex_ai, messages, tools) do
{:error, reason} ->
{:error, reason}
parsed_data ->
# Track the response being received
LangChain.Telemetry.llm_response(
%{system_time: System.system_time()},
%{model: vertex_ai.model, response: parsed_data}
)
{:ok, parsed_data}
end
rescue
err in LangChainError ->
{:error, err}
end
end)
end
@doc false
@spec do_api_request(t(), [Message.t()], [Function.t()]) ::
list() | struct() | {:error, LangChainError.t()}
def do_api_request(%ChatVertexAI{stream: false} = vertex_ai, messages, tools) do
req =
Req.new(
url: build_url(vertex_ai),
json: for_api(vertex_ai, messages, tools),
receive_timeout: vertex_ai.receive_timeout,
# Disable Req-level retry to prevent compounding with LangChain's own
# :closed retry. See https://github.com/brainlid/langchain/issues/503
retry: false,
auth: {:bearer, get_api_key(vertex_ai)}
)
|> Req.merge(vertex_ai.req_config |> Keyword.new())
req
|> Req.post()
|> case do
{:ok, %Req.Response{body: data} = response} ->
Callbacks.fire(vertex_ai.callbacks, :on_llm_response_headers, [response.headers])
case do_process_response(vertex_ai, data) do
{:error, reason} ->
{:error, reason}
result ->
Callbacks.fire(vertex_ai.callbacks, :on_llm_new_message, [result])
# Track non-streaming response completion
LangChain.Telemetry.emit_event(
[:langchain, :llm, :response, :non_streaming],
%{system_time: System.system_time()},
%{
model: vertex_ai.model,
response_size: byte_size(inspect(result))
}
)
result
end
{:error, %Req.TransportError{reason: :timeout} = err} ->
{:error,
LangChainError.exception(type: "timeout", message: "Request timed out", original: err)}
other ->
Logger.warning(fn -> "Unexpected and unhandled API response! #{inspect(other)}" end)
other
end
end
def do_api_request(%ChatVertexAI{stream: true} = vertex_ai, messages, tools) do
Req.new(
url: build_url(vertex_ai),
json: for_api(vertex_ai, messages, tools),
auth: {:bearer, get_api_key(vertex_ai)},
receive_timeout: vertex_ai.receive_timeout
)
|> Req.Request.put_header("accept-encoding", "utf-8")
|> Req.merge(vertex_ai.req_config |> Keyword.new())
|> Req.post(
into:
Utils.handle_stream_fn(
vertex_ai,
&ChatOpenAI.decode_stream/1,
&do_process_response(vertex_ai, &1, MessageDelta)
)
)
|> case do
{:ok, %Req.Response{body: data} = response} ->
Callbacks.fire(vertex_ai.callbacks, :on_llm_response_headers, [response.headers])
# Google AI uses `finishReason: "STOP` for all messages in the stream.
# This field can't be used to terminate the list of deltas, so simulate
# this behavior by forcing the final delta to have `status: :complete`.
complete_final_delta(data)
{:error, %LangChainError{} = error} ->
{:error, error}
{:error, %Req.TransportError{reason: :timeout} = err} ->
{:error,
LangChainError.exception(type: "timeout", message: "Request timed out", original: err)}
other ->
Logger.warning(fn ->
"Unhandled and unexpected response from streamed post call. #{inspect(other)}"
end)
{:error,
LangChainError.exception(
type: "unexpected_response",
message: "Unexpected response",
original: other
)}
end
end
@spec build_url(t()) :: String.t()
defp build_url(%ChatVertexAI{endpoint: endpoint, model: model} = vertex_ai) do
"#{endpoint}/models/#{model}:#{get_action(vertex_ai)}?key=#{get_api_key(vertex_ai)}"
|> use_sse(vertex_ai)
end
@spec use_sse(String.t(), t()) :: String.t()
defp use_sse(url, %ChatVertexAI{stream: true}), do: url <> "&alt=sse"
defp use_sse(url, _model), do: url
@spec get_action(t()) :: String.t()
defp get_action(%ChatVertexAI{stream: false}), do: "generateContent"
defp get_action(%ChatVertexAI{stream: true}), do: "streamGenerateContent"
def complete_final_delta(data) when is_list(data) do
update_in(data, [Access.at(-1), Access.at(-1)], &%{&1 | status: :complete})
end
def do_process_response(model, response, message_type \\ Message)
def do_process_response(model, %{"candidates" => candidates} = data, message_type)
when is_list(candidates) do
token_usage = get_token_usage(data)
case token_usage do
%TokenUsage{} = usage ->
Callbacks.fire(model.callbacks, :on_llm_token_usage, [usage])
:ok
nil ->
:ok
end
candidates
|> Enum.map(&do_process_response(model, &1, message_type))
|> Enum.map(&TokenUsage.set(&1, token_usage))
end
def do_process_response(
model,
%{"content" => %{"parts" => parts} = content_data} = data,
Message
) do
text_part =
parts
|> filter_parts_for_types(["text"])
|> filter_text_parts()
|> Enum.map(fn part ->
type =
case part["thought"] do
true -> :thinking
_ -> :text
end
ContentPart.new!(%{type: type, content: part["text"]})
end)
tool_calls_from_parts =
parts
|> filter_parts_for_types(["functionCall"])
|> Enum.map(fn part ->
do_process_response(model, part, nil)
end)
tool_result_from_parts =
parts
|> filter_parts_for_types(["functionResponse"])
|> Enum.map(fn part ->
do_process_response(model, part, nil)
end)
%{
role: unmap_role(content_data["role"]),
content: text_part,
complete: false,
index: data["index"]
}
|> Utils.conditionally_add_to_map(:tool_calls, tool_calls_from_parts)
|> Utils.conditionally_add_to_map(:tool_results, tool_result_from_parts)
|> Message.new()
|> case do
{:ok, message} ->
message
{:error, %Ecto.Changeset{} = changeset} ->
{:error, LangChainError.exception(changeset)}
end
end
def do_process_response(
model,
%{"content" => %{"parts" => parts} = content_data} = data,
MessageDelta
) do
text_content =
case parts do
[%{"text" => text}] ->
text
_other ->
nil
end
parts
|> filter_parts_for_types(["text"])
|> Enum.map(fn part ->
ContentPart.new!(%{type: :text, content: part["text"]})
end)
tool_calls_from_parts =
parts
|> filter_parts_for_types(["functionCall"])
|> Enum.map(fn part ->
do_process_response(model, part, nil)
end)
%{
role: unmap_role(content_data["role"]),
content: text_content,
complete: true,
index: data["index"]
}
|> Utils.conditionally_add_to_map(:tool_calls, tool_calls_from_parts)
|> MessageDelta.new()
|> case do
{:ok, message} ->
message
{:error, %Ecto.Changeset{} = changeset} ->
{:error, LangChainError.exception(changeset)}
end
end
def do_process_response(
_model,
%{"functionCall" => %{"args" => raw_args, "name" => name}} = data,
_
) do
%{
call_id: "call-#{name}",
name: name,
arguments: raw_args,
complete: true,
index: data["index"],
metadata:
if(data["thoughtSignature"],
do: %{thought_signature: data["thoughtSignature"]},
else: nil
)
}
|> ToolCall.new()
|> case do
{:ok, message} ->
message
{:error, %Ecto.Changeset{} = changeset} ->
{:error, LangChainError.exception(changeset)}
end
end
def do_process_response(
_model,
%{
"finishReason" => finish,
"content" => %{"parts" => parts, "role" => role},
"index" => index
},
message_type
)
when is_list(parts) do
status =
case message_type do
MessageDelta ->
:incomplete
Message ->
case finish do
"STOP" ->
:complete
"SAFETY" ->
:complete
other ->
Logger.warning("Unsupported finishReason in response. Reason: #{inspect(other)}")
nil
end
end
content = Enum.map_join(parts, & &1["text"])
case message_type.new(%{
"content" => content,
"role" => unmap_role(role),
"status" => status,
"index" => index
}) do
{:ok, message} ->
message
{:error, %Ecto.Changeset{} = changeset} ->
{:error, LangChainError.exception(changeset)}
end
end
def do_process_response(_model, %{"error" => %{"message" => reason}} = response, _) do
{:error, LangChainError.exception(message: reason, original: response)}
end
def do_process_response(_model, {:error, %Jason.DecodeError{} = response}, _) do
error_message = "Received invalid JSON: #{inspect(response)}"
{:error,
LangChainError.exception(type: "invalid_json", message: error_message, original: response)}
end
def do_process_response(_model, other, _) do
{:error,
LangChainError.exception(
type: "unexpected_response",
message: "Unexpected response",
original: other
)}
end
@doc false
def filter_parts_for_types(parts, types) when is_list(parts) and is_list(types) do
Enum.filter(parts, fn p ->
Enum.any?(types, &Map.has_key?(p, &1))
end)
end
@doc false
def filter_text_parts(parts) when is_list(parts) do
Enum.filter(parts, fn p ->
case p do
%{"text" => text} -> text && text != ""
_ -> false
end
end)
end
@doc """
Return the content parts for the message.
"""
@spec get_message_contents(MessageDelta.t() | Message.t()) ::
[%{String.t() => any()}] | nil
def get_message_contents(%{content: content} = _message) when is_binary(content) do
[%{"text" => content}]
end
def get_message_contents(%{content: contents} = _message) when is_list(contents) do
Enum.map(contents, &for_api/1)
end
def get_message_contents(%{content: nil} = _message) do
nil
end
defp map_role(role) do
case role do
:assistant -> :model
:tool -> :function
# System prompts are not supported yet. Google recommends using user prompt.
:system -> :user
role -> role
end
end
defp get_token_usage(%{"usageMetadata" => usage} = _response_body) do
# extract out the reported response token usage
TokenUsage.new!(%{
input: Map.get(usage, "promptTokenCount", 0),
output: Map.get(usage, "candidatesTokenCount", 0),
raw: usage
})
end
defp get_token_usage(_response_body), do: nil
defp unmap_role("model"), do: "assistant"
defp unmap_role("function"), do: "tool"
defp unmap_role(role), do: role
@doc """
Determine if an error should be retried. If `true`, a fallback LLM may be
used. If `false`, the error is understood to be more fundamental with the
request rather than a service issue and it should not be retried or fallback
to another service.
"""
@impl ChatModel
@spec retry_on_fallback?(LangChainError.t()) :: boolean()
def retry_on_fallback?(%LangChainError{type: "rate_limited"}), do: true
def retry_on_fallback?(%LangChainError{type: "rate_limit_exceeded"}), do: true
def retry_on_fallback?(%LangChainError{type: "timeout"}), do: true
def retry_on_fallback?(%LangChainError{type: "too_many_requests"}), do: true
def retry_on_fallback?(_), do: false
@doc """
Generate a config map that can later restore the model's configuration.
"""
@impl ChatModel
@spec serialize_config(t()) :: %{String.t() => any()}
def serialize_config(%ChatVertexAI{} = model) do
Utils.to_serializable_map(
model,
[
:endpoint,
:model,
:temperature,
:top_p,
:top_k,
:thinking_config,
:receive_timeout,
:json_response,
:json_schema,
:stream
],
@current_config_version
)
end
@doc """
Restores the model from the config.
"""
@impl ChatModel
def restore_from_map(%{"version" => 1} = data) do
ChatVertexAI.new(data)
end
end